/* eslint no-sync:0 */
// Import
const TaskGroup = require('taskgroup')
const typeChecker = require('typechecker')
const safefs = require('safefs')
const fsUtil = require('fs')
const pathUtil = require('path')
const extractOptsAndCallback = require('extract-opts')
// Prepare
const isWindows = (process.platform || '').indexOf('win') === 0
// =====================================
// Define Globals
// Prepare
if ( global.safepsGlobal == null ) {
global.safepsGlobal = {}
}
// Define Global Pool
// Create a pool with the concurrency of our max number of open processes
if ( global.safepsGlobal.pool == null ) {
global.safepsGlobal.pool = new TaskGroup().setConfig({
concurrency: process.env.NODE_MAX_OPEN_PROCESSES == null ? 100 : process.env.NODE_MAX_OPEN_PROCESSES,
pauseOnError: false
}).run()
}
// =====================================
// Define Module
/**
* Contains methods to safely spawn and manage
* various file system processes. It differs
* from the standard node.js child_process
* (https://nodejs.org/docs/v0.11.13/api/child_process.html#child_process_child_process)
* module in that it intercepts and handles
* many common errors that might occur when
* invoking child processes that could cause
* an application to crash. Most commonly, errors
* such as ENOENT and EACCESS. This enables
* an application to be both cleaner and more robust.
* @class safeps
* @static
*/
const safeps = {
// =====================================
// Open and Close Processes
/**
* Open a file.
* Pass your callback to fire when it is safe to open the process
* @method openProcess
* @param {Function} fn callback
*/
openProcess: function (fn) {
// Add the task to the pool and execute it right away
global.safepsGlobal.pool.addTask(fn)
// Chain
return safeps
},
// =================================
// Environments
// @TODO These should be abstracted out into their own packages
/**
* Returns whether or not we are running on a windows machine
* @method isWindows
* @return {Boolean}
*/
isWindows: function () {
return isWindows
},
/**
* Get locale code - eg: en-AU,
* fr-FR, zh-CN etc.
* @method getLocaleCode
* @param {String} lang
* @return {String}
*/
getLocaleCode: function (lang) {
lang = lang || process.env.LANG || ''
const localeCode = lang.replace(/\..+/, '').replace('-', '_').toLowerCase() || null
return localeCode
},
/**
* Given the localeCode, return
* the language code.
* @method getLanguageCode
* @param {String} localeCode
* @return {String}
*/
getLanguageCode: function (localeCode) {
localeCode = safeps.getLocaleCode(localeCode) || ''
const languageCode = localeCode.replace(/^([a-z]+)[_-]([a-z]+)$/i, '$1').toLowerCase() || null
return languageCode
},
/**
* Given the localeCode, return
* the country code.
* @method getCountryCode
* @param {String} localeCode
* @return {String}
*/
getCountryCode: function (localeCode) {
localeCode = safeps.getLocaleCode(localeCode) || ''
const countryCode = localeCode.replace(/^([a-z]+)[_-]([a-z]+)$/i, '$2').toLowerCase() || null
return countryCode
},
// =================================
// Executeable Helpers
/**
* Has spawn sync. Returns true
* if the child_process spawnSync
* method exists, otherwise false
* @method hasSpawnSync
* @return {Boolean}
*/
hasSpawnSync: function () {
return require('child_process').spawnSync != null
},
/**
* Has exec sync. Returns true
* if the child_process execSync
* method exists, otherwise false
* @method hasExecSync
* @return {Boolean}
*/
hasExecSync: function () {
return require('child_process').execSync != null
},
/**
* Is the path to a file object an executable?
* Boolean result returned as the isExecutable parameter
* of the passed callback.
* next(err, isExecutable)
* @method isExecutable
* @param {String} path path to test
* @param {Object} [opts]
* @param {Boolean} [opts.sync] true to test sync rather than async
* @param {Function} next callback
* @param {Error} next.err
* @param {Boolean} next.isExecutable
* @return {Boolean} returned if opts.sync = true
*/
isExecutable: function (path, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// Sync?
if ( opts.sync ) {
return safeps.isExecutableSync(path, opts, next)
}
// Access (Node 0.12+)
if ( fsUtil.access ) {
fsUtil.access(path, fsUtil.X_OK, function (err) {
const isExecutable = !err
return next(null, isExecutable)
})
}
// Shim
else {
require('child_process').exec(path + ' --version', function (err) {
// If there was no error, then execution worked fine, so we are executable
if ( !err ) return next(null, true)
// If there was an error
// determine if it was an error with trying to run it (not executable)
// or an error from running it (executable)
const isExecutable = err.code !== 127 && (/EACCESS|Permission denied/).test(err.message) === false
return next(null, isExecutable)
})
}
// Chain
return safeps
},
/**
* Is the path to a file object an executable?
* Synchronised version of isExecutable
* @method isExecutableSync
* @param {String} path path to test
* @param {Object} opts
* @param {Function} [next]
* @param {Error} next.err
* @param {Boolean} next.isExecutable
* @return {Boolean}
*/
isExecutableSync: function (path, opts, next) {
// Prepare
let isExecutable
// Access (Node 0.12+)
if ( fsUtil.accessSync ) {
try {
fsUtil.accessSync(path, fsUtil.X_OK)
isExecutable = true
}
catch ( err ) {
isExecutable = false
}
}
// Shim
else {
try {
require('child_process').execSync(path + ' --version')
isExecutable = true
}
catch ( err ) {
// If there was an error
// determine if it was an error with trying to run it (not executable)
// or an error from running it (executable)
isExecutable = err.code !== 127 && (/EACCESS|Permission denied/).test(err.message) === false
}
}
// Return
if ( next ) {
next(null, isExecutable)
return safeps
}
else {
return isExecutable
}
},
/**
* Internal: Prepare options for an execution.
* Makes sure all options are populated or exist and
* gives the opportunity to prepopulate some of those
* options.
* @private
* @method prepareExecutableOptions
* @param {Object} [opts]
* @param {Stream} [opts.stdin=null] in stream
* @param {Array} [opts.stdio=null] Child's stdio configuration
* @param {Boolean} [opts.safe=true]
* @param {Object} [opts.env=process.env]
* @return {Object} opts
*/
prepareExecutableOptions: function (opts) {
// Prepare
opts = opts || {}
// Ensure all options exist
if ( typeof opts.stdin === 'undefined' ) opts.stdin = null
if ( typeof opts.stdio === 'undefined' ) opts.stdio = null
// By default make sure execution is valid
if ( opts.safe == null ) opts.safe = true
// If a direct pipe then don't do output modifiers
if ( opts.stdio ) {
opts.read = opts.output = false
opts.outputPrefix = null
}
// Otherwise, set output modifiers
else {
if ( opts.read == null ) opts.read = true
if ( opts.output == null ) opts.output = !!opts.outputPrefix
if ( opts.outputPrefix == null ) opts.outputPrefix = null
}
// By default inherit environment variables
if ( opts.env == null ) {
opts.env = process.env
}
// If we don't want to inherit environment variables, then don't
else if ( opts.env === false ) {
opts.env = null
}
// Return
return opts
},
/**
* Internal: Prepare result of an execution
* @private
* @method updateExecutableResult
* @param {Object} result
* @param {Object} result.pid Number Pid of the child process
* @param {Object} result.output output Array Array of results from stdio output
* @param {Stream} result.stdout stdout The contents of output
* @param {Stream} result.stderr stderr The contents of output
* @param {Number} result.status status The exit code of the child process
* @param {String} result.signal signal The signal used to kill the child process
* @param {Error} result.error The error object if the child process failed or timed out
* @param {Object} [opts]
* @param {Object} [opts.output]
* @param {Object} [opts.outputPrefix]
* @return {Object} result
*/
updateExecutableResult: function (result, opts) {
// If we want to output, then output the correct streams with the correct prefixes
if ( opts.output ) {
safeps.outputData(result.stdout, 'stdout', opts.outputPrefix)
safeps.outputData(result.stderr, 'stderr', opts.outputPrefix)
}
// If we already have an error, then don't continue
if ( result.error ) {
return result
}
// We don't already have an error, so let's check the status code for an error
// Check if the status code exists, and if it is not zero, zero is the success code
if ( result.status != null && result.status !== 0 ) {
let message = 'Command exited with a non-zero status code.'
// As there won't be that much information on this error, as it was not already provided
// we should output the stdout if we have it
if ( result.stdout ) {
const tmp = safeps.prefixData(result.stdout)
if ( tmp ) {
message += "\nThe command's stdout output:\n" + tmp
}
}
// and output the stderr if we have it
if ( result.stderr ) {
const tmp = safeps.prefixData(result.stderr)
if ( tmp ) {
message += "\nThe command's stderr output:\n" + tmp
}
}
// and create the error from that output
result.error = new Error(message)
return result
}
// Success
return result
},
/**
* Internal: prefix data
* @private
* @method prefixData
* @param {Object} data
* @param {String} [prefix = '>\t']
* @return {Object} data
*/
prefixData: function (data, prefix = '>\t') {
data = data && data.toString && data.toString() || ''
if ( prefix && data ) {
data = prefix + data.trim().replace(/\n/g, '\n' + prefix) + '\n'
}
return data
},
/**
* Internal: Set output data
* @private
* @method outputData
* @param {Object} data
* @param {Object} [channel = 'stdout']
* @param {Object} prefix
*/
outputData: function (data, channel = 'stdout', prefix) {
if ( data.toString().trim().length !== 0 ) {
if ( prefix ) {
data = safeps.prefixData(data, prefix)
}
process[channel].write(data)
}
return null
},
// =================================
// Spawn
/**
* Syncronised version of safeps.spawn. Will not return until the
* child process has fully closed. Results can be returned
* from the method call or via a passed callback. Even if
* a callback is passed to spawnSync, the method will still
* be syncronised with the child process and the callback will
* only return after the child process has closed.
*
* Simple usage example:
*
* var safeps = require('safeps');
* var command = ['npm', 'install', 'jade', '--save'];
*
* //a lot of the time you won't need the opts argument
* var opts = {
* cwd: __dirname //this is actually pointless in a real application
* };
*
* var result = safeps.spawnSync(command, opts);
*
* console.log(result.error);
* console.log(result.status);
* console.log(result.signal);
* console.log("I've finished...");
*
* @method spawnSync
* @param {Array|String} command
* @param {Object} [opts]
* @param {Boolean} [opts.safe] Whether to check the executable path.
* @param {String} [opts.cwd] Current working directory of the child process
* @param {Array|String} [opts.stdio] Child's stdio configuration.
* @param {Array} [opts.customFds] Deprecated File descriptors for the child to use for stdio.
* @param {Object} [opts.env] Environment key-value pairs.
* @param {Boolean} [opts.detached] The child will be a process group leader.
* @param {Number} [opts.uid] Sets the user identity of the process.
* @param {Number} [opts.gid] Sets the group identity of the process
* @param {Function} [next] callback
* @param {Error} next.error
* @param {Stream} next.stdout out stream
* @param {Stream} next.stderr error stream
* @param {Number} next.status node.js exit code
* @param {String} next.signal unix style signal such as SIGKILL or SIGHUP
* @return {Object} {error, pid, output, stdout, stderr, status, signal}
*/
spawnSync: function (command, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
opts = safeps.prepareExecutableOptions(opts)
opts.sync = true
// If the command is a string, then convert it into an array
if ( typeChecker.isString(command) ) {
command = command.split(' ')
}
// Get correct executable path
// Only possible if sync abilities are possible (node 0.12 and up) or if it is cached
// Otherwise, don't worry about it and output a warning to stderr
if ( opts.safe ) {
let wasSync = 0
safeps.getExecPath(command[0], opts, function (err, execPath) {
if ( err ) return
command[0] = execPath
wasSync = 1
})
if ( wasSync === 0 ) {
process.stderr.write('safeps.spawnSync: was unable to get the executable path synchronously')
}
}
// Spawn Synchronously
let result = require('child_process').spawnSync(command[0], command.slice(1), opts)
safeps.updateExecutableResult(result, opts)
// Complete
if ( next ) {
next(result.error, result.stdout, result.stderr, result.status, result.signal)
}
else {
return result
}
},
/**
* Wrapper around node's spawn command for a cleaner, more robust and powerful API.
* Launches a new process with the given command. Command line arguments are
* part of the command parameter (unlike the node.js spawn). Command can be
* an array of command line arguments or a command line string. Opts allows
* additional options to be sent to the spawning action.
*
* Simple usage example:
*
* var safeps = require('safeps');
* var command = ['npm', 'install','jade','--save'];
*
* //a lot of the time you won't need the opts argument
* var opts = {
* cwd: __dirname //this is actually pointless in a real application
* }
* function myCallback(error, stdout, stderr, status, signal){
* console.log(error);
* console.log(status);
* console.log(signal);
* console.log("I've finished...");
* }
* safeps.spawn(command, opts, myCallback);
*
* @method spawn
* @param {Array|String} command
* @param {Object} [opts]
* @param {Boolean} [opts.safe] Whether to check the executable path.
* @param {String} [opts.cwd] Current working directory of the child process
* @param {Array|String} [opts.stdio] Child's stdio configuration.
* @param {Array} [opts.customFds] Deprecated File descriptors for the child to use for stdio.
* @param {Object} [opts.env] Environment key-value pairs.
* @param {Boolean} [opts.detached] The child will be a process group leader.
* @param {Number} [opts.uid] Sets the user identity of the process.
* @param {Number} [opts.gid] Sets the group identity of the process.
* @param {Function} next callback
* @param {Error} next.error
* @param {Stream} next.stdout out stream
* @param {Stream} next.stderr error stream
* @param {Number} next.status node.js exit code
* @param {String} next.signal unix style signal such as SIGKILL or SIGHUP
*/
spawn: function (command, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
opts = safeps.prepareExecutableOptions(opts)
// Check if we want sync instead
if ( opts.sync ) {
return safeps.spawnSync(command, opts, next)
}
// Patience
safeps.openProcess(function (closeProcess) {
// If the command is a string, then convert it into an array
if ( typeChecker.isString(command) ) {
command = command.split(' ')
}
// Prepare
const result = {
pid: null,
stdout: null,
stderr: null,
output: null,
error: null,
status: null,
signal: null
}
let exited = false
// Tasks
const tasks = new TaskGroup().done(function (err) {
exited = true
closeProcess()
next(err || result.error, result.stdout, result.stderr, result.status, result.signal)
})
// Get correct executable path
if ( opts.safe ) {
tasks.addTask(function (complete) {
safeps.getExecPath(command[0], opts, function (err, execPath) {
if ( err ) return complete(err)
command[0] = execPath
complete()
})
})
}
// Spawn
tasks.addTask(function (complete) {
// Spawn
result.pid = require('child_process').spawn(command[0], command.slice(1), opts)
// Write if we want to
// result.pid.stdin may be null of stdio is 'inherit'
if ( opts.stdin && result.pid.stdin) {
result.pid.stdin.write(opts.stdin)
result.pid.stdin.end()
}
// Read if we want to by listening to the streams and updating our result variables
if ( opts.read ) {
// result.pid.stdout may be null of stdio is 'inherit'
if ( result.pid.stdout ) {
result.pid.stdout.on('data', function (data) {
if ( opts.output ) {
safeps.outputData(data, 'stdout', opts.outputPrefix)
}
if ( result.stdout ) {
result.stdout = Buffer.concat([result.stdout, data])
}
else {
result.stdout = data
}
})
}
// result.pid.stderr may be null of stdio is 'inherit'
if ( result.pid.stderr ) {
result.pid.stderr.on('data', function (data) {
if ( opts.output) {
safeps.outputData(data, 'stderr', opts.outputPrefix)
}
if ( result.stderr ) {
result.stderr = Buffer.concat([result.stderr, data])
}
else {
result.stderr = data
}
})
}
}
// Wait
result.pid.on('close', function (status, signal) {
// Apply to local global
result.status = status
result.signal = signal
// Check if we have already exited due to domains
// as without this, then we will fire the completion callback twice
// once for the domain error that will happen first
// then again for the close error
// if it happens the other way round, close, then error, we want to be alerted of that
if ( exited === true ) return
// Check result and complete
opts.output = false
safeps.updateExecutableResult(result, opts)
return complete(result.error)
})
})
// Run
tasks.run()
})
// Chain
return safeps
},
/**
* Spawn multiple processes in the one method call.
* Launches new processes with the given array of commands.
* Each item in the commands array represents a command parameter
* sent to the safeps.spawn method, so each item can be a command line
* string or an array of command line inputs. It is also possible
* to pass a single command string and in this case calling
* spawnMultiple will be effectively the same as calling safeps.spawn.
* @method spawnMultiple
* @param {Array|String} commands
* @param {Object} [opts]
* @param {Boolean} [opts.concurrency=1] Whether to spawn processes concurrently.
* @param {String} opts.cwd Current working directory of the child process.
* @param {Array|String} opts.stdio Child's stdio configuration.
* @param {Array} opts.customFds Deprecated File descriptors for the child to use for stdio.
* @param {Object} opts.env Environment key-value pairs.
* @param {Boolean} opts.detached The child will be a process group leader.
* @param {Number} opts.uid Sets the user identity of the process.
* @param {Number} opts.gid Sets the group identity of the process.
* @param {Function} next callback
* @param {Error} next.error
* @param {Array} next.results array of spawn results
* @param {Stream} next.results[i].stdout out stream
* @param {Stream} next.results[i].stderr error stream
* @param {Number} next.results[i].status node.js exit code
* @param {String} next.results[i].signal unix style signal such as SIGKILL or SIGHUP
*/
spawnMultiple: function (commands, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
const results = []
// Be synchronous by default
if ( opts.concurrency == null ) opts.concurrency = 1
// Make sure we send back the arguments
const tasks = new TaskGroup().setConfig({concurrency: opts.concurrency}).done(function (err) {
next(err, results)
})
// Prepare tasks
if ( !typeChecker.isArray(commands) ) {
commands = [commands]
}
// Add tasks
commands.forEach(function (command) {
tasks.addTask(function (complete) {
safeps.spawn(command, opts, function (...args) {
const err = args[0] || null
results.push(args)
complete(err)
})
})
})
// Run the tasks
tasks.run()
// Chain
return safeps
},
// =================================
// Exec
/**
* Syncronised version of safeps.exec. Runs a command in a shell and
* buffers the output. Will not return until the
* child process has fully closed. Results can be returned
* from the method call or via a passed callback. Even if
* a callback is passed to execSync, the method will still
* be syncronised with the child process and the callback will
* only return after the child process has closed.
* Note:
* Stdout and stderr should be Buffers but they are strings unless encoding:null
* for now, nothing we should do, besides wait for joyent to reply
* https://github.com/joyent/node/issues/5833#issuecomment-82189525.
* @method exec
* @param {Object} command
* @param {Object} [opts]
* @param {Boolean} [opts.sync] true to execute sync rather than async
* @param {String} [opts.cwd] Current working directory of the child process
* @param {Object} [opts.env] Environment key-value pairs
* @param {String} [opts.encoding='utf8']
* @param {String} [opts.shell] Shell to execute the command with (Default: '/bin/sh' on UNIX, 'cmd.exe' on Windows, The shell should understand the -c switch on UNIX or /s /c on Windows. On Windows, command line parsing should be compatible with cmd.exe.)
* @param {Number} [opts.timeout=0]
* @param {Number} [opts.maxBuffer=200*1024] Largest amount of data (in bytes) allowed on stdout or stderr - if exceeded child process is killed.
* @param {String} [opts.killSignal='SIGTERM']
* @param {Number} [opts.uid] Sets the user identity of the process.
* @param {Number} [opts.gid] Sets the group identity of the process.
* @param {Function} next
* @param {Error} next.err
* @param {Buffer|String} next.stdout out buffer
* @param {Buffer|String} next.stderr error buffer
* @return {Object} {error, stdout, stderr}
*/
execSync: function (command, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
opts = safeps.prepareExecutableOptions(opts)
opts.sync = true
// Output
if ( opts.output === true && !opts.outputPrefix ) {
opts.stdio = 'inherit'
opts.output = null
}
// Spawn Synchronously
let stdout, error
try {
stdout = require('child_process').execSync(command, opts)
}
catch ( err ) {
error = err
}
// Check result
const result = {error, stdout}
safeps.updateExecutableResult(result, opts)
// Complete
if ( next ) {
next(result.error, result.stdout, result.stderr)
}
else {
return result
}
},
/**
* Wrapper around node's exec command for a cleaner, more robust and powerful API.
* Runs a command in a shell and buffers the output.
* Note:
* Stdout and stderr should be Buffers but they are strings unless encoding:null
* for now, nothing we should do, besides wait for joyent to reply
* https://github.com/joyent/node/issues/5833#issuecomment-82189525.
* @method exec
* @param {Object} command
* @param {Object} [opts]
* @param {Boolean} [opts.sync] true to execute sync rather than async
* @param {String} [opts.cwd] Current working directory of the child process
* @param {Object} [opts.env] Environment key-value pairs
* @param {String} [opts.encoding='utf8']
* @param {String} [opts.shell] Shell to execute the command with (Default: '/bin/sh' on UNIX, 'cmd.exe' on Windows, The shell should understand the -c switch on UNIX or /s /c on Windows. On Windows, command line parsing should be compatible with cmd.exe.)
* @param {Number} [opts.timeout=0]
* @param {Number} [opts.maxBuffer=200*1024] Largest amount of data (in bytes) allowed on stdout or stderr - if exceeded child process is killed.
* @param {String} [opts.killSignal='SIGTERM']
* @param {Number} [opts.uid] Sets the user identity of the process.
* @param {Number} [opts.gid] Sets the group identity of the process.
* @param {Function} next
* @param {Error} next.err
* @param {Buffer|String} next.stdout out buffer
* @param {Buffer|String} next.stderr error buffer
*/
exec: function (command, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
opts = safeps.prepareExecutableOptions(opts)
// Check if we want sync instead
if ( opts.sync ) {
return safeps.execSync(command, opts, next)
}
// Patience
safeps.openProcess(function (closeProcess) {
// Output
if ( opts.output === true && !opts.outputPrefix ) {
opts.stdio = 'inherit'
opts.output = null
}
// Execute command
require('child_process').exec(command, opts, function (error, stdout, stderr) {
// Complete the task
closeProcess()
// Prepare result
const result = {error, stdout, stderr}
safeps.updateExecutableResult(result, opts)
// Complete
return next(result.error, result.stdout, result.stderr)
})
})
// Chain
return safeps
},
/**
* Exec multiple processes in the one method call.
* Launches new processes with the given array of commands.
* Each item in the commands array represents a command parameter
* sent to the safeps.exec method, so each item can be a command line
* string or an array of command line inputs. It is also possible
* to pass a single command string and in this case calling
* execMultiple will be effectively the same as calling safeps.exec.
* @method spawnMultiple
* @param {Array|String} commands
* @param {Object} [opts]
* @param {Boolean} [opts.concurrency=1] Whether to exec processes concurrently.
* @param {String} opts.cwd Current working directory of the child process.
* @param {Array|String} opts.stdio Child's stdio configuration.
* @param {Array} opts.customFds Deprecated File descriptors for the child to use for stdio.
* @param {Object} opts.env Environment key-value pairs.
* @param {Boolean} opts.detached The child will be a process group leader.
* @param {Number} opts.uid Sets the user identity of the process.
* @param {Number} opts.gid Sets the group identity of the process.
* @param {Function} next callback
* @param {Error} next.error
* @param {Array} next.results array of exec results
* @param {Stream} next.results[i].stdout out buffer
* @param {Stream} next.results[i].stderr error buffer
*/
execMultiple: function (commands, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
const results = []
// Be synchronous by default
if ( opts.concurrency == null ) opts.concurrency = 1
// Make sure we send back the arguments
const tasks = new TaskGroup().setConfig({concurrency: opts.concurrency}).done(function (err) {
next(err, results)
})
// Prepare tasks
if ( !typeChecker.isArray(commands) ) {
commands = [commands]
}
// Add tasks
commands.forEach(function (command) {
tasks.addTask(function (complete) {
safeps.exec(command, opts, function (...args) {
const err = args[0] || null
results.push(args)
complete(err)
})
})
})
// Run the tasks
tasks.run()
// Chain
return safeps
},
// =================================
// Paths
/**
* Determine an executable path from
* the passed array of possible file paths.
* Called by getExecPath to find a path for
* a given executable name.
* @private
* @method determineExecPath
* @param {Array} possibleExecPaths string array of file paths
* @param {Object} [opts]
* @param {Boolean} [opts.sync] true to execute sync rather than async
* @param {Function} next
* @param {Error} next.err
* @param {String} next.execPath
*/
determineExecPath: function (possibleExecPaths, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
let execPath = null
// By default don't be sync
if ( opts.sync == null ) opts.sync = false
// Group
const tasks = new TaskGroup({sync: opts.sync}).done(function (err) {
return next(err, execPath)
})
// Handle
possibleExecPaths.forEach(function (possibleExecPath) {
// Check if the path is invalid, if it is, skip it
if ( !possibleExecPath ) return
tasks.addTask(function (complete) {
// Check if we have found the valid exec path earlier, if so, skip
if ( execPath ) return complete()
// Resolve the path as it may be a virtual or relative path
possibleExecPath = pathUtil.resolve(possibleExecPath)
// Check if the executeable exists
safeps.isExecutable(possibleExecPath, opts, function (err, isExecutable) {
if ( err || !isExecutable ) return complete()
execPath = possibleExecPath
return complete()
})
})
})
// Fire the tasks
tasks.run()
// Chain
return safeps
},
/**
* Get the system's environment paths.
* @method getEnvironmentPaths
* @return {Array} string array of file paths
*/
getEnvironmentPaths: function () {
// Fetch system include paths with the correct delimiter for the system
const environmentPaths = process.env.PATH.split(pathUtil.delimiter)
// Return
return environmentPaths
},
/**
* Get the possible paths for
* the passed executable using the
* standard environment paths. Basically,
* get a list of places to look for the
* executable. Only safe for non-Windows
* systems.
* @private
* @method getStandardExecPaths
* @param {String} execName
* @return {Array} string array of file paths
*/
getStandardExecPaths: function (execName) {
// Fetch
let standardExecPaths = [process.cwd()].concat(safeps.getEnvironmentPaths())
// Get the possible exec paths
if ( execName ) {
standardExecPaths = standardExecPaths.map(function (path) {
return pathUtil.join(path, execName)
})
}
// Return
return standardExecPaths
},
/**
* Get the possible paths for
* the passed executable using the
* standard environment paths. Basically,
* get a list of places to look for the
* executable. Makes allowances for Windows
* executables possibly needing an extension
* to ensure execution (.exe, .cmd, .bat).
* @private
* @method getStandardExecPaths
* @param {String} execName
* @return {Array} string array of file paths
*/
getPossibleExecPaths: function (execName) {
let possibleExecPaths
// Fetch available paths
if ( isWindows && execName.indexOf('.') === -1 ) {
// we are for windows add the paths for .exe as well
const standardExecPaths = safeps.getStandardExecPaths(execName)
possibleExecPaths = []
for ( const standardExecPath of standardExecPaths ) {
possibleExecPaths.push(
standardExecPath,
standardExecPath + '.exe',
standardExecPath + '.cmd',
standardExecPath + '.bat'
)
}
}
else {
// we are normal, try the paths
possibleExecPaths = safeps.getStandardExecPaths(execName)
}
// Return
return possibleExecPaths
},
/**
* Cache of executable paths
* @private
* @property execPathCache
*/
execPathCache: {},
/**
* Given an executable name, search and find
* its actual path. Will search the standard
* file paths defined by the environment to
* see if the executable is in any of those paths.
* @method getExecPath
* @param {Object} execName
* @param {Object} [opts]
* @param {Boolean} [opts.cache=true]
* @param {Function} next
* @param {Error} next.err
* @param {String} next.foundPath path to the executable
*/
getExecPath: function (execName, opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Check for absolute path, as we would not be needed and would just currupt the output
if ( execName.substr(0, 1) === '/' || execName.substr(1, 1) === ':' ) {
next(null, execName)
return safeps
}
// Prepare
const execNameCapitalized = execName[0].toUpperCase() + execName.substr(1)
const getExecMethodName = 'get' + execNameCapitalized + 'Path'
// Check for special case
if ( safeps[getExecMethodName] ) {
return safeps[getExecMethodName](opts, next)
}
else {
// Check for cache
if ( opts.cache && safeps.execPathCache[execName] ) {
next(null, safeps.execPathCache[execName])
return safeps
}
// Fetch possible exec paths
const possibleExecPaths = safeps.getPossibleExecPaths(execName)
// Forward onto determineExecPath
// Which will determine which path it is out of the possible paths
safeps.determineExecPath(possibleExecPaths, opts, function (err, execPath) {
if ( err ) {
next(err)
}
else if ( !execPath ) {
err = new Error(`Could not locate the ${execName} executable path`)
next(err)
}
else {
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.execPathCache[execName] = execPath
return next(null, execPath)
}
})
}
// Chain
return safeps
},
/**
* Get home path. Returns the user's home directory.
* Based upon home function from: https://github.com/isaacs/osenv
* @method getHomePath
* @param {Object} [opts]
* @param {Object} [opts.cache=true]
* @param {Function} next
* @param {Error} next.err
* @param {String} next.homePath
*/
getHomePath: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Cached
if ( opts.cache && safeps.cachedHomePath ) {
next(null, safeps.cachedHomePath)
return safeps
}
// Fetch
const homePath = process.env.USERPROFILE || process.env.HOME || null
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.cachedHomePath = homePath
next(null, homePath)
// Chain
return safeps
},
/**
* Path to the evironment's temporary directory.
* Based upon tmpdir function from: https://github.com/isaacs/osenv
* @method getTmpPath
* @param {Object} [opts]
* @param {Object} [opts.cache=true]
* @param {Error} next.err
* @param {String} next.tmpPath
*/
getTmpPath: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Cached
if ( opts.cache && safeps.cachedTmpPath ) {
next(null, safeps.cachedTmpPath)
return safeps
}
// Prepare
const tmpDirName = isWindows ? 'temp' : 'tmp'
// Try the OS environment temp path
let tmpPath = process.env.TMPDIR || process.env.TMP || process.env.TEMP || null
// Fallback
if ( !tmpPath ) {
// Try the user directory temp path
safeps.getHomePath(opts, function (err, homePath) {
if ( err ) return next(err)
tmpPath = pathUtil.resolve(homePath, tmpDirName)
// Fallback
if ( !tmpPath ) {
// Try the system temp path
// @TODO perhaps we should check if we have write access to this path
tmpPath = isWindows
? pathUtil.resolve(process.env.windir || 'C:\\Windows', tmpDirName)
: '/tmp'
}
})
}
// Check if we couldn't find it, we should always be able to find it
if ( !tmpPath ) {
const err = new Error("Wan't able to find a temporary path")
next(err)
}
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.cachedTmpPath = tmpPath
next(null, tmpPath)
// Chain
return safeps
},
/**
* Path to the evironment's GIT directory.
* As 'git' is not always available in the environment path, we should check
* common path locations and if we find one that works, then we should use it.
* @method getGitPath
* @param {Object} [opts]
* @param {Object} [opts.cache=true]
* @param {Error} next.err
* @param {String} next.gitPath
*/
getGitPath: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Cached
if ( opts.cache && safeps.cachedGitPath ) {
next(null, safeps.cachedGitPath)
return safeps
}
// Prepare
const execName = isWindows ? 'git.exe' : 'git'
const possibleExecPaths = []
// Add environment paths
if ( process.env.GIT_PATH ) possibleExecPaths.push(process.env.GIT_PATH)
if ( process.env.GITPATH ) possibleExecPaths.push(process.env.GITPATH)
// Add standard paths
possibleExecPaths.push(...safeps.getStandardExecPaths(execName))
// Add custom paths
if ( isWindows ) {
possibleExecPaths.push(
`/Program Files (x64)/Git/bin/${execName}`,
`/Program Files (x86)/Git/bin/${execName}`,
`/Program Files/Git/bin/${execName}`
)
}
else {
possibleExecPaths.push(
`/usr/local/bin/${execName}`,
`/usr/bin/${execName}`,
`~/bin/${execName}`
)
}
// Determine the right path
safeps.determineExecPath(possibleExecPaths, opts, function (err, execPath) {
if ( err ) {
next(err)
}
else if ( !execPath ) {
err = new Error('Could not locate git binary')
next(err)
}
else {
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.cachedGitPath = execPath
next(null, execPath)
}
})
// Chain
return safeps
},
/**
* Path to the evironment's Node directory.
* As 'node' is not always available in the environment path, we should check
* common path locations and if we find one that works, then we should use it
* @method getNodePath
* @param {Object} [opts]
* @param {Object} [opts.cache=true]
* @param {Error} next.err
* @param {String} next.nodePath
*/
getNodePath: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Cached
if ( opts.cache && safeps.cachedNodePath ) {
next(null, safeps.cachedNodePath)
return safeps
}
// Prepare
const execName = isWindows ? 'node.exe' : 'node'
const possibleExecPaths = []
// Add environment paths
if ( process.env.NODE_PATH ) possibleExecPaths.push(process.env.NODE_PATH)
if ( process.env.NODEPATH ) possibleExecPaths.push(process.env.NODEPATH)
if ( /node(.exe)?$/.test(process.execPath) ) possibleExecPaths.push(process.execPath)
// Add standard paths
possibleExecPaths.push(...safeps.getStandardExecPaths(execName))
// Add custom paths
if ( isWindows ) {
possibleExecPaths.push(
`/Program Files (x64)/nodejs/${execName}`,
`/Program Files (x86)/nodejs/${execName}`,
`/Program Files/nodejs/${execName}`
)
}
else {
possibleExecPaths.push(
`/usr/local/bin/${execName}`,
`/usr/bin/${execName}`,
`~/bin/${execName}` // User and Heroku
)
}
// Determine the right path
safeps.determineExecPath(possibleExecPaths, opts, function (err, execPath) {
if ( err ) {
next(err)
}
else if ( !execPath ) {
err = new Error('Could not locate node binary')
next(err)
}
else {
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.cachedNodePath = execPath
next(null, execPath)
}
})
// Chain
return safeps
},
/**
* Path to the evironment's NPM directory.
* As 'npm' is not always available in the environment path, we should check
* common path locations and if we find one that works, then we should use it
* @method getNpmPath
* @param {Object} [opts]
* @param {Object} [opts.cache=true]
* @param {Error} next.err
* @param {String} next.npmPath
*/
getNpmPath: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// By default read from the cache and write to the cache
if ( opts.cache == null ) opts.cache = true
// Cached
if ( opts.cache && safeps.cachedNpmPath ) {
next(null, safeps.cachedNpmPath)
return safeps
}
// Prepare
const execName = isWindows ? 'npm.cmd' : 'npm'
const possibleExecPaths = []
// Add environment paths
if ( process.env.NPM_PATH ) possibleExecPaths.push(process.env.NPM_PATH)
if ( process.env.NPMPATH ) possibleExecPaths.push(process.env.NPMPATH)
if ( /node(.exe)?$/.test(process.execPath) ) possibleExecPaths.push(process.execPath.replace(/node(.exe)?$/, execName))
// Add standard paths
possibleExecPaths.push(...safeps.getStandardExecPaths(execName))
// Add custom paths
if ( isWindows ) {
possibleExecPaths.push(
`/Program Files (x64)/nodejs/${execName}`,
`/Program Files (x86)/nodejs/${execName}`,
`/Program Files/nodejs/${execName}`
)
}
else {
possibleExecPaths.push(
`/usr/local/bin/${execName}`,
`/usr/bin/${execName}`,
`~/node_modules/.bin/${execName}` // User and Heroku
)
}
// Determine the right path
safeps.determineExecPath(possibleExecPaths, opts, function (err, execPath) {
if ( err ) {
next(err)
}
else if ( !execPath ) {
err = new Error('Could not locate npm binary')
next(err)
}
else {
// Success, write the result to cache and send to our callback
if ( opts.cache ) safeps.cachedNpmPath = execPath
next(null, execPath)
}
})
// Chain
return safeps
},
// =================================
// Special Commands
// @TODO These should be abstracted out into their own packages
/**
* Initialize a git Repository.
* Requires internet access.
* @method initGitRepo
* @param {Object} opts
* @param {String} opts.path path to initiate local repository
* @param {String} [opts.remote='origin']
* @param {String} opts.url url to git remote repository
* @param {String} [opts.branch='master']
* @param {String} opts.log
* @param {String} opts.output
* @param {String} [opts.cwd=process.cwd()] Current working directory of the child process.
* @param {Function} next
* @param {Error} next.err
* @param {Array} next.results array of spawn results
* @param {Stream} next.results[i].stdout out stream
* @param {Stream} next.results[i].stderr error stream
* @param {Number} next.results[i].status node.js exit code
* @param {String} next.results[i].signal unix style signal such as SIGKILL or SIGHUP
*/
initGitRepo: function (opts, next) {
// Extract
[opts, next] = extractOptsAndCallback(opts, next)
// Defaults
if ( !opts.cwd ) opts.cwd = process.cwd()
if ( !opts.remote ) opts.remote = 'origin'
if ( !opts.branch ) opts.branch = 'master'
// Prepare commands
const commands = []
commands.push(['git', 'init'])
if ( opts.url ) {
commands.push(['git', 'remote', 'add', opts.remote, opts.url])
}
commands.push(['git', 'fetch', opts.remote])
commands.push(['git', 'pull', opts.remote, opts.branch])
commands.push(['git', 'submodule', 'init'])
commands.push(['git', 'submodule', 'update', '--recursive'])
// Perform commands
safeps.spawnMultiple(commands, opts, next)
// Chain
return safeps
},
/**
* Pull from a git repository if it already exists
* on the file system else initialize new Git repository.
* Requires internet access.
* @method initOrPullGitRepo
* @param {Object} opts
* @param {String} opts.path path to local repository
* @param {String} [opts.remote='origin']
* @param {String} opts.url url to git remote repository
* @param {String} [opts.branch='master']
* @param {String} opts.log
* @param {String} opts.output
* @param {String} [opts.cwd=process.cwd()] Current working directory of the child process.
* @param {Function} next
* @param {Error} next.err
* @param {Array} next.results array of spawn results
* @param {Stream} next.results[i].stdout out stream
* @param {Stream} next.results[i].stderr error stream
* @param {Number} next.results[i].status node.js exit code
* @param {String} next.results[i].signal unix style signal such as SIGKILL or SIGHUP
*/
initOrPullGitRepo: function (opts, next) {
// Extract
[opts, next] = extractOptsAndCallback(opts, next)
// Defaults
if ( !opts.cwd ) opts.cwd = process.cwd()
if ( !opts.remote ) opts.remote = 'origin'
if ( !opts.branch ) opts.branch = 'master'
// Check if it exists
safefs.ensurePath(opts.cwd, function (err, exists) {
if ( err ) {
next(err)
}
else if ( exists ) {
safeps.spawn(['git', 'pull', opts.remote, opts.branch], opts, function (err, ...result) {
next(err, 'pull', result)
})
}
else {
safeps.initGitRepo(opts, function (err, result) {
next(err, 'init', result)
})
}
})
// Chain
return safeps
},
/**
* Init Node Modules with cross platform support
* supports linux, heroku, osx, windows
* @method initNodeModules
* @param {Object} opts
* @param {Function} next
* @param {Object} next.err
* @param {Stream} next.stdout out stream
* @param {Stream} next.stderr error stream
* @param {Number} next.status node.js exit code
* @param {String} next.signal unix style signal such as SIGKILL or SIGHUP
*/
initNodeModules: function (opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next)
// Defaults
if ( !opts.cwd ) opts.cwd = process.cwd()
if ( opts.args == null ) opts.args = []
if ( opts.force == null ) opts.force = false
// Paths
const packageJsonPath = pathUtil.join(opts.cwd, 'package.json')
const nodeModulesPath = pathUtil.join(opts.cwd, 'node_modules')
// Split this commands into parts
function partTwo () {
// If there is no package.json file, then we can't do anything
safefs.exists(packageJsonPath, function (exists) {
if ( !exists ) return next()
// Prepare command
const command = ['npm', 'install'].concat(opts.args)
// Execute npm install inside the pugin directory
safeps.spawn(command, opts, next)
})
}
function partOne () {
// If we are not forcing, then skip if node_modules already exists
if ( !opts.force ) {
safefs.exists(nodeModulesPath, function (exists) {
if ( exists ) return next()
partTwo()
})
}
else {
partTwo()
}
}
// Run the first part
partOne()
// Chain
return safeps
},
/**
* Spawn Node Modules with cross platform support
* supports linux, heroku, osx, windows
* spawnNodeModule(name:string, args:array, opts:object, next:function)
* Better than https://github.com/mafintosh/npm-execspawn as it uses safeps
* @method spawnNodeModule
* @param {String} name
* @param {Array} args
* @param {Object} opts
* @param {String} opts.name name of node module
* @param {String} [opts.cwd=process.cwd()] Current working directory of the child process.
* @param {Function} next
* @param {Object} next.err
* @param {Stream} next.stdout out stream
* @param {Stream} next.stderr error stream
* @param {Number} next.status node.js exit code
* @param {String} next.signal unix style signal such as SIGKILL or SIGHUP
*/
spawnNodeModule: function (...args) {
// Prepare
const opts = {cwd: process.cwd()}
let next
// Extract options
for ( const arg of args ) {
const type = typeof arg
if ( Array.isArray(arg) ) {
opts.args = arg
}
else if ( type === 'object' ) {
if ( arg.next ) {
next = arg.next
arg.next = null
}
for ( const key of Object.keys(arg) ) {
opts[key] = arg[key]
}
}
else if ( type === 'function' ) {
next = arg
}
else if ( type === 'string' ) {
opts.name = arg
}
}
// Command
let command
if ( opts.name ) {
command = [opts.name].concat(opts.args || [])
opts.name = null
}
else {
command = [].concat(opts.args || [])
}
// Clean up
opts.args = null
// Paths
command[0] = pathUtil.join(opts.cwd, 'node_modules', '.bin', command[0])
// Spawn
safeps.spawn(command, opts, next)
// Chain
return safeps
}
}
// =====================================
// Export
export default safeps