Kinx ライブラリ - Process
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。
今回は Process です。子プロセス起動とかやっぱり必要ですよね、ということで急遽こしらえました。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
System.exec()
昔から標準で用意していたコマンド実行インタフェース。単純に C レベルでの system()
を呼ぶだけなので手軽だが、終了するまで帰ってこないとか、標準出力を取得できないとか、色々不便ではある。ただし、シェル経由でコマンド実行するので、リダイレクトとかは使える。
で、今回はもっと色々とできる Process
クラスを作ったので、そちらが今回の説明での本命。
Process
using Process
Process ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。
using Process;
Exec
Process オブジェクトは new Process(command, opts)
で作成する。引数はコマンド名と引数の配列、またはコマンド文字列。配列の場合は引数を別々に渡す感じで、コマンドライン文字列の場合は内部で解析して配列形式に自動で分解。
- 配列:
["ls", "-1"]
のような感じ。 - 文字列:
"ls -1"
のような感じ。
作成されたプロセス・オブジェクトは以下のメソッドを持つ。
メソッド | 概要 |
---|---|
run() |
プロセスを開始させる。 |
launch() |
プロセスを開始させて切り離す。 |
std() |
引数で渡されたオプションが返る。{ in: opts.in, out: opts.out, err: opts.err }
|
この時点ではまだ実行していない。run()
または launch()
を行った時点で起動する。run()
すると ProcessController
クラスのオブジェクトが返る。
Run
run()
することで ProcessController
クラスのオブジェクトが返る。
var p = new Process(["cmd","arg1"]).run();
Launch
launch()
は何も返さない(というか null が返る)。突き放して以後、子供の面倒は見ない、という方法。
new Process(["cmd","arg1"]).launch();
ProcessController
run()
で返された ProcessController
クラスは以下のメソッドを持つ。
メソッド | 概要 |
---|---|
isAlive() |
プロセスが生きている場合は true、既に終了している場合、または detach された後は false |
wait() |
プロセスの終了を待ち、終了後にプロセスの終了コードを返す。detach 後は 0 を返す。 |
detach() |
プロセスを detach する |
detach()
はプロセス起動後に切り離す。Linux では launch()
で切り離した場合と微妙に動作が違うがやりたいことは同じ。Windows で内部動作も同じ。
Linux ではプロセス起動時に切り離すためにいわゆる double-fork というやり方で切り離すが、これはプロセス起動時にしか使えない。プロセス起動後に切り離すのは実質的に不可能で、親プロセスではきちんと wait
なり waitpid
なりしてやらないと子はゾンビとなって生き残ってしまう。
そこで、detach()
した瞬間に waitpid
するためだけのスレッドを起動して、子が死ぬまで面倒見るようにしてある。
ちなみに double-fork とは Linux の、
- 親プロセスが死ぬと子プロセスは init 配下になった上に init はひたすら wait しまくって面倒見てくれる
...という機能を利用して、一度 fork したプロセスからさらに fork した上で、最初に fork したプロセスを速攻終了させて孫プロセスの管理を init に任せる、というやり方です。
一番上の親プロセスは、最初に fork した子供の waitpid
を忘れずに。勝手に面倒見てもらうのは孫のほうなので。
Wait
終了を待って終了コードを取得する例は以下の通り。
var p = new Process(["cmd", "arg1"]).run();
var status = p.wait();
detach
していた場合は当然取得できない(0 が返る)。
Detach
先ほどから出てきている detach
。プロセスは detach
(切り離し)することもできる。切り離してしまえば子との縁は切れる。wait
する必要もないし、終了を気にする必要もない。というか、気にしたくてもできなくなる。
var p = new Process(["cmd", "arg1"]).run();
p.detach();
Pipe
お待ちかねパイプ。Process
を作った一番の目的はパイプ。子プロセスとの標準入出力をパイプに自由自在につないで情報のやり取りをしたい、というのが一番欲しい機能ですよね。
パイプの指定は、new Process(cmd, opts)
の opts
で指定。パラメータは以下の 3 種類。
パラメータ | 内容 |
---|---|
in |
標準入力を指定。 指定可能なものは、パイプ・オブジェクト、文字列、 $stdin
|
out |
標準出力を指定。 指定可能なものは、パイプ・オブジェクト、文字列、 $stdout または $stderr
|
err |
標準エラー出力を指定。 指定可能なものは、パイプ・オブジェクト、文字列、 $stdout または $stderr
|
- パイプ・オブジェクト ... パイプを使うためのオブジェクト。詳細は後述。
- 文字列 ... ファイル名として、入力元、出力先ファイル。
-
$stdin
、$stdout
、$stderr
... 入力元、出力先を本プロセスの標準入出力にバインドする。
パイプ・オブジェクト
パイプ・オブジェクトは new Pipe()
で作成する。[Read, Write]
の 2 つのオブジェクトをペアで配列で返す。パイプオブジェクトには、以下のメソッドがある。
通常は Write
パイプを子プロセスの out
または err
に指定して、Read
パイプから読み込む。
Read Pipe
パイプのクローズは run()
してからすること。run()
するときに設定されるため。
メソッド | |
---|---|
peek() |
パイプにデータがなければ 0、あれば 0 より大きい数値を返す。-1 はエラー。 |
read() |
パイプのデータを全て文字列として取得する。データがない場合は空文字列を返す。 |
close() |
パイプを閉じる。 |
Write Pipe
パイプのクローズは run()
してからすること。run()
するときに設定されるため。
メソッド | |
---|---|
write(data) |
パイプにデータを書き込む。全て書き込めるとは限らず、書き込んだバイト数を返す。 |
close() |
パイプを閉じる。 |
サンプル
一般的な形として以下のように使う。
using Process;
var [r1, w1] = new Pipe();
var p1 = new Process([ "ls", "-1" ], { out: w1 }).run();
w1.close(); // もう使わないのでクローズしてよい
while (p1.isAlive() || r1.peek() > 0) {
var buf = r1.read();
if (buf.length() < 0) {
System.println("Error...");
return -1;
} else if (buf.length() > 0) {
System.print(buf);
} else {
// System.println("no input...");
}
}
System.println("");
Write Pipe を親プロセス側で使う場合は、こんな感じ。
using Process;
// stdin はパイプから読み込み、標準出力に出力
[r1, w1] = new Pipe();
var p1 = new Process("cat", { in: r1, out: $stdout }).run();
r1.close(); // もう使わないのでクローズしてよい
// p1 の stdin に送り込む
var nwrite = w1.write("Message\n");
w1.close(); // パイプクロ―ズ、送信終了
p1.wait();
ちなみに、こうすると標準出力と標準エラー出力を制御できる。
new Process("cmd", { out: $stdout, err: $stdout }); // 標準エラー出力を標準出力に合流
new Process("cmd", { out: $stderr, err: $stderr }); // 標準出力を標準エラー出力に合流
new Process("cmd", { out: $stderr, err: $stdout }); // 入れ替え
Pipeline
パイプをつないでいくのは結構面倒(というか、どっちがどっちだっけ...? みたいな)な作業なので、一括して行ってくれる Process.pipeline
というのも定義してみた。最後にコールバック関数を置いて、以下のように使う。
var r = Process.pipeline(cmd1, cmd2, cmd3, ..., &(i, o, pipeline) => {
// i ... 最初のコマンドの stdin への書き込みパイプ
// o ... 最後のコマンドの stdout からの読み込みパイプ
// pipeline ... パイプライン・オブジェクト
// pipeline.input ....... 上記 i と同じ
// pipeline.output ...... 上記 o と同じ
// pipeline.peek() ...... pipeline.output.peek() と同じ
// pipeline.read() ...... pipeline.output.read() と同じ
// pipeline.write() ..... pipeline.input.write() と同じ
// pipeline.isAlive() ... パイプラインのいずれかのプロセスが生きていたら true
// pipeline.wait() ...... パイプラインの全てのプロセスが完了するのを待ち、
// 終了コードを配列で返す
// コールバックの復帰値がそのまま Process.pipeline() の復帰値になる。
return pipeline.wait();
});
コールバックしなくても使える。
var pipeline = Process.pipeline(cmd1, cmd2, cmd3, ...);
// pipeline ... パイプライン・オブジェクト
// 以下省略。
おわりに
子プロセス関係は Windows と Linux で違うので、そういうのを統一的に扱えるのはスクリプトの良いところ。ただし、コマンド自体は違ったりするので、そこはなかなか吸収できませんね。私は Windows ユーザーですが、UnxUtils を使って Unix コマンドをある程度コマンドプロンプトでも使えるようにしています。(Cygwin は環境を変えてしまうのであまり好きではない...)
ということで、ではまた次回。