LoginSignup
36
25

More than 5 years have passed since last update.

child_processをpromise化する話

Posted at

前提

やりたいこと

  • node.jsでシェルコマンドを叩きまくる、みたいなこと
  • 例えば、exec('ls') → exec('mkdir') → exec('ls')みたいな感じ
  • 逐次で実行するためにchild_processをpromise化することが必要

exec

util.promisesifyを使う

util.promisifyを使うとなんとも至って簡単で、util.promisifyで包むだけ!

const util = require('util');
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec);

async function main(){
  let res = await exec('ls');
  console.log(res.stdout);

  await exec('mkdir tempdir');

  let res2 = await exec('ls');
  console.log(res2.stdout);
}

main().catch(e => console.log(e));

なんて便利なんでしょう。
なお、実行してみてちゃんとawaitした出力になっていたので、とってもうれしかったです。

return new Promiseする…

上手く書ける気がしないんですが、

const childProcess = require('child_process');

unction execp(cmd){
  return new Promise((resolve)=>{
    let exec = childProcess.exec(cmd, (error, stdout, stderr) => {
      console.log(stdout);
      resolve();
    });
  })
}

async function nmain(){
   await execp('ls');
   await execp('mkdir newtmp');
   await execp('ls');
}

nmain().catch(e=>console.log(e));

雑だけど上記で動く。

  • 面倒だしresolveとかrejectとかいちいち考えないといけないので断然util.promisifyがいい!

spawn

spawnをpromise化するなんて(私にとっては)めちゃくちゃ難易度高かった。
夢に出てくるくらいに悩んだ。

util.promisifyを使う…?

  • 使い方がわかりませんでした。

Takes a function following the common error-first callback style, i.e. taking a (err, value) => ... callback as the last argument, and returns a version that returns promises.

util.promisify(original)

う〜〜〜〜ん…。spawnてerror-first callback styleじゃないもんなぁ。。。
一見するとexecとは違い使えないようにも見えるんだけど、わからないです。

return new Promiseする

1日くらい悩んだんだけど、closeだかexitだかのときにresolveしたらなんだかいける気がしたのでやってみた。

const childProcess = require('child_process');

function spawn (cmd,args){
  return new Promise((resolve)=>{
    let p = childProcess.spawn(cmd,args);
    p.on('exit', (code)=>{
      resolve();
    });
    p.stdout.setEncoding('utf-8');
    p.stdout.on('data', (data)=>{
      console.log(data);
    });
    p.stderr.on('data', (data)=>{
      console.log(data);
    });
  })
}

async function nnmain(){
  await spawn('ls');
  await spawn('mkdir',['newtmp']);
  await spawn('ls');
}

nnmain().catch(e=>console.log(e));

exitのときにresolve(code)にして、codeが0ならsuccess、!0ならfailedなどの判定ができるのかもしれない。
これで動いたのでなんで悩んでいたのか今となってはよくわからないんだけど、当初は次のようなコードを書いていた。

fault
const spawn = childProcess.spawn;

function spawnw(cmd){
  return new Promise((resolve)=>{
    let cp = spawn('ls');
    cp.on('close', (code)=>{
      resolve();
    });
    ...
  });
}

const spawn = childProcess.spawn(cmd)をreturn new Promiseの外に書いていることだけが違いなんだけど、あれ、これってふわっとした理解の範疇でいうと確かスコープ?の問題なのでは?
うーん、わからない。やっぱり初心者にはちょっと難しかったです。教えてもらえるととても嬉しいです。

  • ちゃんとpromiseの中に入れよう、childProcess.spawn(cmd)

execとspawnの違い

The most significant difference between child_process.spawn and child_process.exec is in what they return - spawn returns a stream and exec returns a buffer.

  • どっちもChildProcess型(公式ドキュメントより)
  • execはbufferを返す
  • spawnはstreamを返す

spawn is best used to when you want the child process to return a large amount of data to Node - image processing, reading binary data etc.

  • spawnは大量のデータのやりとりに向いている
  • execは出力を保持しておいて最後にそれを一気に返す

maxBuffer Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 200 * 1024.
execはバッファサイズがデフォルトで200*1024(200KB)となっていて、これを超える場合はoptionでmaxBufferを変更できる。
なので、stdoutとかstderrが予測できない膨大な量の場合はspawnという風に使い分けるとよいのではないだろうか。

なお、streamかbufferかと言われて最初なんのこっちゃと思ったので次のようなものを作ってみた。

test.sh
#!/usr/bin/env bash

arg=$1
times=$2

for i in `seq $arg`
do
  sleep $times;echo "test :"$i
done
difference_exec_spawn.js
function spawnw(cmd, opt, env){
  return new Promise((resolve) => {
    let p = childProcess.spawn(cmd, opt, env);
    p.stdout.setEncoding('utf8');
    p.stdout.on('data', (data)=>{
      console.log(data);
    });
    p.on('close', (code)=>{
      resolve();
    })
  });
}

async function main(){
  console.log('exec ===============');
  let res = await exec('bash test.sh 10 1',{cwd:'./'})
  console.log(res.stdout);

  console.log('spawn ==============');

  await spawnw('bash',['test.sh','10', '1'], {cwd:'./'});
  console.log('done!');
}

main();

(1つ目の引数回)数、(2つ目の引数)秒sleepしてコンソールに文字列を出力するtest.shと、
それをexecとspawnのそれぞれで実行するプログラムです。

execの方は10回目のechoが終了してからconsoleにそれまでのecho内容を出力するけど、spawnは都度1秒間に1回test :i回目をコンソールログに出力してくる。
なるほど、これがstreamとbufferの違いか〜〜〜〜〜(ふんわり)

以上。

APPENDIX

36
25
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
25