Как обещать Node's child_process.exec и child_process.execFile функции с Bluebird?

Я использую библиотеку обещаний Bluebird под Node.js, это здорово! Но у меня есть вопрос:

Если вы посмотрите на документацию Node child_process.exec и child_process.execFile, вы увидите, что обе эти функции возвращают объект ChildProcess.

Так каков рекомендуемый способ обещать такие функции?

Обратите внимание, что работает следующее (я получаю объект 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);
});

Но если бы я использовал обещанную версию функции exec (execAsync сверху), тогда возвращаемое значение будет обещанием, а не объектом ChildProcess. Это настоящая проблема, о которой я говорю.

10 ответов

Решение

Похоже, вы хотели бы вернуть две вещи из звонка:

  • ChildProcess
  • обещание, которое разрешается после завершения ChildProcess

Так "рекомендуемый способ обещать такие функции"? Не

Вы вне конвенции. Ожидается, что функции возврата обещания вернут обещание, и все. Вы можете вернуть объект с двумя членами (ChildProcess и обещание), но это только сбьет людей с толку.

Я бы предложил вызвать непроизведенную функцию и создать обещание на основе возвращенного childProcess. (Может быть, обернуть это в вспомогательную функцию)

Таким образом, это совершенно очевидно для следующего человека, который читает код.

Что-то вроде:

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);
});

Я бы рекомендовал использовать стандартные JS-обещания, встроенные в язык, вместо дополнительных библиотечных зависимостей, таких как 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.

Начиная с Node v12, встроенный util.promisify позволяет получить доступ к ChildProcess объект в возвращенном Promiseдля встроенных функций, где он был бы возвращен необещанным вызовом. Из документов:

Вернувшийся ChildProcess экземпляр прикреплен к Promise как child свойство.

Это правильно и просто удовлетворяет потребность в доступе 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;

Вот еще один способ:

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(error) {
    console.error(e.message);
});

Или с async/await:

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

Вероятно, нет способа сделать это красиво, который охватывает все варианты использования. Но для ограниченных случаев вы можете сделать что-то вроде этого:

/**
 * 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.stdout а также opts.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!'));

Просто хочу отметить, что есть хороший инструмент, который полностью решит вашу проблему:

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 */);

Еще один пример, когда вы можете столкнуться с проблемами при запуске нескольких команд при деструктуризации с одними и теми же константами, вы можете переименовать их вот так.

      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()

Вот мои два цента. Использует spawn, который передает вывод и записывает в stdout а также stderr. Ошибка и стандартный вывод фиксируются в буферах и возвращаются или отклоняются.

Это написано в Typescript, не стесняйтесь удалять типизацию при использовании JavaScript:

      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));
    });
  });

Другие примеры у меня не сработали, потому что многие команды не возвращали выходные данные, поэтому ошибки stdout и std не выдавались.

Отсутствие вывода часто является хорошим выводом для команды оболочки.

В документации Node js есть встроенное асинхронное ожидание с использованием util.promisify, но у меня оно не сработало по той же причине (не генерировало события закрытия и выхода).

      const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
const { stdout, stderr } = await exec('ls');.

Обертка обещания была упомянута в ответе @LachoTomov, но она не вернула ничего из моей родительской асинхронной функции. Это улучшение после обнаружения других исходящих событий, которые не представлены в большинстве примеров кода в документации 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
}

Есть также пара других событий, таких как отключение, сообщение и появление, которые могут быть полезны.

Вот мой. Он не касается stdin или stdout, поэтому, если они вам нужны, используйте один из других ответов на этой странице.:)

// 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);
            }
        });
    });
};
Другие вопросы по тегам