プロセス置換を使ってjsvcの標準出力をローテートする。

jsvcの起動オプションの-outfile/-errfileにファイルパスを指定すると、そのファイルに標準出力/標準エラー出力を吐きだすようになる。但しこのファイルは普通にはローテートされないので、何らかの方法でローテートするようにしたい。

例としてTomcat6以降 + jsvcを使っている場合を考える。標準的な起動スクリプトだと下のようになっていると思う。
cf: http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/bin/daemon.sh

#!/bin/sh
...
test ".$CATALINA_OUT" = . && CATALINA_OUT="$CATALINA_BASE/logs/catalina-daemon.out"
...
    start   )
      "$JSVC" $JSVC_OPTS \
      -java-home "$JAVA_HOME" \
      -user $TOMCAT_USER \
      -pidfile "$CATALINA_PID" \
      -wait "$SERVICE_START_WAIT_TIME" \
      -outfile "$CATALINA_OUT" \
      -errfile "&1" \
      -classpath "$CLASSPATH" \
      "$LOGGING_CONFIG" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" \
      -Dcatalina.base="$CATALINA_BASE" \
      -Dcatalina.home="$CATALINA_HOME" \
      -Djava.io.tmpdir="$CATALINA_TMP" \
      $CATALINA_MAIN
      exit $?
    ;;
...

これだと「catalina-daemon.out」がどんどん肥大化していくので、これをローテートさせたい。

ググると、logrotateでファイルを移動しKILL -HUPする方法や、mkfifoで作ったファイルをoutfileに指定しつつそれを読み出してrotatelogsに渡す方法があったけど、BashのProcess Substitutionを使えばいいような気がしてやってみたら出来たと思う。


cf: bashのプロセス置換機能を活用して、シェル作業やスクリプト書きを効率化する - 双六工場日誌


まずこれはbashの機能なので起動スクリプトshebangの指定をbashに変える。
それで、jsvcを起動する前に標準出力をrotatelogsへの入力に切り替えてやる。

#!/bin/bash
...
test ".$CATALINA_OUT" = . && CATALINA_OUT="$CATALINA_BASE/logs/catalina-daemon.out"
...
    start   )
      exec 3>&1
      exec 4>&2
      exec 1> >(/usr/local/apache2/bin/rotatelogs "${CATALINA_OUT}.%Y%m%d" 86400 540) 2>&1

      "$JSVC" $JSVC_OPTS \
      -java-home "$JAVA_HOME" \
      -user $TOMCAT_USER \
      -pidfile "$CATALINA_PID" \
      -wait "$SERVICE_START_WAIT_TIME" \
      -outfile "&1" \
      -errfile "&1" \
      -classpath "$CLASSPATH" \
      "$LOGGING_CONFIG" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" \
      -Dcatalina.base="$CATALINA_BASE" \
      -Dcatalina.home="$CATALINA_HOME" \
      -Djava.io.tmpdir="$CATALINA_TMP" \
      $CATALINA_MAIN

      STATUS=$?
      exec 1>&3
      exec 2>&4
      exit $STATUS
    ;;
...

一応元の標準出力らを別のファイルディスクリプタに逃がしておいてあとで戻すようにしている。
Process Substitutionは実体はnamed pipesらしいですがmkfifoするより速いし手軽だしってWikipediaに書いてあった

iPadのSafariでオレオレSSLクライアント証明書を使っていると動画再生できない

イントラ案件だけどiPadで動画を見たいという話があったので、videoタグを使ったHTMLページを作り、Safariから動画を視聴できるようにした。
動画自体は社外秘ではないのだけど、サイト内の他のコンテンツは社外秘なので、SSLクライアント証明書を配布してSSL経由でのアクセスのみ許可するようにしている。(関連: Apache2.2でリモートアドレスベースでの制限 "または" SSLクライアント証明書による認証を行う - terazzoの日記)
ところが、どうやってもSSL経由で動画が再生できない。同じHTML/動画ファイルでも、SSLを使わない場合や、クライアント証明書を使わない場合には再生できたので、どうやら証明書周りが怪しい。

確認したバージョンはこの辺:

結論

videoタグでhttpsを使用した際に、端末にそのドメインに対するクライアント証明書がインストールされている場合、インストールされている証明書の代わりに誤ってApple独自のクライアント証明書が使用されてしまう不具合っぽい。
iOS 7ではまだ調べてないので、直ってなかったら不具合報告しときます。

調べたこと

まずApacheのエラーログを見てみた。以下のように、証明書チェーンが長すぎるとのエラーが出ていた。

[Sun Oct 06 19:20:49 2013] [error] Certificate Verification: Error (19): self signed certificate in certificate chain
[Sun Oct 06 19:20:49 2013] [error] Certificate Verification: Certificate Chain too long (chain has 3 certificates, but maximum allowed are only 1)

今回使用したSSLクライアント証明書は独自に認証局を立てて発行したもので、証明書のチェーンがそんなに長いはずはない。

まあでもチェーン数の最大を増やしてみた。
/etc/httpd/conf.d/ssl.conf:

SSLVerifyDepth 3

もう一度アクセスしてログを確認したところ……

[Sun Oct 06 19:21:39 2013] [error] Certificate Verification: Error (19): self signed certificate in certificate chain

証明書チェーン検証が失敗しているようだ。

確かにオレオレ認証局で署名した証明書なのだが、ちゃんと認証局の証明書ファイルはインストールしているし、動画以外はちゃんと見れる。

/etc/httpd/conf.d/ssl.conf:

SSLCACertificateFile /etc/pki/CA/cacert.pem

どうにも分からないので、ログレベルを上げてもう少し詳しい内容を出力してみた。
/etc/httpd/conf.d/ssl.conf:

ErrorLog logs/ssl_error_log
LogLevel debug

再度アクセスしてログを確認……

[Sun Oct 06 19:22:20 2013] [debug] ssl_engine_kernel.c(1259): Certificate Verification: depth: 3, subject: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA, issuer: /C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA

"CN=Apple Root CA"という知らない認証局の署名に使われてるのでエラーということらしい。
今回用意したSSLクライアント証明書はもちろんApple Root CAで署名されたりしていない。なんでだ???

エラーログ内のダンプの中に"http://www.apple.com/appleca/"というURLがあったので、試しにアクセスしてみたところ、「Apple PKI - Apple」というサイトに飛ばされた。アプリやソフトウェアアップデートの署名に使われている証明書ではないのかな?

試しにこのページにある"Apple Inc. Root Certificate"をサーバに配置してみて、アクセスできるかどうかを確認してみる。

ダウンロードしたAppleIncRootCertificate.cerはX.509のDER形式なのでPEMに変換する。

# openssl x509 -inform der -in AppleIncRootCertificate.cer -outform pem -out AppleIncRootCertificate.pem

元々の認証局の証明書ファイルとコンカチする。

# cat cacert.pem AppleIncRootCertificate.pem >concat.pem

新しく作った証明書ファイルを使うように設定変更してやると……
/etc/httpd/conf.d/ssl.conf

SSLCACertificateFile /etc/pki/CA/concat.pem

アクセスしてみると、無事にSSL経由でvideoタグに指定された動画を再生できた!

但し本来のクライアント証明書は使用されていないので、このままではセキュリティが保てず本質的な解決にはなっていない。(Appleが署名した証明書を使えば大切な社外秘の情報まで見られてしまう。)

Safariの不具合だと思うので直してほしいかも。

JMeterのリモート実行機能をNAT環境で使う

JMeterをNAT環境で使う機会があったので設定方法まとめた。というかほぼ以下のサイトに書いてあった通りにした。*1

今回は接続周りの設定の話しかしないので、インストール方法、テスト計画の設定方法、サーバ上でのCUIによるテスト実行などについては解説ありません。

使用するソフトウェア

今回使用したソフトウェアは以下の通り

JMeterのサーバクライアント間の通信

Apache JMeterは、クライアントアプリケーション(GUI)上で入力したテスト計画を実行するだけではなく、出力したテスト計画をコマンドラインから実行することもできる。さらに、クライアントアプリケーションからサーバにリモート接続し、テスト計画を実行し、その実行結果を表示することもできる。

テスト計画のリモート実行は、テスト実行用のサーバ上でjmeter-serverを起動し、そのサーバのポートにクライアントアプリケーションからソケット接続することでおこなう。さらにjmeter-server上で実行されたテスト計画の実行結果をクライアントアプリケーション上で表示するためには、jmeter-serverからクライアントアプリケーションのポートへの接続が必要になる。

上の図は、それぞれポート一つずつだが、実際にはrmiregistry用のポートも使用する。

SSHによるポート転送

さて、上記の図での「クライアントPC」がNAT環境にあるなどの理由で、サーバから直接接続できない場合について考える。今回は、SSHによるポート転送を使用する。

まず、サーバ・クライアントのどちらからも接続可能な中継サーバを用意する。サーバが中継サーバを兼ねても構わないが、ここでは一応別サーバということにする。

クライアントPCから中継サーバに、以下のようなssh接続をおこなうことでトンネリングをおこなう。(ここでは、クライアントアプリケーションが使用するポート番号を30000、サーバのアドレスを192.0.2.1としている。JMeterにおける設定方法などについては後述)

ssh -R 30000:localhost:30000 192.0.2.1

JMeter(GUI)を実行しているPCから中継サーバに接続をおこない、中継サーバ上でソケットにポート30000番を割り付けて接続を受け付けるようになる。

JMeter Serverでテストが開始されると、結果を送信する為にクライアントへの接続をおこなおうとする。設定(後述)を工夫することで、クライアントPCへ接続する代わりに、中継サーバに接続をおこなうように仕向けることができ、中継サーバ上で先ほどSSHが用意したポートに接続がおこなわれる。中継サーバ上の30000番ポートへの接続は、SSHのポート転送機能によって接続元であるクライアントPCの30000番ポートに転送され、クライアントPC上のJMeterに接続がおこなわれるようにできる。

中継サーバでポート転送をおこなうには、中継サーバのsshdの設定が必要な場合がある。OpenSSHでは、sshd_configで「GatewayPorts yes」を指定する必要があった。

上の例では、テスト実行サーバからクライアントPCへの接続ができない場合の説明であったが、クライアントからサーバへの接続ができない場合も同じようにポート転送で接続を確保できる。

JMeterの設定内容

クライアント、サーバのどちらも相互に接続できないような場合を考える。どちらからも、中継サーバには接続できるとする。中継サーバは下の図のようにインターネット上にあるかもしれないし、サーバ側のDMZ上などにあるかもしれない。

それぞれのIPアドレスを以下のようにする。

中継サーバ 192.0.2.1(グローバル)
クライアントPC 192.168.0.3(プライベート)
JMeter実行サーバ 192.168.1.5(プライベート)

今回使用するポートを以下のように決める。ポート番号は何でも良いが転送できるように固定は必要。

rmiregistryポート 31099 デフォルトは1099
JMeter サーバが使用するポート 40000 デフォルトは不定
JMeter クライアントが使用するポート 30000 デフォルトは不定
クライアントの設定

サーバへの接続を中継サーバに振り替えるために、/etc/hostsに以下の記述をおこなう。

127.0.0.1	localhost jmeter-client
192.0.2.1	jmeter-server

bin/jmeter.properiesの以下の項目を設定する。

remote_hosts=jmeter-server:31099
client.rmi.localport=30000

remote_hostsにはサーバ名とrmiregistryポートを指定する。指定したホスト名がメニューから選択できるようになる。

bin/jmeter.shでJVM_ARGSに以下の引数を追加し、システムプロパティにjava.rmi.server.hostnameを設定する。

JVM_ARGS="-Djava.rmi.server.hostname=jmeter-client"
サーバの設定

クライアントへの接続を中継サーバに振り替えるために、/etc/hostsに以下の記述をおこなう。

192.168.1.5    jmeter-server
192.0.2.1      jmeter-client

bin/jmeter.properiesの以下の項目を設定する。

client.rmi.localport=30000
server.rmi.port=31099
server.rmi.localport=40000

bin/jmeter-serverでRMI_HOST_DEFに以下の引数を追加し、システムプロパティにjava.rmi.server.hostnameを設定する。

RMI_HOST_DEF=-Djava.rmi.server.hostname=jmeter-server
トンネリング設定

サーバから中継サーバに対して、以下のようにSSH接続をおこなう

ssh -ladmin -R 31099:localhost:31099 jmeter-client
ssh -ladmin -R 40000:localhost:40000 jmeter-client

クライアントPCから中継サーバに対して、以下のようにSSH接続をおこなう

ssh -ladmin -R 30000:localhost:30000 jmeter-server

必要であれば、サーバ、クライアントPCから上記のポートへの接続をそれぞれFWで許可する。

プログラムの起動

サーバ側でjmeter-serverを起動する

[apache-jmeter-2.9]$ bin/jmeter-server
Using local port: 40000
Created remote object: UnicastServerRef [liveRef: [endpoint:[jmeter-server:40000](local),objID:...

クライアント側で、jmeter.shを起動する

[apache-jmeter-2.9]$ bin/jmeter.sh
処理の流れの説明

多分以下の理解であっていると思う。

  • サーバ上でjmeter-serverを起動
    • server.rmi.localportで指定したポート(40000)で接続の受け付けを開始する
    • サーバ上のserver.rmi.portで指定したrmiregistryポートに接続情報を登録する
      • rmiregistryが起動されていなければ、server.rmi.portで指定したポートでrmiregistry開始
  • クライアントPCで、メニューから「実行」>「開始(リモート)」でjmeter-server:31099を指定し、テストを実行
    • 実行結果を取得できるように、client.rmi.localportで指定したポート(30000)で接続の受け付けを開始する
    • クライアントから、jmeter-server:31099に接続し、サーバの接続情報を取得
      • クライアントPC上の/etc/hostsでは、jmeter-serverには中継サーバのIPアドレスを指定していたので、中継サーバの接続31099番ポートに接続
      • ポート転送され、サーバ上のrmiregistryに接続
      • rmiregistryは、接続情報として、ポート番号40000と、java.rmi.server.hostnameシステムプロパティで指定したホスト名jmeter-serverを戻す
    • クライアントから、jmeter-server:40000に接続する
      • クライアントPC上の/etc/hostsではjmeter-serverには中継サーバのIPアドレスを指定していたので、中継サーバの接続40000番ポートに接続
      • ポート転送され、サーバ上のJMeterサーバ(40000番ポート)に接続
      • JMeterクライアントはサーバに対して、自分の接続情報としてjava.rmi.server.hostnameシステムプロパティで指定したホスト名jmeter-clientと、ポート番号30000を送る
    • JMeterサーバから、jmeter-client:30000に接続する
      • サーバ上の/etc/hostsではjmeter-clientには中継サーバのIPアドレスを指定していたので、中継サーバの接続30000番ポートに接続
      • ポート転送され、ローカルPCのJMeterクライアント(30000番ポート)に接続

その他補足

元々クライアント→サーバ間の回線が細かったのでサーバ側で実行しようとして始めたんだけど、リスナー「結果をツリーで表示」などを使用している場合、レスポンスの内容が転送されるので転送量は減らない。「ログエラーのみ」にチェックするか、ネタ元サイト様のようにmode=Asynch設定した方がいいかも。

*1:サーバ側もNATというのは今回追加した条件だけど。

PDT(PHP Development Tools)で複数行ペースト時に次行のインデントが変更される

仕事でPHPのコードを編集することがあるのだが、自分が頻繁に使う「トリプルクリックで行選択→ドラッグで数行選択→コピー→別の箇所にペースト」という操作をおこなうと、なぜかペーストした行の次の行のインデントが変わってしまうという現象があり、不便をしている。

内部でどういう処理をしているのかちょっと調べてみた。

プラグインのjarファイルとプラグインの定義

今回関係があるのは、以下の二つのjarファイルだった。

  • plugins/org.eclipse.php.core_3.2.0.201306051924.jar (PDTのコア)
  • plugins/org.eclipse.php.ui_3.2.0.201306051924.jar (エディタなどのUI周りの処理をおこなう)

Eclipseプラグインテキストエディタなどのドキュメントを操作するようなものを作る場合、IAutoEditStrategyの実装クラスを作成してエディタに渡しておくことで、クリップボードの内容の貼り付けやキーボードからの文字入力など、ドキュメントへの更新操作のコマンドをハンドリングすることができるようだ。
PDTでは、MainAutoEditStrategyというのがその実装クラスだった。

org.eclipse.php.ui_3.2.0.201306051924.jarのプラグインの定義ファイル(plugin.xml)をみたところ、以下のような流れでMainAutoEditStrategyを使用するように設定されていた。

  • PHP用のエディタ本体であるorg.eclipse.php.internal.ui.editor.PHPStructuredEditor (plugin.xmlで定義)
    • PHPStructuredEditor用のSourceViewerConfigurationとして、PHPStructuredTextViewerConfigurationが指定されている(plugin.xmlで定義)
      • PHPStructuredTextViewerConfigurationのgetAutoEditStrategies()で、IAutoEditStrategyの実装としてMainAutoEditStrategyを返す

という感じで、エディタに対してMainAutoEditStrategyが渡されている。

実際の処理内容

MainAutoEditStrategyのcustomizeDocumentCommand()の定義を見ると、コマンド操作対象領域のタイプによって処理が振り分けられており、通常のPHPコードは、各種自動インデントの設定に従ってコマンドをハンドリングする各々のAutoEditStrategyに処理が委譲されている。今回の「次行のインデントが勝手に変更される」という挙動は、そのうちの「IndentLineAutoEditStrategy」というクラスが引き起こしているようだ。

IndentLineAutoEditStrategyのcustomizeDocumentCommandメソッドは、以下のような実装になっている。

        public void customizeDocumentCommand(final IDocument document,
                        final DocumentCommand command) {
                if (command.text != null
                                && TextUtilities.endsWith(document.getLegalLineDelimiters(),
                                                command.text) != -1)
                        autoIndentAfterNewLine((IStructuredDocument) document, command);
        }

与えられるテキストが空でなく、テキストの末尾が改行の場合のみ、autoIndentAfterNewLine()を呼び出している。この中身が怪しい。

ここまでの処理を見る限り、ペーストしたテキストの最後が改行の場合、改行キー押下時と同じようにインデントを行っている。(しかも、それはOFFには出来ない。) この挙動は不具合かと思ったが、改行キー押下時と改行コードペースト時で同じように自動インデントを実施しているという意味では、そういうポリシーなら仕方がない気もする。

autoIndentAfterNewLine()の中身も見てみる。挿入対象行の情報を取得している箇所があった。

        private void autoIndentAfterNewLine(final IStructuredDocument document,
                        final DocumentCommand command) {
...
                        final int currentOffset = command.offset;

                        final int lineNumber = document.getLineOfOffset(currentOffset);
...
                        final IRegion lineInfo = document.getLineInformation(lineNumber);

                        final int startOffset = lineInfo.getOffset();
                        final int length = lineInfo.getLength();

挿入対象箇所をcommand.offsetで取得し、それを含む行番号をIStructuredDocumentのgetLineOfOffset()で取得し、行自体の情報をIStructuredDocumentのgetLineInformation()で取得している。

startOffset(行の先頭)と、currentOffset(挿入箇所)があるので、これを比較すれば「挿入箇所が先頭行の場合に限り、インデントを行わない」という処理が書けそうだ。

改造(汚いパッチ)

上のコードに続いて、次のように書けば気に入らない挙動を無効化できそうだ。

                        if (startOffset == command.offset)
                                return;

入力内容の末尾が改行で、かつ挿入先のカーソルが行の先頭の場合は、その行のインデントは変更しない、という風になる。

実際にソースコードを修正して(無理やり)コンパイルし、生成した.classファイルをオリジナルのjarファイルにjar -ufで上書き適用したところ、トリプルクリックで行選択してコピー→ペーストを実行しても、挿入箇所の次の行のインデントが変更されることは無くなった(汚い。)

ちなJDK1.5でコンパイルし、コンパイルにあたってパスを通したのはこの辺。

Scala 2.9でパターンマッチでコンパイルエラー

Scalaはパターンマッチ内で正規表現を使ってマッチするかどうかの判定及びマッチした箇所の抽出が出来る。

   val regex0 = "(.*)hell".r
   val input = "nutshell"
   val result = input match {
      case regex0(p) => Some(p)
      case _ => None
   }
   assert(result == Some("nuts"))

これは便利なんだけど、調子に乗って沢山書きすぎると2.9系だとコンパイル時にエラーが出るようになる。

   val r1 = "(\\d+)個".r
   val r2 = "(\\d+)匹".r
   val r3 = "(\\d+)尾".r
   val r4 = "(\\d+)人".r
   val r5 = "第(\\d+)番".r
   val r6 = "バージョン(\\d+)".r
   val r7 = "(\\d+)円".r
   val r8 = "¥(\\d+)".r
   val r9 = "(\\d+)羽".r

   val input = "10円"
   val result = input match {
      case r1(n) => Some(n)
      case r2(n) => Some(n)
      case r3(n) => Some(n)
      case r4(n) => Some(n)
      case r5(n) => Some(n)
      case r6(n) => Some(n)
      case r7(n) => Some(n)
      case r8(n) => Some(n)      
      case r9(n) => Some(n)  
      case _ => None
   }
   assert(result == Some("10"))

まあ内容は再現用サンプルなんで特に意味はないです。
このコードは、こんなエラーが出てコンパイル出来ない。

[info] Compiling 1 Scala source to /tmp/ScalaSample/target/scala-2.9.1/classes...
[trace] Stack trace suppressed: run 'last compile:compile' for the full output.
[error] (compile:compile) java.lang.Error: ch.epfl.lamp.fjbg.JCode$OffsetTooBigException: offset too big to fit in 16 bits: 42287
[error] Total time: 15 s, completed 2013/06/15 4:58:17

どうやら既知の問題のようで、2.10では解消していて、ちゃんとコンパイル出来る。

どうしても2.9系を使い続けたい時には、match式を複数のPartialFunctionに分割してorElseで結合したらどうか。

   ... 
   val input = "10円"
   val cases1: PartialFunction[String, Option[String]] = {
      case r1(n) => Some(n)
      case r2(n) => Some(n)
      case r3(n) => Some(n)
      case r4(n) => Some(n)
      case r5(n) => Some(n)
   }
   val cases2: PartialFunction[String, Option[String]] = {
      case r6(n) => Some(n)
      case r7(n) => Some(n)
      case r8(n) => Some(n)      
      case r9(n) => Some(n)  
      case _ => None
   }
   val result = (cases1 orElse cases2) apply input

   assert(result == Some("10"))

コンパイルが通って実行も出来る。でもコンパイル時に網羅性のチェックは出来ないかも。

PartialFunctionを可変長引数で渡してreduceで合成するようにしておけば見た目きれいかも。

object Match {
   def forCases[A, B](a: A)(cases: PartialFunction[A, B]*): B = {
      cases.reduce(_ orElse _) apply a
   }
}

使う側

   val input = "10円"
   val result = Match.forCases(input)(
      { case r1(n) => Some(n) },
      { case r2(n) => Some(n) },
      { case r3(n) => Some(n) },
      { case r4(n) => Some(n) },
      { case r5(n) => Some(n) },
      { case r6(n) => Some(n) },
      { case r7(n) => Some(n) },
      { case r8(n) => Some(n) },
      { case r9(n) => Some(n) },
      { case _ => None}
   )

   assert(result == Some("10"))

まあ結局網羅性のチェックは出来ないので完全にはmatch式の代わりにはならないけど。

とっとと2.10に上げましょうということかな。