まえがき

Windows 環境で親プロセスから子プロセスを child_process 経由で起動して、子プロセスのほうで以下のようなコードを書いた場合の挙動についてメモしておきます。

fs.readFile('./hoge.json');

ちょっとハマったの node.js のコードを見に行ってみました。

fs.readFile が相対パスを解決するまでの流れ

binding.open(pathModule._makeLong(path),
           stringToFlags(options.flag || 'r'),
           0o666,
           req);

node/fs.js at master · nodejs/node

_makeLong: function _makeLong(path) {
  // Note: this will *probably* throw somewhere.
  if (typeof path !== 'string')
    return path;

  if (path.length === 0) {
    return '';
  }

  const resolvedPath = win32.resolve(path);
}

node/path.js at master · nodejs/node

for (var i = arguments.length - 1; i >= -1; i--) {
  var path;
  if (i >= 0) {
    path = arguments[i];
  } else if (!resolvedDevice) {
    // ここに入る
    path = process.cwd();
  }
}

win32.resolve については win32-in-path.js ここに抜き出してみました。

結果的には、 process.cwd() を path にセットする部分が呼ばれて、子プロセスの起動元となるワーキングディレクトリがセットされ、相対パスなのでカレントから見に行ってくれると思いきや、そうはなりません。

node/path.js at master · nodejs/node

サンプルコード

parent

const childProcess = require('child_process');

console.log('parent __dirname: ', __dirname);

let p = childProcess.spawn('node', ['../child/child.js'], {
  detached: true,
  stdio: ['ignore', 'ignore', 'ignore']
});
p.unref();

child

const log = require('electron-log-rotate');
const fs = require('fs');
const path = require('path');
const win32 = require('./lib/win32');

log.setup({
  appName: 'node-child_process'
});

log.log('child __dirname: ' + __dirname);
log.log('path._makeLong: ' + path._makeLong('./package.json'));
log.log('win32.resolve: ' + win32.resolve('./package.json'));
log.log('process.cwd(): ' + process.cwd());

try {
  log.log('fs.readFile start');
  // このように相対パスにせずに絶対パスにしてから fs などに渡すと安全
  // fs.readFile(path.resolve(__dirname, 'package.json'), (err, data) => {
  fs.readFile('./package.json', (err, data) => {
    if (err) {
      log.log(err);
      return;
    }

    log.log(data);
    log.log('fs.readFile end');
  });
} catch (e) {
  log.log(e);
}

あとがき

基本的には、相対パスはなるべく使わずに絶対パスにしてからファイルを読んだりしたほうが安全かと思います。

ためしに親となる electron の exe から child_process 経由で子となる electron の exe をキックして、親を閉じたあとに子のほうで

fs.readFile('./package.json');

してみましたが、結果は同じでした、 process.cwd() は親プロセスが生きているかどうかで判断などはせずに起動元のワーキングディレクトリを記憶しているんですね。

node.js のライブラリなどを提供する場合、こういったパスを意識しておかないと、 child_process 経由で起動したときに意図した通りに動かない場合が出てきそうなので、気をつけたいところです。