From f52044cd3106266aa8efc1fae05e8c923315a5e5 Mon Sep 17 00:00:00 2001 From: execjosh Date: Fri, 28 Dec 2012 22:43:25 +0900 Subject: [PATCH] Emulate spawn and execFile from node.js's child_process module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a rudimentary implementation of the following methods from [node.js's `child_process` module][1]: * `spawn` * `execFile` The examples are relevant only for *nix operating systems... The following methods are Not Yet Implemented™: * `exec` * `fork` [1]: http://nodejs.org/docs/v0.8.16/api/child_process.html http://code.google.com/p/phantomjs/issues/detail?id=219 --- examples/child_process-examples.coffee | 20 +++ examples/child_process-examples.js | 27 ++++ src/bootstrap.js | 1 + src/childprocess.cpp | 136 ++++++++++++++++++++ src/childprocess.h | 93 ++++++++++++++ src/modules/child_process.js | 165 +++++++++++++++++++++++++ src/phantom.cpp | 11 ++ src/phantom.h | 7 ++ src/phantomjs.pro | 3 + src/phantomjs.qrc | 1 + 10 files changed, 464 insertions(+) create mode 100644 examples/child_process-examples.coffee create mode 100644 examples/child_process-examples.js create mode 100644 src/childprocess.cpp create mode 100644 src/childprocess.h create mode 100644 src/modules/child_process.js diff --git a/examples/child_process-examples.coffee b/examples/child_process-examples.coffee new file mode 100644 index 00000000..47e9b507 --- /dev/null +++ b/examples/child_process-examples.coffee @@ -0,0 +1,20 @@ +{spawn, execFile} = require "child_process" + +child = spawn "ls", ["-lF", "/rooot"] + +child.stdout.on "data", (data) -> + console.log "spawnSTDOUT:", JSON.stringify data + +child.stderr.on "data", (data) -> + console.log "spawnSTDERR:", JSON.stringify data + +child.on "exit", (code) -> + console.log "spawnEXIT:", code + +#child.kill "SIGKILL" + +execFile "ls", ["-lF", "/usr"], null, (err, stdout, stderr) -> + console.log "execFileSTDOUT:", JSON.stringify stdout + console.log "execFileSTDERR:", JSON.stringify stderr + +setTimeout (-> phantom.exit 0), 2000 diff --git a/examples/child_process-examples.js b/examples/child_process-examples.js new file mode 100644 index 00000000..a4970d13 --- /dev/null +++ b/examples/child_process-examples.js @@ -0,0 +1,27 @@ +var spawn = require("child_process").spawn +var execFile = require("child_process").execFile + +var child = spawn("ls", ["-lF", "/rooot"]) + +child.stdout.on("data", function (data) { + console.log("spawnSTDOUT:", JSON.stringify(data)) +}) + +child.stderr.on("data", function (data) { + console.log("spawnSTDERR:", JSON.stringify(data)) +}) + +child.on("exit", function (code) { + console.log("spawnEXIT:", code) +}) + +//child.kill("SIGKILL") + +execFile("ls", ["-lF", "/usr"], null, function (err, stdout, stderr) { + console.log("execFileSTDOUT:", JSON.stringify(stdout)) + console.log("execFileSTDERR:", JSON.stringify(stderr)) +}) + +setTimeout(function () { + phantom.exit(0) +}, 2000) diff --git a/src/bootstrap.js b/src/bootstrap.js index 0f481109..f38dc7d3 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -115,6 +115,7 @@ phantom.callback = function(callback) { // (for future, now both fs and system are loaded anyway) var nativeExports = { get fs() { return phantom.createFilesystem(); }, + get child_process() { return phantom._createChildProcess(); }, get system() { return phantom.createSystem(); } }; var extensions = { diff --git a/src/childprocess.cpp b/src/childprocess.cpp new file mode 100644 index 00000000..127fc033 --- /dev/null +++ b/src/childprocess.cpp @@ -0,0 +1,136 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "childprocess.h" + +// +// ChildProcessContext +// + +ChildProcessContext::ChildProcessContext(QObject *parent) + : QObject(parent) + , m_proc(this) +{ + connect(&m_proc, SIGNAL(readyReadStandardOutput()), this, SLOT(_readyReadStandardOutput())); + connect(&m_proc, SIGNAL(readyReadStandardError()), this, SLOT(_readyReadStandardError())); + connect(&m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(_finished(int, QProcess::ExitStatus))); + connect(&m_proc, SIGNAL(error(QProcess::ProcessError)), this, SLOT(_error(QProcess::ProcessError))); +} + +ChildProcessContext::~ChildProcessContext() +{ +} + +// public: + +qint64 ChildProcessContext::pid() const +{ + Q_PID pid = m_proc.pid(); + +#if !defined(Q_OS_WIN32) && !defined(Q_OS_WINCE) + return pid; +#else + return pid->dwProcessId; +#endif +} + +void ChildProcessContext::kill(const QString &signal) +{ + // TODO: it would be nice to be able to handle more signals + if ("SIGKILL" == signal) { + m_proc.kill(); + } else { + // Default to "SIGTERM" + m_proc.terminate(); + } +} + +void ChildProcessContext::_setEncoding(const QString &encoding) +{ + m_encoding.setEncoding(encoding); +} + +// This is affected by [QTBUG-5990](https://bugreports.qt-project.org/browse/QTBUG-5990). +// `QProcess` doesn't properly handle the situations of `cmd` not existing or +// failing to start... +bool ChildProcessContext::_start(const QString &cmd, const QStringList &args) +{ + m_proc.start(cmd, args); + // TODO: Is there a better way to do this??? + return m_proc.waitForStarted(1000); +} + +// private slots: + +void ChildProcessContext::_readyReadStandardOutput() +{ + QByteArray bytes = m_proc.readAllStandardOutput(); + emit stdoutData(m_encoding.decode(bytes)); +} + +void ChildProcessContext::_readyReadStandardError() +{ + QByteArray bytes = m_proc.readAllStandardError(); + emit stderrData(m_encoding.decode(bytes)); +} + +void ChildProcessContext::_finished(const int exitCode, const QProcess::ExitStatus exitStatus) +{ + Q_UNUSED(exitStatus) + + emit exit(exitCode); +} + +void ChildProcessContext::_error(const QProcess::ProcessError error) +{ + Q_UNUSED(error) + + emit exit(m_proc.exitCode()); +} + + +// +// ChildProcess +// + +ChildProcess::ChildProcess(QObject *parent) + : QObject(parent) +{ +} + +ChildProcess::~ChildProcess() +{ +} + +// public: + +QObject *ChildProcess::_createChildProcessContext() +{ + return new ChildProcessContext(this); +} diff --git a/src/childprocess.h b/src/childprocess.h new file mode 100644 index 00000000..543e2374 --- /dev/null +++ b/src/childprocess.h @@ -0,0 +1,93 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CHILDPROCESS_H +#define CHILDPROCESS_H + +#include +#include + +#include "encoding.h" + +/** + * This class wraps a QProcess and facilitates emulation of node.js's ChildProcess + */ +class ChildProcessContext : public QObject +{ + Q_OBJECT + Q_PROPERTY(qint64 pid READ pid) + +public: + explicit ChildProcessContext(QObject *parent = 0); + virtual ~ChildProcessContext(); + + qint64 pid() const; + Q_INVOKABLE void kill(const QString &signal = "SIGTERM"); + + Q_INVOKABLE void _setEncoding(const QString &encoding); + Q_INVOKABLE bool _start(const QString &cmd, const QStringList &args); + +signals: + void exit(const int code) const; + + /** + * For emulating `child.stdout.on("data", function (data) {})` + */ + void stdoutData(const QString &data) const; + /** + * For emulating `child.stderr.on("data", function (data) {})` + */ + void stderrData(const QString &data) const; + +private slots: + void _readyReadStandardOutput(); + void _readyReadStandardError(); + void _error(const QProcess::ProcessError error); + void _finished(const int exitCode, const QProcess::ExitStatus exitStatus); + +private: + QProcess m_proc; + Encoding m_encoding; +}; + +/** + * Helper class for child_process module + */ +class ChildProcess : public QObject +{ + Q_OBJECT + +public: + explicit ChildProcess(QObject *parent = 0); + virtual ~ChildProcess(); + + Q_INVOKABLE QObject *_createChildProcessContext(); +}; + +#endif // CHILDPROCESS_H diff --git a/src/modules/child_process.js b/src/modules/child_process.js new file mode 100644 index 00000000..6fcaaeaa --- /dev/null +++ b/src/modules/child_process.js @@ -0,0 +1,165 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var NOP = function () {} + +/** + * spawn(command, [args], [options]) + */ +exports.spawn = function (cmd, args, opts) { + var ctx = newContext() + + if (null == opts) { + opts = {} + } + + opts.encoding = opts.encoding || "utf8" + ctx._setEncoding(opts.encoding) + + ctx._start(cmd, args) + + return ctx +} + +/** + * exec(command, [options], callback) + */ +exports.exec = function (cmd, opts, cb) { + if (null == cb) { + cb = NOP + } + + return cb(new Error("NotYetImplemented")) +} + +/** + * execFile(file, args, options, callback) + */ +exports.execFile = function (cmd, args, opts, cb) { + var ctx = newContext() + + if (null == cb) { + cb = NOP + } + + if (null == opts) { + opts = {} + } + + opts.encoding = opts.encoding || "utf8" + ctx._setEncoding(opts.encoding) + + var stdout = "" + ctx.stdout.on("data", function (chunk) { + stdout += chunk + }) + + var stderr = "" + ctx.stderr.on("data", function (chunk) { + stderr += chunk + }) + + ctx.on("exit", function (code) { + return cb(null, stdout, stderr) + }) + + ctx._start(cmd, args) + + return ctx +} + +/** + * fork(modulePath, [args], [options]) + */ +exports.fork = function (modulePath, args, opts) { + throw new Error("NotYetImplemented") +} + + +// private + +function newContext() { + var ctx = exports._createChildProcessContext() + + // TODO: "Buffer" the signals and redispatch them? + + ctx.on = function (evt, cb) { + var handler + + switch (evt) { + case "exit": + handler = ctx[evt] + break + default: + break + } + + // Connect the callback to the signal + if (isFunction(handler)) { + handler.connect(cb) + } + } + + ctx.stdout = new FakeReadableStream("stdout") + ctx.stderr = new FakeReadableStream("stderr") + + // Emulates `Readable Stream` + function FakeReadableStream(streamName) { + this.on = function (evt, cb) { + switch (evt) { + case 'data': + ctx[streamName + "Data"].connect(cb) + break + default: + break + } + } + } + + return ctx +} + +function delayCallback() { + var args = 0 < arguments.length ? [].slice.call(arguments, 0) : [] + var fn = args.shift() + if (!isFunc(fn)) { + return + } + var that = this + setTimeout(function () { + fn.apply(that, args) + }, 0) +} + +function isFunction(o) { + return typeof o === 'function' +} diff --git a/src/phantom.cpp b/src/phantom.cpp index 9614799a..21854ead 100644 --- a/src/phantom.cpp +++ b/src/phantom.cpp @@ -48,6 +48,7 @@ #include "system.h" #include "callback.h" #include "cookiejar.h" +#include "childprocess.h" static Phantom *phantomInstance = NULL; @@ -58,6 +59,7 @@ Phantom::Phantom(QObject *parent) , m_returnValue(0) , m_filesystem(0) , m_system(0) + , m_childprocess(0) { QStringList args = QApplication::arguments(); @@ -341,6 +343,15 @@ QObject *Phantom::createSystem() return m_system; } +QObject *Phantom::_createChildProcess() +{ + if (!m_childprocess) { + m_childprocess = new ChildProcess(this); + } + + return m_childprocess; +} + QObject* Phantom::createCallback() { return new Callback(this); diff --git a/src/phantom.h b/src/phantom.h index e922f77c..e1e88965 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -39,6 +39,7 @@ #include "config.h" #include "replcompletable.h" #include "system.h" +#include "childprocess.h" class WebPage; class CustomPage; @@ -102,6 +103,11 @@ public: bool webdriverMode() const; + /** + * Create `child_process` module instance + */ + Q_INVOKABLE QObject *_createChildProcess(); + public slots: QObject *createWebPage(); QObject *createWebServer(); @@ -185,6 +191,7 @@ private: QVariantMap m_defaultPageSettings; FileSystem *m_filesystem; System *m_system; + ChildProcess *m_childprocess; QList > m_pages; QList > m_servers; Config m_config; diff --git a/src/phantomjs.pro b/src/phantomjs.pro index c3c915dc..92e1e7b4 100644 --- a/src/phantomjs.pro +++ b/src/phantomjs.pro @@ -25,6 +25,7 @@ HEADERS += csconverter.h \ terminal.h \ encoding.h \ config.h \ + childprocess.h \ repl.h \ replcompletable.h @@ -43,6 +44,7 @@ SOURCES += phantom.cpp \ terminal.cpp \ encoding.cpp \ config.cpp \ + childprocess.cpp \ repl.cpp \ replcompletable.cpp @@ -52,6 +54,7 @@ OTHER_FILES += \ modules/fs.js \ modules/webpage.js \ modules/webserver.js \ + modules/child_process.js \ repl.js include(gif/gif.pri) diff --git a/src/phantomjs.qrc b/src/phantomjs.qrc index 2a8a4046..31839b5a 100644 --- a/src/phantomjs.qrc +++ b/src/phantomjs.qrc @@ -8,6 +8,7 @@ modules/webserver.js modules/fs.js modules/system.js + modules/child_process.js modules/_coffee-script.js repl.js