@peccul is peccu

(love peccu '(emacs lisp cat outdoor bicycle mac linux coffee))

Caveman2を試しに起動し、pm2でプロセス管理してみるところまで

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.

github.com

前提

  • 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というぬるま湯で育っているのでわからないことが多い。

雛形を動かすまで

READMEのQuickstartに従う。

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

見た目

f:id:peccu:20161122115737p:plain

以下は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.
  • FastCGI 利用の場合はfcgiのインストールが必要
% 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の設定ファイルを書いてみる。