upstart, pty-keeper, reptyr, socat - ターミナルアプリをリモートサーバでデーモン化する方法(Earthquakeをサーバで実行するようにした)

試行錯誤で6日かかったンゴ……普通に嵌ってしんどかった……。1 release/dayが途絶えて残念なり……。一応、毎日勤勉に取り組んでたんですけどねぇ。でも最後はかなりシンプルになって良かった。似たような方法でターミナルアプリは全て同じ方法でデーモン化して接続できるようになると思います。

例はEarthquakeです、というか、Earthquakeをデーモン化するためにいろいろ調べた。

要件

  1. Earthquakeをデーモン化したい
  2. ローカルから1コマンドで接続
  3. ssh認証したい

1は先日event_chainsave_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二回という風に対応してるのですが、RubyProcess.daemonupstartが追えないプロセス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=0rawなんかを試したんですがうまくいきませんでした。

次に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

「多分、落ちてる」ってw。
ufw かわいいよ ufw。もう Ubuntu から離れられないっ!!

リモートの 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})

プライオリティが設定できるので赤くなったりしてこっちの方が嬉しい!