Як перспектифікувати функції child_process.exec та child_process.execFile Node за допомогою Bluebird?


80

Я використовую бібліотеку обіцянок Bluebird під Node.js, це здорово! Але у мене питання:

Якщо ви подивитесь на документацію Node's 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. Це справжня проблема, про яку я кажу.


Вам потрібні і обіцянка, і ChildProcessекземпляр? Приклад коду того, як ви хочете використовувати потрібну функцію, був би корисним.
Бергі

@Bergi Так, саме так! Мені знадобились би і обіцянка, і об’єкт дочірнього процесу. Це більше схоже на теоретичне питання, тому що я вирішив свою проблему. Але ось що я хотів зробити: я хотів виконати програму з child_process.execFile, а потім хотів подати (конвеювати) дані в її stdin і прочитати її stdout. І мені потрібна була обіцянка через ланцюжок обіцянок. У всякому разі, я працював навколо нього promisifying child_process.exec замість ExecFile і запустити програму через оболонку , як це: prg <input >output. Але тепер я мушу втекти від усього (як у Windows, так і * nix) ...
Золтан

Якщо ви просто хочете отримати доступ до stdout / err, повернутий об’єкт вам не потрібен. Оскільки stdout / err - це параметри функції зворотного виклику.
KFL

3
Перевірте npmjs.com/package/child-process-promise та пов’язаний з ним код ( github.com/patrick-steele-idem/child-process-promise ).
Jason C

Або вам може сподобатися API github.com/jcoreio/promisify-child-process , який дозволяє вам простоconst {stdout, stderr} = await exec('echo test')
Енді

Відповіді:


72

Здається, ви хочете повернути дві речі із дзвінка:

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

3
Велике спасибі за це, змусило мене розпочати правильний шлях! Мені потрібно було трохи його налаштувати, щоб коди виходу оброблялись належним чином:child.addListener('exit', (code, signal) => { if (code === 0) { resolve(); } else { reject(); } });
dain

2
У child.stderr.onзворотному дзвінку ведення журналу stderrзамість stdoutбуде чіткішим.
GreenRaccoon, 23

Дякую @ GreenRaccoon23. Помилка виправлена.
Іван Гамільтон,

+1 для цього питання / відповіді; натхненний цією відповіддю, я знайшов parallel-mochas.jsподібний приклад із використанням обіцянок ES6, а не залежності від синього птаха.
mhulse

чи не краще тут використовувати .catch(err) instead of using the reject handler in .then () `?
Міладіньо

90

Я б рекомендував використовувати стандартні обіцянки 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.


3
Це дивно!
електровір

5
Це має бути прийнятою відповіддю, це працює і для багатьох інших функцій на основі зворотного виклику
Бен Віндінг

19

Ось ще один спосіб:

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.
Лукас

1
@Lucas Ви повинні опублікувати це як відповідь. const execAsync = require('util').promisify(require('child_process').exec);
MrHIDEn

9

Починаючи з 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;

Дякую. Я знайшов ці документи, але не зрозумів. Ваш приклад був дуже корисним.
Флетчер Мур

7

Напевно, немає способу зробити добре, який би охоплював усі випадки використання. Але для обмежених випадків ви можете зробити щось подібне:

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

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

9
Виходячи з цієї рекомендації, я пішов і використовував основного працівника. Я виявив, що він надзвичайно непрозорий щодо надання вихідних даних, і виявив, що він буде видавати ненульові коди виходу, коли команди будуть правильно виконані. Я б не користувався цією бібліотекою.
pbanka

Чи можете ви бути такими добрими та відкрити випуск / проблеми, якщо у вас проблеми з бібліотекою? Ми використовуємо його скрізь, де йдеться про зовнішні процеси, і це основна бібліотека нашого набору функціональних тестів, що використовується в десятках продуктивних проектів.
Тобіас

0

Ось моя. Це не стосується 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);
            }
        });
    });
};

Welp, можливо, це не перший варіант, який ви хочете спробувати вирішити свої проблеми з 'child_process', але я можу підтвердити, що це насправді працює.
поет
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.