From 2afec5cf5316b80415e6d952d53c0f29851eba1d Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 10 Jul 2012 15:07:28 -0700 Subject: [PATCH] Add ChildProcess.exec(cmd, [options]) Uses promises for failure and success states. Takes optional stderr and stdout callbacks for incremental reading. --- Atom/src/native_handler.mm | 84 ++++++++++++++++++++++++++- spec/stdlib/child-process-spec.coffee | 47 +++++++++++++++ src/stdlib/child-process.coffee | 54 ++++------------- 3 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 spec/stdlib/child-process-spec.coffee diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 68e8a687e..1460cbb7e 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -4,6 +4,7 @@ #import "AtomController.h" #import "client_handler.h" #import "PathWatcher.h" +#import #import #import @@ -18,7 +19,7 @@ NSString *stringFromCefV8Value(const CefRefPtr& value) { NativeHandler::NativeHandler() : CefV8Handler() { std::string extensionCode = "var $native = {}; (function() {"; - const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "toggleDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", "md5ForPath"}; + const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "toggleDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", "md5ForPath", "exec"}; NSUInteger arrayLength = sizeof(functionNames) / sizeof(const char *); for (NSUInteger i = 0; i < arrayLength; i++) { std::string functionName = std::string(functionNames[i]); @@ -423,5 +424,86 @@ bool NativeHandler::Execute(const CefString& name, retval = CefV8Value::CreateString([hash UTF8String]); return true; } + else if (name == "exec") { + NSString *command = stringFromCefV8Value(arguments[0]); + CefRefPtr options = arguments[1]; + CefRefPtr callback = arguments[2]; + + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:@"/bin/sh"]; + [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; + [task setArguments:[NSArray arrayWithObjects:@"-l", @"-c", command, nil]]; + + NSPipe *stdout = [NSPipe pipe]; + NSPipe *stderr = [NSPipe pipe]; + [task setStandardOutput:stdout]; + [task setStandardError:stderr]; + + CefRefPtr context = CefV8Context::GetCurrentContext(); + void (^outputHandle)(NSFileHandle *fileHandle, CefRefPtr function) = nil; + void (^taskTerminatedHandle)() = nil; + + outputHandle = ^(NSFileHandle *fileHandle, CefRefPtr function) { + context->Enter(); + + NSData *data = [fileHandle availableData]; + NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + CefV8ValueList args; + CefRefPtr retval = CefV8Value::CreateBool(YES); + CefRefPtr e; + args.push_back(CefV8Value::CreateString([contents UTF8String])); + + function->ExecuteFunction(function, args, retval, e, true); + [contents release]; + context->Exit(); + }; + + taskTerminatedHandle = ^() { + context->Enter(); + NSString *output = [[NSString alloc] initWithData:[[stdout fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + NSString *errorOutput = [[NSString alloc] initWithData:[[task.standardError fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + + CefV8ValueList args; + CefRefPtr retval = CefV8Value::CreateBool(YES); + CefRefPtr e; + + args.push_back(CefV8Value::CreateInt([task terminationStatus])); + args.push_back(CefV8Value::CreateString([output UTF8String])); + args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); + + callback->ExecuteFunction(callback, args, retval, e, true); + context->Exit(); + + stdout.fileHandleForReading.writeabilityHandler = nil; + stderr.fileHandleForReading.writeabilityHandler = nil; + }; + + task.terminationHandler = ^(NSTask *) { + dispatch_sync(dispatch_get_main_queue(), taskTerminatedHandle); + }; + + CefRefPtr stdoutFunction = options->GetValue("stdout"); + if (stdoutFunction->IsFunction()) { + stdout.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(fileHandle, stdoutFunction); + }); + }; + } + + CefRefPtr stderrFunction = options->GetValue("stderr"); + if (stderrFunction->IsFunction()) { + stderr.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(fileHandle, stderrFunction); + }); + }; + } + + [task launch]; + + return true; + } return false; }; \ No newline at end of file diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee new file mode 100644 index 000000000..74c106e06 --- /dev/null +++ b/spec/stdlib/child-process-spec.coffee @@ -0,0 +1,47 @@ +ChildProcess = require 'child-process' + +fdescribe 'Child Processes', -> + describe ".exec(command, options)", -> + it "returns a promise that resolves to stdout and stderr", -> + waitsForPromise -> + cmd = "echo 'good' && echo 'bad' >&2" + ChildProcess.exec(cmd).done (stdout, stderr) -> + expect(stdout).toBe 'good\n' + expect(stderr).toBe 'bad\n' + + describe "when `options` contains a stdout function", -> + it "calls the stdout function when new data is received", -> + stderrHandler = jasmine.createSpy "stderrHandler" + + cmd = "echo '1111' >&2 && sleep .1 && echo '2222' >&2" + ChildProcess.exec(cmd, stderr: stderrHandler) + + waitsFor -> + stderrHandler.callCount > 1 + + runs -> + expect(stderrHandler.argsForCall[0][0]).toBe "1111\n" + expect(stderrHandler.argsForCall[1][0]).toBe "2222\n" + + it "calls the stderr function when new data is received", -> + stdoutHandler = jasmine.createSpy "stdoutHandler" + + cmd = "echo 'first' && sleep .1 && echo 'second' && sleep .1 && echo 'third'" + ChildProcess.exec(cmd, stdout: stdoutHandler) + + waitsFor -> + stdoutHandler.callCount > 2 + + runs -> + expect(stdoutHandler.argsForCall[0][0]).toBe "first\n" + expect(stdoutHandler.argsForCall[1][0]).toBe "second\n" + expect(stdoutHandler.argsForCall[2][0]).toBe "third\n" + + describe "when the command fails", -> + it "executes the callback with error set to the exit status", -> + waitsForPromise -> + cmd = "exit 2" + ChildProcess.exec(cmd).fail (error) -> + expect(error.exitStatus).toBe 2 + + diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index a724439e0..18977c41e 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -1,50 +1,20 @@ # node.js child-process # http://nodejs.org/docs/v0.6.3/api/child_processes.html +$ = require 'jquery' _ = require 'underscore' module.exports = - exec: (command, options, callback) -> - callback = options if _.isFunction options +class ChildProccess + @exec: (command, options={}) -> + deferred = $.Deferred() + $native.exec command, options, (exitStatus, stdout, stdin) -> + if error != 0 + error = new Error("Exec failed (#{exitStatus}) command '#{command}'") + error.exitStatus = exitStatus + deferred.reject(error) + else + deferred.resolve(stdout, stdin) - # make a task - task = OSX.NSTask.alloc.init + deferred - # try to use their login shell - task.setLaunchPath "/bin/bash" - - # set stdin to /dev/null - task.setStandardInput OSX.NSFileHandle.fileHandleWithNullDevice - - # -l = login shell, -c = command - args = ["-l", "-c", command] - task.setArguments args - - # setup stdout and stderr - task.setStandardOutput stdout = OSX.NSPipe.pipe - task.setStandardError stderr = OSX.NSPipe.pipe - stdoutHandle = stdout.fileHandleForReading - stderrHandle = stderr.fileHandleForReading - - # begin - task.launch - - # read pipes - err = @readHandle stderrHandle - out = @readHandle stdoutHandle - - # check for a dirty exit - if not task.isRunning - code = task.terminationStatus - if code > 0 - error = new Error - error.code = code - - # call callback - callback error, out, err - - readHandle: (handle) -> - OSX.NSString. - alloc. - initWithData_encoding(handle.readDataToEndOfFile, OSX.NSUTF8StringEncoding). - toString()