Wie verspreche ich die Funktionen child_process.exec und child_process.execFile von Node mit Bluebird?

75

Ich benutze die Bluebird-Versprechensbibliothek unter Node.js, es ist großartig! Aber ich habe eine Frage:

Wenn Sie sich die Dokumentation von child_process.exec und child_process.execFile von Node ansehen , können Sie feststellen, dass beide Funktionen ein ChildProcess-Objekt zurückgeben.

Was ist der empfohlene Weg, um solche Funktionen zu versprechen?

Beachten Sie, dass Folgendes funktioniert (ich erhalte ein Promise-Objekt):

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

Aber wie kann man auf den ursprünglichen Rückgabewert der ursprünglichen Node.js-Funktionen zugreifen? (In diesen Fällen müsste ich auf die ursprünglich zurückgegebenen ChildProcess-Objekte zugreifen können.)

Jeder Vorschlag wäre dankbar!

BEARBEITEN:

Hier ist ein Beispielcode, der den Rückgabewert der Funktion child_process.exec verwendet:

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

Wenn ich jedoch die versprochene Version der exec-Funktion (execAsync von oben) verwenden würde, wäre der Rückgabewert ein Versprechen, kein ChildProcess-Objekt. Dies ist das eigentliche Problem, über das ich spreche.

Zoltan
quelle
Benötigen Sie sowohl das Versprechen als auch die ChildProcessInstanz? Ein Codebeispiel, wie Sie die gewünschte Funktion verwenden möchten, wäre hilfreich.
Bergi
@Bergi Ja genau! Ich hätte sowohl das Versprechen als auch das untergeordnete Prozessobjekt benötigt. Es ist eher eine theoretische Frage, weil ich mein Problem gelöst habe. Aber hier ist, was ich tun wollte: Ich wollte ein Programm mit child_process.execFile ausführen und dann wollte ich Daten in seine stdin einspeisen (Pipe) und seine stdout lesen. Und ich brauchte ein Versprechen wegen der Verkettung von Versprechen. Wie auch immer, ich habe es umgangen, indem ich child_process.exec anstelle von execFile versprochen und das Programm über eine Shell wie die folgende ausgeführt habe : prg <input >output. Aber jetzt muss ich alles entkommen (sowohl unter Windows als auch unter * nix) ...
Zoltan
Wenn Sie nur auf stdout / err zugreifen möchten, benötigen Sie das zurückgegebene Objekt nicht. Weil stdout / err Parameter für die Rückruffunktion sind.
KFL
3
Schauen Sie sich npmjs.com/package/child-process-promise und den zugehörigen Code an ( github.com/patrick-steele-idem/child-process-promise ).
Jason C
Oder Sie mögen vielleicht die API von github.com/jcoreio/promisify-child-process , mit der Sie einfachconst {stdout, stderr} = await exec('echo test')
Andy

Antworten:

70

Es hört sich so an, als würden Sie zwei Dinge aus dem Anruf zurückgeben möchten:

  • der ChildProcess
  • Ein Versprechen, das aufgelöst wird, wenn der ChildProcess abgeschlossen ist

Also "der empfohlene Weg, solche Funktionen zu versprechen"? Tu es nicht .

Du bist außerhalb der Konvention. Von Funktionen, die Versprechen zurückgeben, wird erwartet, dass sie ein Versprechen zurückgeben, und das war's. Sie könnten ein Objekt mit zwei Mitgliedern zurückgeben (ChildProcess & das Versprechen), aber das verwirrt die Leute nur.

Ich würde vorschlagen, die nicht versprochene Funktion aufzurufen und ein Versprechen zu erstellen, das auf dem zurückgegebenen childProcess basiert. (Vielleicht wickeln Sie das in eine Hilfsfunktion)

Auf diese Weise ist es für die nächste Person, die den Code liest, ziemlich explizit.

Etwas wie:

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);
});
Ivan Hamilton
quelle
3
Vielen Dank dafür, ich bin auf dem richtigen Weg! Ich musste es leicht child.addListener('exit', (code, signal) => { if (code === 0) { resolve(); } else { reject(); } });
anpassen,
2
Im child.stderr.onRückruf wäre die Protokollierung stderrstatt stdoutklarer.
GreenRaccoon23
Danke @ GreenRaccoon23. Tippfehler behoben.
Ivan Hamilton
+1 für diese Frage / Antwort; Inspiriert von dieser Antwort fand ich parallel-mochas.jsein ähnliches Beispiel mit ES6-Versprechungen und nicht mit der Bluebird-Abhängigkeit.
mhulse
.catch(err) instead of using the reject handler in Wäre es hier nicht besser, .then () `zu verwenden?
Miladinho
79

Ich würde empfehlen, in die Sprache integrierte Standard-JS-Versprechen über eine zusätzliche Bibliotheksabhängigkeit wie Bluebird zu verwenden.

Wenn Sie Node 10+ verwenden, empfehlen die Dokumente von Node.js,util.promisify ein Promise<{ stdout, stderr }>Objekt zurückzugeben. Siehe ein Beispiel unten:

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

Behandeln Sie Fehler zuerst von stderr.

shmck
quelle
3
Das ist großartig!
Elektrovir
5
Dies sollte die akzeptierte Antwort sein, es funktioniert auch für viele andere Callback-basierte Funktionen
Ben Winding
20

Hier ist ein anderer Weg:

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

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

Verwenden Sie die Funktion:

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

Oder mit async / warte:

try {
    var result = await execPromise(command);
} catch (e) {
    console.error(e.message);
}
Lacho Tomov
quelle
1
Dies ist nicht geeignet, wenn Sie stdout oder stderr streamen möchten, beispielsweise wenn sie sehr umfangreich sind.
Jeroen
2
Sie können auch verwenden util.promisifyund dann darauf zugreifen .stdout.
Lucas
1
@ Lucas Du solltest es als Antwort posten. const execAsync = require('util').promisify(require('child_process').exec);
MrHIDEn
7

Es gibt wahrscheinlich keine gute Möglichkeit, die alle Anwendungsfälle abdeckt. In begrenzten Fällen können Sie jedoch Folgendes tun:

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

Dies akzeptiert opts.stdoutund opts.stderrargumentiert, so dass stdio aus dem untergeordneten Prozess erfasst werden kann.

Zum Beispiel:

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

Oder einfach:

execp('ls ./', {
    stdout: process.stdout,
    stderr: process.stderr
}).then(() => console.log('done!'));
Edan
quelle
5

Ich möchte nur erwähnen, dass es ein nettes Tool gibt, das Ihr Problem vollständig löst:

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

Dieses Paket erleichtert die Abwicklung von Prozessen erheblich.

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

oder kombinieren Sie diese Funktionen:

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 */);
Tobias
quelle
9
Aufgrund dieser Empfehlung ging ich los und setzte Core-Worker ein. Ich fand es in Bezug auf die Bereitstellung der Ausgabe äußerst undurchsichtig und stellte fest, dass es Exit-Codes ungleich Null auslösen würde, wenn die Befehle ordnungsgemäß ausgeführt würden. Ich würde diese Bibliothek nicht benutzen.
Pbanka
Können Sie so nett sein und ein Problem / Probleme eröffnen, wenn Sie Probleme mit der Bibliothek haben? Wir verwenden es überall dort, wo es um externe Prozesse geht, und es ist eine Kernbibliothek unserer Funktionstestsuite, die in Dutzenden von produktiven Projekten verwendet wird.
Tobias
5

Seit Node v12 ermöglicht das eingebaute Objekt util.promisifyden Zugriff auf das ChildProcessObjekt in den zurückgegebenen Promiseintegrierten Funktionen, wo es durch den nicht versprochenen Aufruf zurückgegeben worden wäre. Aus den Dokumenten :

Die zurückgegebene ChildProcessInstanz wird Promiseals childEigenschaft an die angehängt .

Dies erfüllt korrekt und einfach die Notwendigkeit, auf ChildProcessdie ursprüngliche Frage zuzugreifen , und macht andere Antworten veraltet, vorausgesetzt, dass Node v12 + verwendet werden kann.

Durch Anpassen des vom Fragesteller bereitgestellten Beispiels (und des prägnanten Stils) kann der Zugriff auf das ChildProcesswie folgt erreicht werden:

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;
user1823021
quelle
-1

Hier ist meins. Es geht nicht um stdin oder stdout. Wenn Sie diese benötigen, verwenden Sie eine der anderen Antworten auf dieser Seite. :) :)

// 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);
            }
        });
    });
};
Jay
quelle