Node 上の JavaScript からの子プロセスの起動
子プロセスの起動とは?
Windows 環境を想定して説明しています。
プログラム起動の重要性について
Node でアプリケーション開発をしていて何か機能を拡張したい場合、ソフトウェア開発者の皆さんなら、 自分でその機能を実装するとか、あるいは利用可能なライブラリを探してパッケージマネージャ npm で取り込んで使うということをしていると思います。
理想的には、そんな流れで Node 内部の処理を使って必要な処理を実装できるのに越したことはありません。
しかし実際にはそれだけで済まず、他の言語で書いた単体のプログラムを呼び出したい場合も少なくないものです。
理由は様々です。これまでのリソースを再利用する必要があるとか、Node では簡単に利用できるライブラリがないとか、 EXE の形式でしか欲しいプログラムが販売されていない、など、いろいろな理由で外部プログラムを呼び出して使う必要性が出てくることがあります。
このため単体の外部プログラムを必要に応じて起動するということができないと、開発が行き詰まりかねません。 このため、プログラムを自由に呼び出せるというのはとても大切なことなのです。
子プロセスの起動とは?
「単体のプログラム」というのは、Windows で言えばよく「EXE ファイル」と呼ばれる、実行可能ファイルのことです。
macOS や Linux では拡張子では区別されず、パーミッションによって実行可能ファイルとします。
通常、こうした実行可能プログラムを起動したときには、OS 内ではそのプログラム専用の「プロセス」が作成されて、その中でプログラムが実行されます。
また、あるプロセスから他のプロセスを起動したときには、起動された側のプロセスは「子プロセス」として作成され、起動した側のセキュリティコンテキストを自動的に引き継ぎます。
プロセスは多くの場合セキュリティの境界にもなりますし、メモリ空間もそれ専用の空間が割り当てられて他のプロセスと干渉しないようにもなっていたりして、プログラム1つ1つは独立して動作するものとして開発できるようになっています。
子プロセスを使うのはどうして難しい?
それぞれのプロセスは独立していて扱いやすいという面がある一方で、逆に扱いが難しい面もあります。
例えば、プログラム A からプログラム B を呼び出すとします。A では B の処理が終わってから、次の処理を行いたいとします。
プログラム A は B が終了するのをどのように待てば良いでしょうか。 あるいは B の実行結果が成功だったのか、失敗だったのかは、どうすればわかるでしょうか。
もし同じプロセス内の処理なら、意図的にマルチスレッドにでもしない限り、処理は一本道で次々と実行されますので、 順番に処理をすることに難しさはありません。しかしプロセスが異なれば、それぞれ独立して処理が実行されますので、 「B の処理が終わったら先に進む」ということを明示的に指定しないといけません。
また、同じプロセスなら実行結果の確認も関数の戻り値を見るだけでできたりするので、とても簡単です。 しかし、プロセスが異なれば、処理結果もメモリ空間が違うので、簡単には読み出せません。
このように、複数のプロセスを扱うには特有の難しさがあります。
しかし実は Node ではこうした問題を解決する、良い方法が用意されています。
今回は Node 上の JavaScript から他の EXE ファイルを実行して、そのプログラムの終了を待って、その結果を取得するということをやってみましょう。
今回起動するテスト用のプログラムについて
今回は Windows 環境で動作確認を行います。他の環境でも動作するはずですが、パスの指定方法とか、微妙に違いますので適当に読み替えてください。
起動される側のプログラムはなんでもいいのですが、今回の実験では次のようなプログラムにしてみましょう。
- 起動したら現在のディレクトリに a.txt というテキストファイルを作成する。
- a.txt には START という文字と時刻を書き込み、5秒スリープしてから、END と時刻を書き込む。
- プログラムに渡した1つ目の引数を整数にして、プログラムの終了コードとする。(もちろん、通常は実際の処理結果を終了コードとすべきです。これはテストのためです)
- 標準出力と標準エラー出力にも文字を少し書き込みます。
これを .NET Core 3 上の C# コンソールプログラムとして実装すると、だいたい次のようになります。
using System;
using System.IO;
using System.Threading;
namespace cs_app1 {
class Program {
static void Main(string[] args) {
var i = 0;
if (args.Length > 0) {
int.TryParse(args[0],out i);
}
using (var outStream = File.CreateText(@"a.txt")) {
outStream.WriteLine($"=== START {DateTime.Now.ToLongTimeString()}");
Thread.Sleep(5000);
outStream.WriteLine($"=== END {DateTime.Now.ToLongTimeString()}");
}
Console.WriteLine("THIS IS STDOUT!");
Console.Error.WriteLine("THIS IS STDERR!!");
Environment.ExitCode = i;
}
}
}
標準出力、標準エラー出力などについては、「「標準出力」「標準エラー出力」とは?」をみてください。 C# の記事として書いていますが、特に言語に特有の考え方ではありません。
このコードをビルドして作成したプログラムを cs_app1.exe としています。 私がこの EXE ファイルを置いたパスは 'C:/src/test/node/cs_app1/bin/netcoreapp3.0/cs_app1.exe' ですが、 試す時には適当にあなたの環境に合わせて書き換えてください。
子プロセスを起動する TypeScript
子プロセスを起動する TypeScript コードを書いていきましょう。
TypeScript プロジェクトの準備
TypeScript を記述するための準備として、次のページを参考に TypeScript のプロジェクトを用意してください。
このプロジェクト作成方法は必須ではありませんが、もし異なる手順で環境設定した場合は、コンパイルの仕方が変わったりしますので、適当に読み替えてください。
子プロセスを起動する TypeScript のコードの説明
上で作成したコンソールプログラム cs_app1.exe を呼び出す TypeScript コードは次のようになります。
import * as child_process from 'child_process';
import * as util from 'util';
const execFile = util.promisify(child_process.execFile);
const EXE_FILE = 'C:/src/test/node/cs_app1/bin/netcoreapp3.0/cs_app1.exe';
async function runChildProc() {
execFile(EXE_FILE, ['123'])
.then(() => {
console.log('Successfully executed.');
})
.catch((err: any) => {
console.log('Error!');
console.log(err);
});
}
runChildProc();
このコードを index.ts として保存します。
実行環境は Node (v10.16.3 LTS), Windows 10 で動作確認しています。
上から順番に説明します。
import * as child_process from 'child_process';
import * as util from 'util';
const execFile = util.promisify(child_process.execFile);
const EXE_FILE = 'C:/src/test/node/cs_app1/bin/netcoreapp3.0/cs_app1.exe';
子プロセスを起動するために、 child_process モジュールを利用しますので、import で取り込んでいます。
child_process モジュールの execFile メソッドを使うと、引数で指定した EXE ファイルを直接実行します。
似た名前の exec メソッドでは、シェルを起動してその中でプログラムを実行します。
関数を util モジュールの promisify メソッドに渡すと、 戻り値として関数オブジェクトが返ります。返ってきた関数を使うと、Promise の then、catch スタイルで非同期処理を記述することができるようになります。
ここでは child_process.execFile 関数を Promise にして使います。
このため util モジュールを取り込み、execFile を promisify に渡しています。 そして返ってきた関数オブジェクトを execFile という変数にいれています。
EXE ファイルのパス名は定数で EXE_FILE としています。環境に合わせて書き換えてください。
async function runChildProc() {
execFile(EXE_FILE, ['123'])
.then(() => {
console.log('Successfully executed.');
})
.catch((err: any) => {
console.log('Error!');
console.log(err);
});
}
関数 runChildProc を定義して、ここで上で取得した execFile 関数を呼び出します。結果は Promise 的な then と catch で扱えます。
関数の呼び出し自体は promisify 関数に渡した元の child_process.execFile 関数と同じです。
第一引数に EXE ファイルのパスを、第二引数に EXE ファイルへのパラメータを渡しています。
上でみたように、今回呼び出すプログラムは引数を受け取り、受け取った値を終了コードとしてセットします。今回は成功を表す 0 ではなく、123 という数字を渡すことによって、擬似的にエラーを「発生」させて、ちゃんとエラー処理が行われることを確認しています。
エラーの場合は catch が呼び出されます。ここでエラーオブジェクトがどのように取得できたか確認するために、console.log で出力しています。
runChildProc();
全て下準備が終わったら、runChildProc() を呼び出して実行します。
TypeScript のビルドと実行
index.ts ができたら、TypeScript コンパイラでコンパイルして、Node.js で実行できる形式にします。
上で説明した方法で Node/TypeScript プロジェクトを設定していたら、次のコマンドでコンパイルできるはずです。
> npm run build
その結果、index.js ファイルが dist という名前のサブディレクトリ内に作成されるはずです。
呼び出したプロセスの終了コードによる動作の違い
作成された index.js を実行するには次のコマンドを実行します。
> node ./dist/index.js
この結果、約 5 秒後に次のように表示されるはずです。
確かに catch が実行され、その際に err にエラー情報が渡されていることがわかります。
index.ts 内で EXE ファイルに引数を渡している箇所がありますが (123 を渡している箇所です)、ここを 0 にすれば成功時の振る舞いが確認できます。 このときは Promise の then が実行されるはずです。
上で作成された EXE ファイルは現在のディレクトリに a.txt という名前のファイルを作成するはずですから、それもどこに出力されたか確認しておくと良いと思います。
以上で、Node 環境で EXE ファイルを実行して、その成否を確認する方法について説明しました。