diff --git a/spec/spawner-spec.coffee b/spec/spawner-spec.coffee new file mode 100644 index 000000000..4ae2149e5 --- /dev/null +++ b/spec/spawner-spec.coffee @@ -0,0 +1,57 @@ +ChildProcess = require 'child_process' +Spawner = require '../src/browser/spawner' + +describe "Spawner", -> + beforeEach -> + # Prevent any commands from actually running and affecting the host + originalSpawn = ChildProcess.spawn + + harmlessSpawn = + # Just spawn something that won't actually modify the host + if process.platform is 'win32' + originalSpawn('dir') + else + originalSpawn('ls') + + spyOn(ChildProcess, 'spawn').andCallFake (command, args, callback) -> + harmlessSpawn + + it "invokes passed callback", -> + someCallback = jasmine.createSpy('someCallback') + + Spawner.spawn('some-command', 'some-args', someCallback) + + waitsFor -> + someCallback.callCount is 1 + + it "spawns passed command with arguments", -> + actualCommand = null + actualArgs = null + + # Redefine fake invocation, so to remember passed arguments + jasmine.unspy(ChildProcess, 'spawn') + spyOn(ChildProcess, 'spawn').andCallFake (command, args) -> + actualCommand = command + actualArgs = args + harmlessSpawn + + expectedCommand = 'some-command' + expectedArgs = 'some-args' + someCallback = jasmine.createSpy('someCallback') + + Spawner.spawn(expectedCommand, expectedArgs, someCallback) + + expect(actualCommand).toBe expectedCommand + expect(actualArgs).toBe expectedArgs + + it "ignores errors by spawned process", -> + # Redefine fake invocation, so to cause an error + jasmine.unspy(ChildProcess, 'spawn') + spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY") + + someCallback = jasmine.createSpy('someCallback') + + expect(Spawner.spawn('some-command', 'some-args', someCallback)).toBe undefined + + waitsFor -> + someCallback.callCount is 1 diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee index e3891ed1c..e11fb1f00 100644 --- a/spec/squirrel-update-spec.coffee +++ b/spec/squirrel-update-spec.coffee @@ -1,39 +1,22 @@ -ChildProcess = require 'child_process' {EventEmitter} = require 'events' fs = require 'fs-plus' path = require 'path' temp = require 'temp' SquirrelUpdate = require '../src/browser/squirrel-update' +Spawner = require '../src/browser/spawner' describe "Windows Squirrel Update", -> tempHomeDirectory = null - originalSpawn = ChildProcess.spawn - - harmlessSpawn = -> - # Just spawn something that won't actually modify the host - if process.platform is 'win32' - originalSpawn('dir') - else - originalSpawn('ls') beforeEach -> # Prevent the actual home directory from being manipulated tempHomeDirectory = temp.mkdirSync('atom-temp-home-') spyOn(fs, 'getHomeDirectory').andReturn(tempHomeDirectory) - # Prevent any commands from actually running and affecting the host - spyOn(ChildProcess, 'spawn').andCallFake (command, args) -> - harmlessSpawn() - - it "ignores errors spawning Squirrel", -> - jasmine.unspy(ChildProcess, 'spawn') - spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY") - - app = quit: jasmine.createSpy('quit') - expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')).toBe true - - waitsFor -> - app.quit.callCount is 1 + # Prevent any spawned command from actually running and affecting the host + spyOn(Spawner, 'spawn').andCallFake (command, args, callback) -> + # do nothing on command, just run passed callback + invokeCallback callback it "quits the app on all squirrel events", -> app = quit: jasmine.createSpy('quit') @@ -69,51 +52,52 @@ describe "Windows Squirrel Update", -> describe "Desktop shortcut", -> desktopShortcutPath = '/non/existing/path' - + beforeEach -> desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk') - jasmine.unspy(ChildProcess, 'spawn') - spyOn(ChildProcess, 'spawn').andCallFake (command, args) -> + jasmine.unspy(Spawner, 'spawn') + spyOn(Spawner, 'spawn').andCallFake (command, args, callback) -> if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut' - fs.writeFileSync(path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'), '') - harmlessSpawn() + fs.writeFileSync(desktopShortcutPath, '') else - throw new Error("API not mocked") - + # simply ignore other commands + + invokeCallback callback + it "does not exist before install", -> expect(fs.existsSync(desktopShortcutPath)).toBe false - + describe "on install", -> beforeEach -> app = quit: jasmine.createSpy('quit') SquirrelUpdate.handleStartupEvent(app, '--squirrel-install') waitsFor -> app.quit.callCount is 1 - + it "creates desktop shortcut", -> expect(fs.existsSync(desktopShortcutPath)).toBe true - + describe "when shortcut is deleted and then app is updated", -> beforeEach -> fs.removeSync(desktopShortcutPath) expect(fs.existsSync(desktopShortcutPath)).toBe false - + app = quit: jasmine.createSpy('quit') SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated') waitsFor -> app.quit.callCount is 1 - + it "does not recreate shortcut", -> expect(fs.existsSync(desktopShortcutPath)).toBe false - + describe "when shortcut is kept and app is updated", -> beforeEach -> app = quit: jasmine.createSpy('quit') SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated') waitsFor -> app.quit.callCount is 1 - + it "still has desktop shortcut", -> expect(fs.existsSync(desktopShortcutPath)).toBe true @@ -125,7 +109,13 @@ describe "Windows Squirrel Update", -> SquirrelUpdate.restartAtom(app) expect(app.quit.callCount).toBe 1 - expect(ChildProcess.spawn.callCount).toBe 0 + expect(Spawner.spawn.callCount).toBe 0 app.emit('will-quit') - expect(ChildProcess.spawn.callCount).toBe 1 - expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'atom.cmd' + expect(Spawner.spawn.callCount).toBe 1 + expect(path.basename(Spawner.spawn.argsForCall[0][0])).toBe 'atom.cmd' + +# Run passed callback as Spawner.spawn() would do +invokeCallback = (callback) -> + error = null + stdout = '' + callback?(error, stdout) diff --git a/src/browser/spawner.coffee b/src/browser/spawner.coffee new file mode 100644 index 000000000..edf93182e --- /dev/null +++ b/src/browser/spawner.coffee @@ -0,0 +1,36 @@ +ChildProcess = require 'child_process' + +# Spawn a command and invoke the callback when it completes with an error +# and the output from standard out. +# +# * `command` The underlying OS command {String} to execute. +# * `args` (optional) The {Array} with arguments to be passed to command. +# * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments: +# * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown. +# * `code` Error code returned by the command. +# * `stdout` The {String} output text generated by the command. +# * `stdout` The {String} output text generated by the command. +# +# Returns `undefined`. +exports.spawn = (command, args, callback) -> + stdout = '' + + try + spawnedProcess = ChildProcess.spawn(command, args) + catch error + # Spawn can throw an error + process.nextTick -> callback?(error, stdout) + return + + spawnedProcess.stdout.on 'data', (data) -> stdout += data + + error = null + spawnedProcess.on 'error', (processError) -> error ?= processError + spawnedProcess.on 'close', (code, signal) -> + error ?= new Error("Command failed: #{signal ? code}") if code isnt 0 + error?.code ?= code + error?.stdout ?= stdout + callback?(error, stdout) + # This is necessary if using Powershell 2 on Windows 7 to get the events to raise + # http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs + spawnedProcess.stdin.end() diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee index 9d6f772db..7afc7641f 100644 --- a/src/browser/squirrel-update.coffee +++ b/src/browser/squirrel-update.coffee @@ -1,6 +1,6 @@ -ChildProcess = require 'child_process' fs = require 'fs-plus' path = require 'path' +Spawner = require './spawner' appFolder = path.resolve(process.execPath, '..') rootAtomFolder = path.resolve(appFolder, '..') @@ -25,35 +25,9 @@ backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe' environmentKeyPath = 'HKCU\\Environment' -# Spawn a command and invoke the callback when it completes with an error -# and the output from standard out. -spawn = (command, args, callback) -> - stdout = '' - - try - spawnedProcess = ChildProcess.spawn(command, args) - catch error - # Spawn can throw an error - process.nextTick -> callback?(error, stdout) - return - - spawnedProcess.stdout.on 'data', (data) -> stdout += data - - error = null - spawnedProcess.on 'error', (processError) -> error ?= processError - spawnedProcess.on 'close', (code, signal) -> - error ?= new Error("Command failed: #{signal ? code}") if code isnt 0 - error?.code ?= code - error?.stdout ?= stdout - callback?(error, stdout) - # This is necessary if using Powershell 2 on Windows 7 to get the events to raise - # http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs - spawnedProcess.stdin.end() - - # Spawn reg.exe and callback when it completes spawnReg = (args, callback) -> - spawn(regPath, args, callback) + Spawner.spawn(regPath, args, callback) # Spawn powershell.exe and callback when it completes spawnPowershell = (args, callback) -> @@ -69,16 +43,16 @@ spawnPowershell = (args, callback) -> args.unshift('RemoteSigned') args.unshift('-ExecutionPolicy') args.unshift('-noprofile') - spawn(powershellPath, args, callback) + Spawner.spawn(powershellPath, args, callback) # Spawn setx.exe and callback when it completes spawnSetx = (args, callback) -> - spawn(setxPath, args, callback) + Spawner.spawn(setxPath, args, callback) # Spawn the Update.exe with the given arguments and invoke the callback when # the command completes. spawnUpdate = (args, callback) -> - spawn(updateDotExe, args, callback) + Spawner.spawn(updateDotExe, args, callback) # Install the Open with Atom explorer context menu items via the registry. installContextMenu = (callback) -> @@ -220,7 +194,7 @@ exports.existsSync = -> exports.restartAtom = (app) -> if projectPath = global.atomApplication?.lastFocusedWindow?.projectPath args = [projectPath] - app.once 'will-quit', -> spawn(path.join(binFolder, 'atom.cmd'), args) + app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args) app.quit() # Handle squirrel events denoted by --squirrel-* command line arguments.