Common Lispでウェブアプリを作りたい。これは雛形を生成してサーバーを起動するまでの話。
ningleとCaveman2の違いがわからず迷ったが、READMEのこの文より Caveman2 を選んだ。
One of the most frequently asked questions was "Which should I use ningle or Caveman? What are the differences?" I think it was because the roles of them were too similar. Both of them are saying "micro" and no database support.
Caveman2 is no more "micro" web application framework. It supports CL-DBI and has database connection management by default. Caveman has started growing up.
前提
- OS X
- roswellがインストールされている
% ros version roswell 0.0.6.69 build with Apple LLVM version 8.0.0 (clang-800.0.42.1) libcurl=7.49.1 Quicklisp=2016-02-22 Dist=2016-10-31 lispdir='/usr/local/Cellar/roswell/0.0.6.69/etc/roswell/' configdir='/Users/peccu/.roswell/'
- clackがインストールされている (
ros install clack
) - 私はCommon Lispの流儀がわからなくて試行錯誤している。Common Lispでは普通のことでつまずいている気がする。Node.jsというぬるま湯で育っているのでわからないことが多い。
雛形を動かすまで
Caveman2のインストール
REPLを起動してql:quickload
を実行する
% ros run * (ql:quickload :caveman2) To load "caveman2": Load 1 ASDF system: caveman2 ; Loading "caveman2" .................. (略) (:CAVEMAN2)
雛形の生成
続けてREPLで実行する。
* (caveman2:make-project #P"/path/to/myapp/" :author "peccu" :name "myapp") writing /path/to/myapp/myapp.asd writing /path/to/myapp/myapp-test.asd writing /path/to/myapp/app.lisp writing /path/to/myapp/README.markdown writing /path/to/myapp/.gitignore writing /path/to/myapp/db/schema.sql writing /path/to/myapp/src/config.lisp writing /path/to/myapp/src/db.lisp writing /path/to/myapp/src/main.lisp writing /path/to/myapp/src/view.lisp writing /path/to/myapp/src/web.lisp writing /path/to/myapp/static/css/main.css writing /path/to/myapp/t/myapp.lisp writing /path/to/myapp/templates/index.html writing /path/to/myapp/templates/_errors/404.html writing /path/to/myapp/templates/layouts/default.html T *
ファイルが生成される。
:name
を省略すると"caveman"というプロジェクトが生成された。
"caveman"のまま進めたところasdfのパスが通っておらず、次のql:quickload
でローカルプロジェクトではなくQuicklispのcavemanがロードされて混乱した。
雛形の修正
app.lisp先頭のql:quickload
でプロジェクトのasdファイルが読み込めてなさそうだったので、asdfのパスに追加する。
c.f. Quicklisp beta FAQ
下記内容をapp.lispの先頭に追加した。
(push #P"/path/to/myapp/" asdf:*central-registry*)
これがないと以下のようなエラーとスタックトレースが出力された。
% APP_ENV=development clackup --server :hunchentoot --port 8080 app.lisp Unhandled QUICKLISP-CLIENT:SYSTEM-NOT-FOUND: System "myapp" not found Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1002DE7953}> 0: ((LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX)) 1: (SB-IMPL::CALL-WITH-SANE-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {100512C18B}>) 2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {100512C15B}>) 3: (PRINT-BACKTRACE :STREAM #<SYNONYM-STREAM :SYMBOL SB-SYS:*STDERR* {10001530A3}> :START 0 :FROM :INTERRUPTED-FRAME :COUNT NIL :PRINT-THREAD T :PRINT-FRAME-SOURCE NIL :METHOD-FRAME-STYLE NIL) 4: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<QUICKLISP-CLIENT:SYSTEM-NOT-FOUND {1005129713}> #<unavailable argument>) 5: (SB-DEBUG::RUN-HOOK *INVOKE-DEBUGGER-HOOK* #<QUICKLISP-CLIENT:SYSTEM-NOT-FOUND {1005129713}>) 6: (INVOKE-DEBUGGER #<QUICKLISP-CLIENT:SYSTEM-NOT-FOUND {1005129713}>) 7: (CERROR "Try again" QUICKLISP-CLIENT:SYSTEM-NOT-FOUND :NAME "myapp") 8: ((LABELS QUICKLISP-CLIENT::RECURSE :IN QUICKLISP-CLIENT::COMPUTE-LOAD-STRATEGY) "myapp") 9: (QL-DIST::CALL-WITH-CONSISTENT-DISTS #<CLOSURE (LAMBDA NIL :IN QUICKLISP-CLIENT::COMPUTE-LOAD-STRATEGY) {1005110E7B}>) 10: (QUICKLISP-CLIENT::COMPUTE-LOAD-STRATEGY #<unavailable argument>) 11: (QUICKLISP-CLIENT::AUTOLOAD-SYSTEM-AND-DEPENDENCIES "myapp" :PROMPT NIL) 12: ((:METHOD QL-IMPL-UTIL::%CALL-WITH-QUIET-COMPILATION (T T)) #<unavailable argument> #<CLOSURE (FLET QUICKLISP-CLIENT::QL :IN QUICKLISP-CLIENT:QUICKLOAD) {1005110D3B}>) [fast-method] 13: ((:METHOD QL-IMPL-UTIL::%CALL-WITH-QUIET-COMPILATION :AROUND (QL-IMPL:SBCL T)) #<QL-IMPL:SBCL {1006F12043}> #<CLOSURE (FLET QUICKLISP-CLIENT::QL :IN QUICKLISP-CLIENT:QUICKLOAD) {1005110D3B}>) [fast-method] 14: ((:METHOD QUICKLISP-CLIENT:QUICKLOAD (T)) #<unavailable argument> :PROMPT NIL :SILENT NIL :VERBOSE NIL) [fast-method] 15: (QL-DIST::CALL-WITH-CONSISTENT-DISTS #<CLOSURE (LAMBDA NIL :IN QUICKLISP-CLIENT:QUICKLOAD) {100510374B}>) 16: (SB-INT:SIMPLE-EVAL-IN-LEXENV (QUICKLISP-CLIENT:QUICKLOAD :MYAPP) #<NULL-LEXENV>) 17: (EVAL (QUICKLISP-CLIENT:QUICKLOAD :MYAPP)) 18: (CLACK::EVAL-FILE #P"/path/to/myapp/app.lisp") 19: (CLACK:CLACKUP "app.lisp" :USE-THREAD NIL :SERVER :HUNCHENTOOT :PORT 8080) 20: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROS:*ARGV*) #<NULL-LEXENV>) 21: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) #<NULL-LEXENV>) 22: (EVAL-TLF (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) NIL #<NULL-LEXENV>) 23: ((FLET SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) NIL) 24: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {10031F3A63}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading") 25: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {10031F3A63}> NIL) 26: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {10031F3A63}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT) 27: ((FLET ROS::BODY :IN ROS:SCRIPT) #<SB-SYS:FD-STREAM for "file /Users/peccu/.roswell/bin/clackup" {10031ECD23}>) 28: (ROS:SCRIPT :SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":hunchentoot" "--port" "8080" "app.lisp") 29: (ROS:RUN ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":hunchentoot" "--port" "8080" "app.lisp") (:QUIT NIL))) 30: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROS:RUN (QUOTE ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":hunchentoot" "--port" "8080" "app.lisp") (:QUIT NIL)))) #<NULL-LEXENV>) 31: (EVAL (ROS:RUN (QUOTE ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":hunchentoot" "--port" "8080" "app.lisp") (:QUIT NIL)))))[f:id:peccu:20161122115737p:plain] 32: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/Cellar/roswell/0.0.6.69/etc/roswell/init.lisp\"))") (:EVAL . "(ros:quicklisp)") (:EVAL . "(ros:run '((:script \"/Users/peccu/.roswell/bin/clackup\"\"--server\"\":hunchentoot\"\"--port\"\"8080\"\"app.lisp\")(:quit ())))"))) 33: (SB-IMPL::TOPLEVEL-INIT) 34: ((FLET #:WITHOUT-INTERRUPTS-BODY-85 :IN SAVE-LISP-AND-DIE)) 35: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE)) unhandled condition in --disable-debugger mode, quitting
アプリケーションの起動
clackup
コマンドにapp.lispを指定するとlocalhostで起動する。デフォルトのサーバーはHunchentootの模様。
% APP_ENV=development clackup --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Hunchentoot server is going to start. Listening on localhost:8080.
localhost へのリンク Welcome to Caveman2
見た目
以下はCaveman2というよりもClackを動かすために、という話の記録。
- Wookie 利用の場合はlibuvのインストールが必要
% APP_ENV=development clackup --server :wookie --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" .................................................. [package myapp.config]............................ [package myapp.view].............................. [package myapp.djula]............................. [package myapp.db]................................ [package myapp]................................... [package myapp.web]. Unhandled LOAD-FOREIGN-LIBRARY-ERROR: Unable to load any of the alternatives: ("libuv.dylib") (トレース:略) % brew install libuv (略) % APP_ENV=development clackup --server :wookie --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Wookie server is going to start. Listening on localhost:8080.
% APP_ENV=development clackup --server :fcgi --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Unhandled LOAD-FOREIGN-LIBRARY-ERROR: Unable to load any of the alternatives: (PATH "libfcgi.dylib") (トレース:略) % brew install fcgi (略) % APP_ENV=development clackup --server :fcgi --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Fcgi server is going to start. Listening on localhost:8080.
- Woo 利用の場合libevのインストールが必要
% APP_ENV=development clackup --server :woo --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Unhandled CFFI:LOAD-FOREIGN-LIBRARY-ERROR: Unable to load any of the alternatives: ("libev.4.dylib" "libev.4.so" "libev.so.4" "libev.dylib" "libev.so") % brew install libev (略) % APP_ENV=development clackup --server :woo --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Woo server is going to start. Listening on localhost:8080.
- Toot 利用はうまくいかなかった
% APP_ENV=development clackup --server :toot --port 8080 app.lisp To load "myapp": Load 1 ASDF system: myapp ; Loading "myapp" ................................. Toot server is going to start. Listening on localhost:8080. Unhandled SIMPLE-ERROR: There is no applicable method for the generic function #<STANDARD-GENERIC-FUNCTION (SETF TOOT::ACCEPTOR-PROCESS) (1)> when called with arguments (NIL #<TOOT::SINGLE-THREADED-TASKMASTER {1009D0ABA3}>). Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1002DE7933}> 0: ((LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX)) 1: (SB-IMPL::CALL-WITH-SANE-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {100A0C758B}>) 2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<CLOSURE (LAMBDA NIL :IN SB-DEBUG::FUNCALL-WITH-DEBUG-IO-SYNTAX) {100A0C755B}>) 3: (PRINT-BACKTRACE :STREAM #<SYNONYM-STREAM :SYMBOL SB-SYS:*STDERR* {10001530A3}> :START 0 :FROM :INTERRUPTED-FRAME :COUNT NIL :PRINT-THREAD T :PRINT-FRAME-SOURCE NIL :METHOD-FRAME-STYLE NIL) 4: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "~@<There is no applicable method for the generic function ~2I~_~S~ ~I~_when called with arguments ~2I~_~S.~:>" {100A0C3383}> #<unavailable argument>) 5: (SB-DEBUG::RUN-HOOK *INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "~@<There is no applicable method for the generic function ~2I~_~S~ ~I~_when called with arguments ~2I~_~S.~:>" {100A0C3383}>) 6: (INVOKE-DEBUGGER #<SIMPLE-ERROR "~@<There is no applicable method for the generic function ~2I~_~S~ ~I~_when called with arguments ~2I~_~S.~:>" {100A0C3383}>) 7: (ERROR "~@<There is no applicable method for the generic function ~2I~_~S~ ~I~_when called with arguments ~2I~_~S.~:>" #<STANDARD-GENERIC-FUNCTION (SETF TOOT::ACCEPTOR-PROCESS) (1)> (NIL #<TOOT::SINGLE-THREADED-TASKMASTER {1009D0ABA3}>)) 8: ((:METHOD NO-APPLICABLE-METHOD (T)) #<STANDARD-GENERIC-FUNCTION (SETF TOOT::ACCEPTOR-PROCESS) (1)> NIL #<TOOT::SINGLE-THREADED-TASKMASTER {1009D0ABA3}>) [fast-method] 9: (SB-PCL::CALL-NO-APPLICABLE-METHOD #<STANDARD-GENERIC-FUNCTION (SETF TOOT::ACCEPTOR-PROCESS) (1)> (NIL #<TOOT::SINGLE-THREADED-TASKMASTER {1009D0ABA3}>)) 10: (CLACK.HANDLER.TOOT:RUN #<CLOSURE (LAMBDA (LACK.MIDDLEWARE.BACKTRACE::ENV) :IN "/Users/peccu/.roswell/lisp/quicklisp/dists/quicklisp/software/lack-20161031-git/src/middleware/backtrace.lisp") {1007B3D64B}> :ALLOW-OTHER-KEYS T :PORT 8080 :DEBUG T :USE-THREAD NIL) 11: (CLACK.HANDLER:RUN #<CLOSURE (LAMBDA (LACK.MIDDLEWARE.BACKTRACE::ENV) :IN "/Users/peccu/.roswell/lisp/quicklisp/dists/quicklisp/software/lack-20161031-git/src/middleware/backtrace.lisp") {1007B3D64B}> :TOOT :PORT 8080 :DEBUG T :USE-THREAD NIL) 12: (CLACK:CLACKUP "app.lisp" :USE-THREAD NIL :SERVER :TOOT :PORT 8080) 13: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROS:*ARGV*) #<NULL-LEXENV>) 14: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) #<NULL-LEXENV>) 15: (EVAL-TLF (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) NIL #<NULL-LEXENV>) 16: ((FLET SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROS:QUIT (APPLY (QUOTE MAIN) ROS:*ARGV*)) NIL) 17: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {10031F2A73}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading") 18: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {10031F2A73}> NIL) 19: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {10031F2A73}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT) 20: ((FLET ROS::BODY :IN ROS:SCRIPT) #<SB-SYS:FD-STREAM for "file /Users/peccu/.roswell/bin/clackup" {10031EB9E3}>) 21: (ROS:SCRIPT :SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":toot" "--port" "8080" "app.lisp") 22: (ROS:RUN ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":toot" "--port" "8080" "app.lisp") (:QUIT NIL))) 23: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROS:RUN (QUOTE ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":toot" "--port" "8080" "app.lisp") (:QUIT NIL)))) #<NULL-LEXENV>) 24: (EVAL (ROS:RUN (QUOTE ((:SCRIPT "/Users/peccu/.roswell/bin/clackup" "--server" ":toot" "--port" "8080" "app.lisp") (:QUIT NIL))))) 25: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/Cellar/roswell/0.0.6.69/etc/roswell/init.lisp\"))") (:EVAL . "(ros:quicklisp)") (:EVAL . "(ros:run '((:script \"/Users/peccu/.roswell/bin/clackup\"\"--server\"\":toot\"\"--port\"\"8080\"\"app.lisp\")(:quit ())))"))) 26: (SB-IMPL::TOPLEVEL-INIT) 27: ((FLET #:WITHOUT-INTERRUPTS-BODY-85 :IN SAVE-LISP-AND-DIE)) 28: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE)) unhandled condition in --disable-debugger mode, quitting
常時起動についての疑問
一般的なCommon Lispウェブアプリだとログとプロセス管理ってどうするんだろう。
systemd、launchd等になるんだろうか。それともtmuxとかで実行しておいて、いつでもREPLでデバッグできるようにしておくんだろうか。
その場合はclackup
コマンドじゃなくてREPLからアプリを起動しないといけないか。
Node.jsではpm2を使っている。Common Lispでも似たようなものがあるのか、はたまた各社で自作しているのか。
Caveman2のドキュメントでは perlの Server::Starter モジュールを利用していた。ログは設定ファイルの :error-log
に指定したファイルに書き出されるとのこと。
pm2で試してみた。
- 起動スクリプト作成
試しに
pm2.sh
というファイルを作成し、実行権限をつける
APP_ENV=development clackup --server :woo --port 8080 app.lisp
- pm2で起動する
% pm2 start pm2.sh --name caveman [PM2] Starting /path/to/myapp/pm2.sh in fork_mode (1 instance) [PM2] Done. ┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────┬──────────┬──────────┐ │ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ watching │ ├──────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────┼──────────┼──────────┤ │ caveman │ 0 │ fork │ 5186 │ online │ 0 │ 0s │ 0% │ 1.1 MB │ disabled │ └──────────┴────┴──────┴──────┴────────┴─────────┴────────┴─────┴──────────┴──────────┘ Use `pm2 show <id|name>` to get more details about an app
これで普通に起動していて、pm2 logs caveman
でログが確認でき、pm2 restart caveman
で再起動する。startOrRestart
はできなかった。pm2 startup
してあればpm2 save
でマシン再起動後も起動してくれるだろう。
また追ってpm2.jsonの設定ファイルを書いてみる。