upstart, pty-keeper, reptyr, socat - ターミナルアプリをリモートサーバでデーモン化する方法(Earthquakeをサーバで実行するようにした)
試行錯誤で6日かかったンゴ……普通に嵌ってしんどかった……。1 release/dayが途絶えて残念なり……。一応、毎日勤勉に取り組んでたんですけどねぇ。でも最後はかなりシンプルになって良かった。似たような方法でターミナルアプリは全て同じ方法でデーモン化して接続できるようになると思います。
例はEarthquakeです、というか、Earthquakeをデーモン化するためにいろいろ調べた。
要件
- Earthquakeをデーモン化したい
- ローカルから1コマンドで接続
- ssh認証したい
1は先日event_chainとsave_imageってEarthquakeプラグイン作ったので、サーバに常駐させときたいなぁということです。ローカルはラップトップなので閉じてることあるから。
2は基本的には常時接続なんですが、やっぱ気が向いた時にすぐ見れなきゃまずいっしょ。
3は外から誰でも接続できると嫌なんで。危ない。
ソリューション
とりあえず、ざっくり解決法書きます。解説は後で。
リモートサーバ
reptyrとsocatをインストールします。UbuntuでやってるのでOSに合わせて適度に読み替えてください。
$ sudo apt-get install reptyr $ sudo apt-get install socat
reptyrは別のpty(pseoudo tty)で動いてるプロセスを手元のptyで乗っ取るソフトウェアです。 socatはnetcatの高機能版みたいなもんで、ネットワーク用途に限らない色々なことができます。今回はコマンド実行して終了するんじゃなくて、リモートサーバへの接続を維持するために使ってます。
そして、拙作のpty-keeperをダウンロードします。
$ wget https://gist.github.com/babie/7297810/raw/ae3793ae39b3165e0c50f7ca4a05fb00921a9cdd/pty-keeper $ chmod +x pty-keeper
適当な場所に置いてね。
ptyを持たせて子プロセスを起動しながら、自分から子プロセスのIOへ読み書きができなくなっても、子プロセスが生きている限りは死なない、っつーソフトウェアが見つからなかったので作りました。
要点は、PTY.spawn
でpty付きでspawnするのと、SIGCHLD
,SIGCLD
は子プロセスが死んだ時だけじゃなくて、ptyが変更された時なんかも飛んでくるので、ホントに死んだかどうか確かめるってとこっすね。汎用的にしなければ10行です。
んで、Upstart用設定ファイル/etc/init/earthquake.conf
を作ります。
これでサーバ起動時に自動で立ち上がります。ユーザーやパスは環境に合わせて適度に改変してください。
HOME
はEarthquakeが設定ディレクトリを探すために、LANG
はEarthquakeへ文字入力するときに日本語が化けるのを防ぐのに、COLUMNS
はpty及びreadlineがデフォルト80を採用しそれ以上の入力すると強制的に行頭に戻されてつらいので、PATH
はEarthquake及びpty-keeperがRubyを探すために必要です。
--no-daemon
オプションを付けてるのは、upstartはプロセスIDを追跡する記述があって、expect fork
はfork一回、expect daemon
はfork二回という風に対応してるのですが、RubyのProcess.daemon
はupstartが追えないプロセスIDの変わり方してるみたいなんで付けざるを得ませんでした。pty-keeper自体はconsole none
でどのtty/ptyにも接続してないので問題ありません。
プロセスの起動・終了を何に任せるかはいろんな方法がありますよね。
昔ながらのstart-stop-daemon
使って/etc/init.d/earthquake
書いてたんだけど、ほら、sysvinitってrespawn(プロセスを監視して自動再起動)ないじゃないっすかー。んでその後、daemon
使ってやってたんすけど、--pty
オプションがうまく使えなかった。これデバッグ用でupstart(init)は元々pty持ってないからかな?んでググると、なんとUbuntuでは今upstart
が普通っぽいじゃないですかー。こいつはrespawnもうまく捌いてくれるしかっこいいなということでこれ使いました。
最初は直接earthquakeを呼び出すearthquake.conf書いてたんですが、デーモン化はうまくいくんですが、文字入力がダメ。なんか行単位のバッファになってるみたいなんすよね。socatのオプションでicanon=0
やraw
なんかを試したんですがうまくいきませんでした。
次にemptyを介したearthquake.confを書いたんですが、これはデーモン化も文字入力も問題なかったんですけど、earthquakeプロセスが2つできちゃう。デフォルトのEarthquake使うならこれでも良かったんですが、私が使ってるプラグインにタイムラインを監視してイベントに応じてアクションするってのがあるんすよね。具体的にはfavった画像つきツイートの画像を保存するって奴なんですが、画像が2個保存されて宜しくない。どうしてプロセス2つになるかというと、reptyrがpty乗っ取ったと同時に読み書きできる子プロセスがいなくなったemptyが自殺してupstartが再起動かけるって具合です。このempty、Ubuntuでのパッケージ名がempty-expectとなっているように、ptyを生成してコマンドを起動してそいつと対話するexpectなんですわ。なので子プロセス自体が生きていてもIOが読み書きできなくなったら速やかに死ぬようになってんですね。そもそもの目的がそれなんだからしょうがない、他の用途に使う俺が悪かった。
あ、この設定ファイル、密かにrbenv環境のupstart設定の書き方にもなってますね。
手動で立ち上げるときは
$ sudo start earthquake
です。この辺はinitctl
辺りを調べてください。
ローカルホスト
.zshrcに以下を追記
別にaliasでもいいと思うんですけど、私はwhich
ですぐ見れるようにfunctionにしてます。設定再読み込みも忘れずに。
moshじゃなくてsshなのは、moshは接続を安定させるために複数のコネクションを張るので、プロセスが複数実行されて悲しいことになります。
socatは2つのブロックの引数を取り、STDIN
がこっちの標準入力、SYSTEM
があちらでshを実行することを意味してます。
STDIN
のオプション、echo=0
は文字が二重に表示されるのを防ぐため、icanon=0
は非カノニカルモードにして行単位ではなく文字単位で書き込みするために使ってます。詳しくはTERMIOSを読んで下さい。
SYSTEM
は引数なしのコマンドを実行するならEXEC
でもいいんですが、今回は引数に加えてshの実行も必要だったのでこれにしました。オプションのpty
はリモートサーバでpty作るかどうか、stderr
はSTDERRもSTDOUTにリダイレクトしてくれるやつ、ignbrk=1
はこれまたTERMIOS関連でシグナルのC(Ctrl-c)を無視するかどうかですね。どっちにしろsocat/reptyrの終了でearthquakeも再起動するんで今回はあんま意味ないんですが。
reptyrにオプション-s
を付けてますが、これはプロセスにtty/ptyが接続されてない時に使うもので不要なんですが、オプションなしの既存のptyを乗っ取るより、オプションありの新しくpty作って繋ぎ変える方が速くて安定してるみたいなので付けました。
あかんかったわ。-sオプションつけると複数行や長文の入力に難あり。取り除いたら無事できました。遅延じゃろか。
これで、
$ earthquake
とすればリモートで動いているEarthquakeに接続しTwitterを楽しめます。
次回予告
はてなダイアリーに予約投稿がなくてつらいので何とかしたいと思います。んだけど、MacBook Proが届いて環境設定しないとアカンのでちょっと遅れるかも。
Ubuntu に Redmine をインストールする(ただし RVM で)
目標:
- Ubuntu 10.04。安定版なのよー。多分新しいやつでも大丈夫。
- Nginx 使う。ちょっぱやらしいんで。
- RVM 使う。Redmine の要求する Ruby 1.8.7 はともかく、Rails 2.3.11 ってどういうことよ!ってわけで汚したくないんで。
- REE(Ruby Enterprise Edition)使う。メモリ抑えたい。
- Passenger の Nginx モジュール使う。
- Redmine は 1.2.1。
いやー、色々なブログを放浪しましたが、結局4.5時間ぐらいかかってしましましたよ。お客さん、ラッキー!これが、最短だ!!
# Nginx(の起動スクリプト)インストール(本体は使わない) $ aptitude install nginx # MySQL インストール $ sudo aptitude install mysql-server libmysql++-dev $ sudo update-rc.d mysql defaults 64 36 # RVM を最新に $ rvm get latest $ rvm reload $ rvm repair all # REE インストール $ rvm pkg install zlib $ rvm pkg install readline $ rvm pkg install openssl $ rvm pkg install iconv $ rvm install ree --with-readline-dir=$rvm_usr_path --with-iconv-dir=$rvm_usr_path --with-zlib-dir=$rvm_usr_path --with-openssl-dir=$rvm_usr_path # for Redmine $ rvm use ree $ rvm use gemset global # あー、ここで redmine って名前の gemset 作れば良かった $ rvm rubygems 1.5.2 # 1.5.2より上の場合はダウングレード $ gem install rack -v=1.1.1 $ gem install rake -v=0.8.7 $ gem uninstall rake -v=0.9.2 $ gem install i18n -v=0.4.2 $ gem install mysql $ gem install rails -v=2.3.11 # MySQL 設定 $ vi /etc/mysql/my.cnf [mysqld] default-character-set=utf8 # 追加 [mysql] default-character-set=utf8 # 追加 $ sudo /etc/init.d/mysql restart $ mysql -uroot -p(パスワード) mysql> create database redmine default character set utf8; mysql> grant all on redmine.* to redmine identified by '(Redmine の DB 接続用のパスワード)'; mysql> flush privileges; mysql> exit # Redmine $ cd ~/dev/ # お好きな場所に $ git clone git://github.com/edavis10/redmine.git # オフィシャルクローンリポジトリらしい $ cd redmine $ git checkout -b 1.2.1 refs/tags/1.2.1 $ vi config/database.yml production: adapter: mysql database: redmine host: localhost username: redmine password: (Redmine の DB 接続用のパスワード) encoding: utf8 $ cp config/configuration.yml.example config/configuration.yml $ vi config/configuration.yml # メール送信設定のサンプルがたくさんついてるのであなたのお好みの設定で。私は gmail にした。 production: email_delivery: delivery_method: :smtp smtp_settings: tls: true address: "smtp.gmail.com" port: 587 domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps authentication: :plain user_name: "(メールアドレス)" password: "(メールアカウントのパスワード)" $ rake generate_session_store $ rake db:migrate RAILS_ENV=production # Passenger $ aptitude install libcurl4-openssl-dev $ gem install passenger $ rvmsudo passenger-install-nginx-module # なんか、Nginx をダウンロードしてインストールするぜー、って言われる Automatically download and install Nginx? 1. Yes: download, compile and install Nginx for me. (recommended) 2. No: I want to customize my Nginx installation. (for advanced users) Enter your choice (1 or 2) or press Ctrl-C to abort: 1 #<= 1 を選んで Passernger 用のの Nginx をインストール。 $ sudo vi /opt/nginx/conf/nginx.conf user www-data; #<= コレ大事 worker_processes 1; error_log /var/log/nginx/error.log; pid /var/run/nginx.pid; #<= コレ大事 ... http { ... passenger_root /home/babie/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.9; passenger_ruby /home/babie/.rvm/wrappers/ree-1.8.7-2011.03@global/ruby; ... } ... server { listen 80; server_name (あなたのサーバのドメイン名); root /home/babie/dev/redmine/public; # <--- Redmine の 'public' ディレクトリへのパス passenger_enabled on; ... # この辺コメントアウト #location / { # root html; # index index.html index.htm; #} ... #error_page 500 502 503 504 /50x.html; #location = /50x.html { # root html; #} ... } $ sudo vi /etc/init.d/nginx # Ubuntu 標準の Nginx から Passenger 用の Nginx にすり替える ... #DAEMON=/usr/sbin/nginx DAEMON=/opt/nginx/sbin/nginx .... # Nginx 起動 $ sudo /etc/init.d/nginx start # ufw でファイアウォール管理してる人のみ。ポートを空ける $ sudo ufw allow 80 # Redmine ユーザー(www-data)が書き込めるようにパーミッションを変更 $ sudo chown -R babie:www-data files log tmp public/plugin_assets $ sudo chmod -R 775 files log tmp public/plugin_assets
長い戦いだった。後はここらへんを参考に設定していけばいいはず。
Nginx も REE も超速くて幸せ。
MongoDBが起動しなくなったときの対処法(公式Ubuntuパッケージの場合)
なんかさくらのVPSが勝手にリスタートしたみたいで、MongoDBが起動しなくなった。
モッピー知ってるよ、--repair コマンド使えばいいんだよね。
$ sudo mongod --dbpath=/var/lib/mongodb --repair
あれれー?mongodb.lock ファイル消えないし、起動できないし。
ググると、公式 Ubuntu パッケージの場合はちょっと対処法がちがうっぽい:
$ sudo rm /var/lib/mongodb/mongod.lock $ sudo -u mongodb mongod -f /etc/mongodb.conf --repair $ sudo /etc/init.d/mongodb start
これでいけた。ふぅ・・・
Git サーバー gitosis のインストールと設定
github を利用しようかなーと思ったんですけど、複数人で秘密リポジトリが使えるプランは、零細企業には高いっすわー。ということで、せっかく遠隔ペアプログラミング用にさくらのVPSを借りているので、そこに Git サーバーを立てることにしました。
インストール:
$ sudo aptitude install gitosis
あ、Ubuntu なので。他のディストリビューションは知らん。
初期化します。同時に設定を嬲れるユーザーの公開鍵を登録します:
$ sudo -H -u gitosis gitosis-init < ~/.ssh/id_rsa.pub
設定は、gitosis サーバーから gitosis-admin というリポジトリを clone して、編集して、commit して、push することで行います。
以下では sample というリポジトリを、私と test@example.com というユーザーが読み書きできるように設定してみます。
設定ファイルを格納している gitosis-admin リポジトリを取得:
$ git clone gitosis@localhost:gitosis-admin.git
新規リポジトリ用の設定を追加:
$ cd gitosis-admin $ vi gitosis.conf [group sample] writable = sample members = babie@example.com test@example.com
test ユーザーの公開鍵を配置:
$ cd keydir $ ls babie@example.com.pub $ sudo cp ~test/.ssh/id_rsa.pub . $ ls -l -rw-rw-r-- 1 babie babie 404 2011-04-21 10:38 babie@example.com.pub -rw-r--r-- 1 root root 399 2011-04-21 10:43 id_rsa.pub $ sudo chown babie:babie id_rsa.pub $ ls -l -rw-rw-r-- 1 babie babie 404 2011-04-21 10:38 babie@example.com.pub -rw-r--r-- 1 babie babie 399 2011-04-21 10:43 id_rsa.pub $ mv id_rsa.pub test@example.com.pub $ ls babie@example.com.pub test@example.com.pub
設定変更を反映:
$ cd .. $ git status # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: gitosis.conf # # Untracked files: # (use "git add <file>..." to include in what will be committed) # # keydir/test@example.com.pub no changes added to commit (use "git add" and/or "git commit -a") $ git add . $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: gitosis.conf # new file: keydir/test@example.com.pub # $ git commit -m "add sample repos and test user" $ git push origin master
んで、リポジトリのプレースホルダができたので、リポジトリを push すれば共有できます:
$ cd ~/dev/sample $ git init . $ git add . $ git commit -m 'initial commit' $ git remote add origin gitosis@localhost:sample.git $ git push origin master
以上。
リポジトリをブラウザで閲覧できる gitweb のインストールと設定はまた今度。
Ubuntu 10.04 で V8 JavaScript エンジンのシェル(d8)をビルド
ペアトレーニング用に V8 のシェルが欲しかったんだけど、libv8-2.0.3, libv8-dev にはどうも付属されてないようなので、しょうがないからコンパイルした。x86_64 環境だからちょっと苦労した。
$ sudo aptitude install subversion scons $ cd ~/dev/ $ svn co http://v8.googlecode.com/svn/trunk/ v8-read-only $ cd v8-read-only $ sudo aptitude install ia32-libs lib32z1-dev lib32bz2-dev lib32readline-dev $ sudo ln -s /usr/lib32/libstdc++.so.6.0.13 /usr/lib32/libstdc++.so $ scons d8 console=readline
色々試行錯誤した結果、これが最短経路っぽい。Ubuntu 10.10 でも、libstdc++ のバージョン番号に注意すれば問題ないんじゃないかなー。
32bit 環境だったら、32 が含まれている行は飛ばして下さい。つーか、64bit でコンパイルできないの?なんでなんで〜?
まぁ、とにかく、
$ ./d8 V8 version 3.2.1.1 [console: readline] d8> print("Hello, world!"); Hello, world! d8> quit();
ってな感じで、できました。
さくらのVPSでファイアーウォールが何も設定されていないのに驚愕したが ufw で解決
さくらのVPS ってデフォルトではファイアーウォールの設定何もされてないという記事をみて驚愕した。と、よく考えたら Ubuntu 10.04 LTS を再インストールしたから、どっちにしても初期状態だな。
とりあえず確かめた。
$ sudo iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination
オウフ、デフォルトは空なのか……
iptables の設定めんどくせぇなーどうしようかなー、と思っていたら、どうも Ubuntu では ufw という iptables の(というか NetFilter の)ラッパーがあるらしいので、使ってみることにした。
$ sudo ufw default deny # デフォルトは全部拒否 $ sudo ufw allow 22 # for ssh $ sudo ufw allow 3000 # for rails $ sudo ufw enable # ufw を有効化
わーお!チョー簡単!
ssh を許可する前に ufw を有効化したら、死ねるので注意な!(さくらのVPSならリモートコンソールがあるから何とかなるって id:tenkoma さんがブクマコメで教えてくれたよ!)
うむうむ、ssh も切れてないし、3000番も大丈夫、中からの ping や Growl も大丈夫だし、問題なし!
現在の設定を確認するには status コマンドを使う:
$ sudo ufw status Status: active To Action From -- ------ ---- 22 ALLOW Anywhere 3000 ALLOW Anywhere
enable してないと設定が表示されないのが不親切かなぁ。あと、さらに verbose を付け足すともっと詳しい内容がわかるよ!
どうれ、外からテストだ:
$ nmap xxx.xxx.xxx.xxx Starting Nmap 5.50 ( http://nmap.org ) at 2011-02-20 15:00 JST Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn Nmap done: 1 IP address (0 hosts up) scanned in 3.34 seconds
リモートの VPS から手元の Growl に autotest の通知を受けつけるようにした
※ 追記したから最後まで読んでから実行してね。
VPS から autotest の Growl の通知を受けたい。autotest-growl は Linux 対応してるのかしら?と思ってソースを見たら、どうも notify-send というコマンドに依存しているらしい。
Ubuntu では libnotify-bin というパッケージを入れれば notify-send が入るらしいのだが、こいつが Manpage を見る限りどうもリモートホストに対応してないっぽい。
調べると、ruby-growl というパッケージに growl ってコマンドが付いているらしい。んでこれは Pure Ruby 製で libなんたらとかインストールしないでいい上に -H オプションでリモートホストに対応してるらしい。いいぞ!こいつのラッパースクリプトを書こう!
$ gem install ruby-growl
zsh の人は rehash しておくこと。
Mac では、[システム環境設定]→[Growl]→[ネットワーク]の[受信される通知を開く]と[リモートアプリケーション登録を許可]にチェック。Windows Growl はわからん。
あと、CTU やルーターの設定で UDP 9887 に穴を空けること。growl コマンドは TCP 23052 じゃなくて UDP を使うっぽい。あと、Windows for Growl は UDP に対応して無くて、TCP 23053 らしい。なので、growl コマンドを使ったこの方法ではできないので注意。
growl コマンドをテスト。
$ growl -H xxx.xx.xx.xx -t title -m message
UDP で打ちっ放しなので、ちゃんと穴が開いてなくても普通終了します。頑張れ!
Growl 受け付け側というか今触っているコンピューターのグローバルIPを知るには、
$ echo $SSH_CLIENT xxx.xx.xx.xx 61873 22
こうすれば、こっちの端末がVPS側からどう見えているのか分かる。
んで、ruby-growl の growl コマンドをラップした、オリジナルの notify-send コマンドを作る。
前準備:
$ mkdir -p ~/opt/bin $ export PATH=~/opt/bin:$PATH
PATH の設定は .zshenv にも書いておく。俺はオリジナルコマンドは ~/opt/bin/ に入れることにしてるが、まぁお好きなように。
~/opt/bin/notifiy-send を作成:
#!/usr/bin/env ruby # -*- coding: utf-8 -*- require 'optparse' if __FILE__ == $0 OPTS = { :name => `hostname`, :hostname => ENV["SSH_CLIENT"].split.first, } OptionParser.new do |opt| # for notify-send compatible opt.on("-u LEVEL", "--urgency=LEVEL") {|v| } opt.on("-t TIME", "--expire-time=TIME") {|v| } opt.on("-i ICON", "--icon=ICON[,ICON,...]") {|v| OPTS[:icon] = v } opt.on("-c CATEGORY", "--category=TYPE[,TYPE,...]") {|v| } opt.on("-?", "--help") {|v| puts opt; exit} opt.on("-h", "--hint=TYPE:NAME:VALUE") {|v| } # for ruby-growl opt.on("-H HOSTNAME", "--host HOSTNAME") {|v| OPTS[:hostname] = v } opt.on("-n NAME", "--name NAME") {|v| OPTS[:name] = v } #opt.on("-P PASSWORD", "--password PASSWORD") {|v| OPTS[:password] = v } #opt.on("-s", "--sticky") {|v| OPTS[:sticky] = true } opt.parse!(ARGV) end title = ARGV[0] message = ARGV[1] `growl -H #{OPTS[:hostname]} -t "#{title}" -m "#{message}" -n "#{OPTS[:name]}"` end
notify-send のオプションは受け入れるけどほとんど無視!autotest-growl を動かすための必要最低限しか書きません。
うまく動くかテスト:
$ chmod +x ~/opt/bin/notify-send $ rehash $ notify-send title message
動くっぽい。俺の環境では動いた。
これで安心して autotest-growl を入れられます。
$ gem install autotest-growl
~/.autotest に以下を追加:
require 'autotest/growl'
ラッパースクリプトで --password オプションを有効にした人は、Growl 側で設定した上で Autotest::Growl::custom_options = "--password PASSWORD" とかつけるといいんじゃないかなぁ。
できた!!色つかないけど、まぁよし!!
……とここまでやっておきながら、ラッパースクリプトは必要なくて、autotest-growlの中で、growl コマンドを呼び出せばもっとリッチになることに気づいた……ショック!!
autotest-growl-0.2.9/lib/autotest/growl.rb:96 辺り、
when /linux|bsd/i #system %(notify-send "#{title}" "#{message}" -i #{image} -t 5000 #{@@custom_options}) system %(growl -H #{ENV["SSH_CLIENT"].split.first} -n "#{`hostname`}" -t "#{title}" -m "#{message}" --priority=#{priority} #{'-s' if sticky} #{@@custom_options})
プライオリティが設定できるので赤くなったりしてこっちの方が嬉しい!