Help us understand the problem. What is going on with this article?

node の spawn に関して調べてみた

More than 1 year has passed since last update.

VSTS の VSTS Task lib というライブラリを触っていると、よく、spawn という言葉に出くわす。この spawn は一体何なのかを調べてみた。

spawn は node で、OS のコマンドを node から実行したい場合に使われるもので、新しい子プロセスを生成してコマンドを実行するメソッドの一部だ。英語の意味合い的には、魚が産卵するときの様子を指している。

子プロセスを生成して、コマンドを実行するメソッドにはいくつかあるので、そのメリットデメリットを整理しておきたい。

1. execFile

execFile は、子プロセスを作成して、コマンドを実行する。例えばこんな感じ。

どのようなケースで使うかというと、あまり重くない処理、時間がかからないものに関して使われる。

import * as child_process from "child_process";

namespace ExecfileSample {
    console.log("parent:" + process.pid);
    let child = child_process.execFile("ls", ["-l"], (error, stdout, stderr) => {

        if (error) {
            console.error("stderr", stderr);
            throw error;
        }
        console.log(stdout);
        console.log("pid: " +child.pid);
    });
}

実行結果はこうなる。ちゃんと、子プロセスが生成されて動いているのがわかる。

$ node execfile.js
parent:2636
合計 11654
-rwxrwxrwx 1 root root   32  7月 16 15:49 Dockerfile
-rwxrwxrwx 1 root root  901  7月 16 16:58 exec.js
-rwxrwxrwx 1 root root  777  7月 16 16:43 exec.ts
-rwxrwxrwx 1 root root  635  7月 16 16:58 execfile.js
-rwxrwxrwx 1 root root  507  7月 16 16:58 execfile.ts
drwxrwxrwx 0 root root  512  7月 16 15:18 node_modules
-rwxrwxrwx 1 root root  331  7月 16 15:18 package.json
-rwxrwxrwx 1 root root  423  7月 16 16:58 spawn.js
-rwxrwxrwx 1 root root  319  7月 16 15:59 spawn.ts
-rwxrwxrwx 1 root root  695  7月 16 16:58 spawnspike.js
-rwxrwxrwx 1 root root  550  7月 16 16:48 spawnspike.ts
-rwxrwxrwx 1 root root 4616  7月 16 15:14 tsconfig.json

pid: 2642

2. spawn

さて、問題の spawn だが、execFile が入出力が固定のものになっているの対し、spawn はストリームになっている部分が異なる。execFile が大量のデータなどに対応できないのに対して、spawnは、ストリームなので、効率よく、大量データのケースにも対応できる。重くて、時間のかかる、docker build コマンドを実行させてみよう。

spawn を実行すると、docker build の実行に時間がかかるが、少しづつ結果が反映される。

import * as child_process from "child_process";

namespace spawn {
    console.log("parent:" + process.pid);
    let  proc =  child_process.spawn('docker', ['build', '-t', 'aaa', '.']);
    console.log("child:" + proc.pid);
    proc.stdout.on('data', (data) => {
        console.log(data.toString());
    });
}

結果は一気に出てこずに、少しづつ出力される。(ストリームなので)

$ node spawn.js
parent:2664
child:2670
Sending build context to Docker daemon 27.46 MB

Step 1/3 : FROM nginx
 ---> 2f7f7bce8929

Step 2/3 : RUN ls
 ---> Using cache
 ---> 971b157ac789
Step 3/3 : COPY . .

 ---> 26edec92b96b

Removing intermediate container b5073d23183c

Successfully built 26edec92b96b

Successfully tagged aaa:latest

さて、これと同じコードを、execFile で書いたらこうなる。

import * as child_process from "child_process";

namespace ExecfileSample {
    console.log("parent:" + process.pid);
    let child = child_process.execFile("docker", ["build", "-t", "aaa", "."], (error, stdout, stderr) => {
        if (error) {
            console.error("stderr", stderr);
            throw error;
        }
        console.log(stdout);
        console.log("pid: " +child.pid);
    });
}

実行結果は上記と同じだが、徐々に結果がでてくるのではなく、全部終わってから一気に出てくる。ストリームではなく、一括で処理されるので、大量データには向いていない。

3. exec

他には、exec という形態もある。これは、内部的にシェルを実行するようになっている。上記の2つが、パイプに対応していないのに対し、exec はシェルファイルを作って実行するようなものなので、パイプに対応している。ただし、なんでも実行できてしまうので、shell injection というセキュリティ問題をはらんでいる。比較してみよう。

import * as child_process from "child_process";

namespace ExecSample {
    child_process.exec('ls -l | grep exec', (error, stdout, stderr) => {
        if ( error instanceof Error) {
            console.error(error);
            console.log('exec Error *******');
        } else {
            console.log(stdout);
            console.log('exec Success!');
    } 
    });
  child_process.execFile('ls -l | grep exec' , (error, stdout, stderr) => {
 //   child_process.execFile('ls', ['-l', '|','grep', 'exec'] , (error, stdout, stderr) => {
        if (error instanceof Error) {
            console.error(error);
            console.log('execFile Error ******');
        } else {
            console.log('execFile Success!');
        }
    } );

結果はこんな感じ

$ node exec.js
{ Error: spawn ls -l | grep exec ENOENT
    at exports._errnoException (util.js:1018:11)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:193:32)
    at onErrorNT (internal/child_process.js:367:16)
    at _combinedTickCallback (internal/process/next_tick.js:80:11)
    at process._tickCallback (internal/process/next_tick.js:104:9)
    at Module.runMain (module.js:606:11)
    at run (bootstrap_node.js:389:7)
    at startup (bootstrap_node.js:149:9)
    at bootstrap_node.js:504:3
  code: 'ENOENT',
  errno: 'ENOENT',
  syscall: 'spawn ls -l | grep exec',
  path: 'ls -l | grep exec',
  spawnargs: [],
  cmd: 'ls -l | grep exec' }
execFile Error ******
-rwxrwxrwx 1 root root  901  7月 16 16:58 exec.js
-rwxrwxrwx 1 root root  777  7月 16 16:43 exec.ts
-rwxrwxrwx 1 root root  635  7月 16 16:58 execfile.js
-rwxrwxrwx 1 root root  507  7月 16 16:58 execfile.ts

exec Success!

ENOENT は、ディレクトリにエントリがないということを意味する。ちなみに、この書き方はexecFile の書き方としては間違っているので、違う書き方をしてみる。

  // child_process.execFile('ls -l | grep exec' , (error, stdout, stderr) => {
    child_process.execFile('ls', ['-l', '|','grep', 'exec'] , (error, stdout, stderr) => {
$ node exec.js
-rwxrwxrwx 1 root root  896  7月 16 17:14 exec.js
-rwxrwxrwx 1 root root  777  7月 16 17:13 exec.ts
-rwxrwxrwx 1 root root  635  7月 16 17:14 execfile.js
-rwxrwxrwx 1 root root  507  7月 16 16:58 execfile.ts

exec Success!
{ Error: Command failed: ls -l | grep exec
ls: '|' にアクセスできません: そのようなファイルやディレクトリはありません
ls: 'grep' にアクセスできません: そのようなファイルやディレクトリはありません
ls: 'exec' にアクセスできません: そのようなファイルやディレクトリはありません

    at ChildProcess.exithandler (child_process.js:204:12)
    at emitTwo (events.js:106:13)
    at ChildProcess.emit (events.js:191:7)
    at maybeClose (internal/child_process.js:891:16)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:226:5) killed: false, code: 2, signal: null, cmd: 'ls -l | grep exec' }
execFile Error ******

こちらの方は実行前に、止めてくれる。ちなみに、spawn で書くと何も出力されない、実行されない感じだった。この辺デバッグの知見があるかたは、コメントいただけると嬉しいです。

おわりに

今回は、spawn という意味を調べて、その周辺のメソッドを探索した。基本的に、プロセスを生成して、コマンドを実行する仕組みだが、exec メソッド以外では、パイプは使えないとわかった。(プロセスをもう一つ生む必要があるからと思われる)VSTS の内部では、いづれかのメソッドが使われているだろうから、今度はでてきた エラーメッセージでより推測が立てやすくなりそうだ。
 ちなみに、folkという、spawn の発展形でプロセス間通信ができるものも存在する。今回はタイムアップで眠いのでここまで。node のストリーム周りとかも探索してみたいし、パイプもメソッドがありそうなので、使える仕組みも探索してみたい。

ここら辺と、Mock 周りを理解していると、vsts task lib のコードは結構読めるようになるかもしれない。

追記 2017/07/17

spawn が何も出力されない件に関して、問題がわかったのと、spawn は shell も実行できるのがわかったので次のエントリをかきました。

node の spawn に関して調べてみた その2

参考

Understanding execFile, spawn, exec, and fork in Node.js
Readable Stream
VSTS task lib

TsuyoshiUshio@github
プログラマ。自分の学習用のブログです。内容は会社とは一切関係ありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした