如何使用Bluebird将Node的child_process.exec和child_process.execFile函数转换为Promises?

177

我正在使用 Node.js 下的 Bluebird Promise 库,它非常好用!但我有一个问题:

如果你看一下 Node 的 child_process.execchild_process.execFile 文档,你会发现这两个函数都返回了一个 ChildProcess 对象。

那么如何推荐地将这些函数 promisify 化呢?

请注意,以下代码是可以正常工作的(我得到了一个 Promise 对象):

var Promise = require('bluebird');
var execAsync = Promise.promisify(require('child_process').exec);
var execFileAsync = Promise.promisify(require('child_process').execFile);

但是如何获取原始的Node.js函数的返回值呢?(在这些情况下,我需要能够访问最初返回的ChildProcess对象。)

欢迎提出任何建议!

编辑:

以下是一个使用child_process.exec函数返回值的示例代码:

var exec = require('child_process').exec;
var child = exec('node ./commands/server.js');
child.stdout.on('data', function(data) {
    console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
    console.log('stderr: ' + data);
});
child.on('close', function(code) {
    console.log('closing code: ' + code);
});

但是,如果我使用经过 Promise 封装的 exec 函数(上面的 execAsync),那么返回值将是一个 Promise 对象,而不是 ChildProcess 对象。这就是我所说的真正问题。


你需要同时使用Promise和ChildProcess实例吗?提供一个代码示例来说明你想要使用的函数将会更有帮助。 - Bergi
@Bergi 是的,完全正确!我需要的是promise和child process对象。事实上,这更像是一个理论问题,因为我已经解决了我的问题。但这就是我想做的事情:我想使用 child_process.execFile 执行一个程序,然后将数据馈送(pipe)到其stdin并读取其stdout。由于存在promise链,我需要一个promise。无论如何,我通过将child_process.exec替换为promisifying execFile来解决了它,并通过类似prg <input >output 的shell运行程序。但现在我必须对所有内容进行shell转义(无论是在Windows还是*nix上)... - Zoltan
如果您只想访问stdout/err,那么您不需要返回的对象。因为stdout/err是回调函数的参数。 - KFL
3
请查看 https://www.npmjs.com/package/child-process-promise 及其相关代码(https://github.com/patrick-steele-idem/child-process-promise)。 - Jason C
或者您可能会喜欢 https://github.com/jcoreio/promisify-child-process 的API,它允许您简单地使用 const {stdout, stderr} = await exec('echo test') - Andy
10个回答

345

我建议使用语言内置的标准JS Promise,而不是像Bluebird这样的附加库依赖。

如果你正在使用Node 10+, Node.js文档 推荐使用 util.promisify,它返回一个 Promise<{stdout,stderr}> 对象。以下是一个示例:

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

async function lsExample() {
  try {
    const { stdout, stderr } = await exec('ls');
    console.log('stdout:', stdout);
    console.log('stderr:', stderr);
  } catch (e) {
    console.error(e); // should contain code (exit code) and signal (that caused the termination).
  }
}
lsExample()

首先处理来自 stderr 的错误。


11
太棒了! - derpedy-doo
24
这个应该是被接受的答案,它也适用于许多其他基于回调的函数。 - Ben Winding
4
这个回答忽略了问题的核心前提:“访问最初返回的ChildProcess对象”。 - Ivan Hamilton
也适用于TypeScript - Mochamad Arifin
3
不幸的是,如果抛出错误,即使在try块上面定义了stdoutstderrstderr也不可用,这使得故障排除非常困难。 - Micah Henning

86

看起来你希望从调用中返回两个东西:

  • ChildProcess
  • 一个在ChildProcess完成时解决的promise

那么“promisify这样的函数的推荐方式”?不要

你违反了规范。Promise返回函数应该只返回一个promise,其他都不需要。你可以返回一个具有两个成员的对象(ChildProcess和Promise),但这只会让人们感到困惑。

我建议调用未经promisify的函数,并基于返回的childProcess创建一个promise。(可以将其封装到帮助函数中)

这样,下一个读代码的人就很明确了。

像这样:

var Promise = require('bluebird');
var exec = require('child_process').execFile;

function promiseFromChildProcess(child) {
    return new Promise(function (resolve, reject) {
        child.addListener("error", reject);
        child.addListener("exit", resolve);
    });
}

var child = exec('ls');

promiseFromChildProcess(child).then(function (result) {
    console.log('promise complete: ' + result);
}, function (err) {
    console.log('promise rejected: ' + err);
});

child.stdout.on('data', function (data) {
    console.log('stdout: ' + data);
});
child.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});
child.on('close', function (code) {
    console.log('closing code: ' + code);
});

如果你只想将child_process.exec()child_process.execFile()转换为Promises,最近的Node.js版本中有一个更好的答案在这里


5
非常感谢您的帮助,让我朝着正确的方向开始了!但我需要稍微调整一下,以便正确处理退出代码:child.addListener('exit', (code, signal) => { if (code === 0) { resolve(); } else { reject(); } }); - dain
2
child.stderr.on 回调函数中,记录 stderr 而不是 stdout 会更清晰。 - GreenRaccoon23
感谢 @GreenRaccoon23。错别字已经修正。 - Ivan Hamilton
这个问题的答案值得点赞!受此答案启发,我发现了 parallel-mochas.js,这是一个类似的例子,使用ES6 promise而不是bluebird依赖。 - mhulse
在这里使用.catch(err)而不是在.then()中使用拒绝处理程序会更好,不是吗? - Miladinho

56
自 Node v12 版本起,内置的 util.promisify 允许在返回的 Promise 中访问 ChildProcess 对象,针对内置函数而言,在未进行 Promisification 的调用中也会返回该对象。根据文档

返回的 ChildProcess 实例作为 child 属性附加到 Promise 上。

这个实现正确又简单地满足了原问题中访问 ChildProcess 的需求,同时提供了使得其他答案过时的解决方案,只要使用 Node v12+ 即可。

按照提问者提供的(简洁)样式,可以通过如下方式访问 ChildProcess

const util = require('util');
const exec = util.promisify(require('child_process').exec);
const promise = exec('node ./commands/server.js');
const child = promise.child; 

child.stdout.on('data', function(data) {
    console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
    console.log('stderr: ' + data);
});
child.on('close', function(code) {
    console.log('closing code: ' + code);
});

// i.e. can then await for promisified exec call to complete
const { stdout, stderr } = await promise;

1
谢谢。我找到了那些文档,但是不太理解。你的示例非常有帮助。 - Fletcher Moore
1
不错!看起来是在v12.6.0中引入的。 - Ivan Hamilton
大量谷歌搜索才找到这个例子。谢谢!显然,手动构建Promise也可以实现,但这种解决方案更易读。 - maschwenk
命令“java -version”会破坏此代码。 const promise = exec('java -version'); - Dawit

35

这里有另外一种方式:

function execPromise(command) {
    return new Promise(function(resolve, reject) {
        exec(command, (error, stdout, stderr) => {
            if (error) {
                reject(error);
                return;
            }

            resolve(stdout.trim());
        });
    });
}

使用此函数:

execPromise(command).then(function(result) {
    console.log(result);
}).catch(function(e) {
    console.error(e.message);
});

使用async/await:

try {
    var result = await execPromise(command);
} catch (e) {
    console.error(e.message);
}

1
如果您想流式传输stdout或stderr,例如它们非常大,则这不太合适。 - Jeroen
2
你也可以使用 util.promisify 然后访问 .stdout - Lucas
2
@Lucas 你应该将它发布为答案。 const execAsync = require('util').promisify(require('child_process').exec); - MrHIDEn

9

可能没有一种方法可以覆盖所有用例,但是对于有限的情况,您可以像这样做:

/**
 * Promisified child_process.exec
 *
 * @param cmd
 * @param opts See child_process.exec node docs
 * @param {stream.Writable} opts.stdout If defined, child process stdout will be piped to it.
 * @param {stream.Writable} opts.stderr If defined, child process stderr will be piped to it.
 *
 * @returns {Promise<{ stdout: string, stderr: stderr }>}
 */
function execp(cmd, opts) {
    opts || (opts = {});
    return new Promise((resolve, reject) => {
        const child = exec(cmd, opts,
            (err, stdout, stderr) => err ? reject(err) : resolve({
                stdout: stdout,
                stderr: stderr
            }));

        if (opts.stdout) {
            child.stdout.pipe(opts.stdout);
        }
        if (opts.stderr) {
            child.stderr.pipe(opts.stderr);
        }
    });
}

这个函数接受 opts.stdoutopts.stderr 参数,以便可以从子进程中捕获 stdio。

例如:

execp('ls ./', {
    stdout: new stream.Writable({
        write: (chunk, enc, next) => {
            console.log(chunk.toString(enc));
            next();
        }
    }),
    stderr: new stream.Writable({
        write: (chunk, enc, next) => {
            console.error(chunk.toString(enc));
            next();
        }
    })
}).then(() => console.log('done!'));

或者简单地说:
execp('ls ./', {
    stdout: process.stdout,
    stderr: process.stderr
}).then(() => console.log('done!'));

5

我想提一下,有一个很好的工具可以完全解决你的问题:

https://www.npmjs.com/package/core-worker

这个包使处理进程变得更加容易。

import { process } from "CoreWorker";
import fs from "fs";

const result = await process("node Server.js", "Server is ready.").ready(1000);
const result = await process("cp path/to/file /newLocation/newFile").death();

或者将这些功能组合起来:
import { process } from "core-worker";

const simpleChat = process("node chat.js", "Chat ready");

setTimeout(() => simpleChat.kill(), 360000); // wait an hour and close the chat

simpleChat.ready(500)
    .then(console.log.bind(console, "You are now able to send messages."))
    .then(::simpleChat.death)
    .then(console.log.bind(console, "Chat closed"))
    .catch(() => /* handle err */);

11
基于这个建议,我开始使用core-worker库。但我发现它在提供输出方面非常不透明,并且即使命令已成功完成,它也会抛出非零的退出代码。因此,我不会再使用该库。 - pbanka
如果您在使用该库时遇到问题,是否可以好心地开一个 issue / issues 呢?我们在所有涉及外部进程的地方都在使用它,并且它是我们功能测试套件的核心库,在数十个生产项目中使用。 - Tobias

3
这是我的意见。使用 spawn 流式传输输出并写入 stdoutstderr。错误和标准输出被捕获到缓冲区中并返回或拒绝。 这是用 TypeScript 编写的,如果使用 JavaScript,请随意删除 typings:
import { spawn, SpawnOptionsWithoutStdio } from 'child_process'

const spawnAsync = async (
  command: string,
  options?: SpawnOptionsWithoutStdio
) =>
  new Promise<Buffer>((resolve, reject) => {
    const [spawnCommand, ...args] = command.split(/\s+/);
    const spawnProcess = spawn(spawnCommand, args, options);
    const chunks: Buffer[] = [];
    const errorChunks: Buffer[] = [];
    spawnProcess.stdout.on("data", (data) => {
      process.stdout.write(data.toString());
      chunks.push(data);
    });
    spawnProcess.stderr.on("data", (data) => {
      process.stderr.write(data.toString());
      errorChunks.push(data);
    });
    spawnProcess.on("error", (error) => {
      reject(error);
    });
    spawnProcess.on("close", (code) => {
      if (code === 1) {
        reject(Buffer.concat(errorChunks).toString());
        return;
      }
      resolve(Buffer.concat(chunks));
    });
  });

3

当您使用相同的常量进行解构时,可能会在运行多个命令时遇到问题,您可以像这样重命名它们。

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

async function runCommands() {
    try {
        const { stdout, stderr } = await exec('ls');
        console.log('stdout:', stdout);
        console.log('stderr:', stderr);

        const { stdout: stdoutTwo, stderr: stderrTwo } = await exec('ls');
        console.log('stdoutTwo:', stdoutTwo);
        console.log('stderrTwo:', stderrTwo);

        const { stdout: stdoutThree, stderr: stderrThree } = await exec('ls');
        console.log('stdoutThree:', stdoutThree);
        console.log('stderrThree:', stderrThree);

    } catch (e) {
        console.error(e); // should contain code (exit code) and signal (that caused the termination).
    }
}
runCommands()

0
这是我的。它不涉及标准输入或输出,所以如果你需要这些内容,请使用本页面上的其他答案。 :)
// promisify `child_process`
// This is a very nice trick :-)
this.promiseFromChildProcess = function (child) {
    return new Promise((resolve, reject) => {
        child.addListener('error', (code, signal) => {
            console.log('ChildProcess error', code, signal);
            reject(code);
        });
        child.addListener('exit', (code, signal) => {
            if (code === 0) {
                resolve(code);
            } else {
                console.log('ChildProcess error', code, signal);
                reject(code);
            }
        });
    });
};

好吧,也许这不是您想尝试使用“child_process”解决问题的首选,但我可以确认这确实有效。 - asceta

0
其他的例子对我来说都不起作用,因为很多命令没有返回输出,stdout和std error都没有输出,因此无法发出信号。
对于一个shell命令来说,没有输出通常是好事。
Node.js在他们的文档中使用util.promisify来实现原生的async await,但是对我来说也不起作用,原因是没有发出close和exit事件。
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
const { stdout, stderr } = await exec('ls');.

在@LachoTomov的回答中提到了Promise包装器,但它未能从我的父异步函数中返回任何内容。在发现其他事件发出时,这是对此的改进,这些事件在大多数nodejs文档的代码示例中没有提到,但在ChildProcess类属性文档中列出。
   const { exec } = require('child_process')

    let res = await execChild_process(`ls ${var}`)
    console.log('result', res)

    async function execChild_process(command) {
    /**
     * Commands with no output send the exit event.
     */
    console.log('\n execChild_process command : ', command)
    let output = await new Promise((resolve, reject) => {
        childProcess = exec(command)
        childProcess.stdout.on('data', (data) => {
            resolve(`exec success: ${data}`)
        })
        childProcess.stderr.on('data', (data) => {
            resolve(`stderr error: ${data}`)
        })
        childProcess.on('close', (code) => {
            resolve(`exec success: 'close'  ${code}`)
        })
        childProcess.on('exit', (code) => {
            resolve(`exec success: 'exit' ${code}`)
        })
        childProcess.on('error', (error) => {
            resolve(`exec error: ${error}`)
        })
    });
    return output
}

还有一些其他事件,比如断开连接、消息和生成,可能会很有用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接