Skip to content

Commit

Permalink
fix: add support for looking up commands in the npm run path (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
shazron authored Oct 21, 2020
1 parent 03ddb85 commit 87c1ba9
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 19 deletions.
32 changes: 27 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"unit-tests": "jest --config test/jest.config.js --maxWorkers=2"
},
"keywords": [],
"author": "",
"author": "Adobe Inc.",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.2.0"
"debug": "^4.2.0",
"lookpath": "^1.1.0",
"npm-run-path": "^4.0.1"
},
"devDependencies": {
"@adobe/eslint-config-aio-lib-config": "^1.2.1",
Expand Down
48 changes: 39 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ governing permissions and limitations under the License.
*/

const { fork } = require('child_process')
const { lookpath } = require('lookpath')
const npmRunPath = require('npm-run-path')
const debug = require('debug')('aio-run-detached')
const path = require('path')
const pkg = require(path.join(__dirname, '..', 'package.json'))
const fs = require('fs')
const os = require('os')

const LOGS_FOLDER = 'logs'

/**
* Starts out a log file using the name provided.
*
* @param {string} name the name of the log file
* @returns {object} properties: fd for filedescriptor, filepath for log file path
*/
function startLog (name) {
const filepath = path.resolve(path.join(LOGS_FOLDER, name))
const timestamp = new Date().toISOString()

const fd = fs.openSync(filepath, 'a')
fs.writeSync(fd, `${timestamp} log start${os.EOL}`)
debug(`Writing to logfile ${filepath}`)

return {
fd,
filepath
}
}

/**
* Run the commands specified in a detached process.
*
Expand All @@ -27,24 +50,31 @@ async function run (args = []) {
throw new Error('You must specify at least one argument')
}

fs.accessSync(args[0], fs.constants.X_OK)
// add the node_modules/.bin folder to the path
process.env.PATH = npmRunPath()
// lookpath looks for the command in the path, and checks whether it is executable
const commandPath = await lookpath(args[0])
if (!commandPath) {
throw new Error(`Command "${args[0]}" was not found in the path, or is not executable.`)
} else {
debug(`Command "${args[0]}" found at ${commandPath}`)
}

if (!fs.existsSync(LOGS_FOLDER)) {
fs.mkdirSync(LOGS_FOLDER)
}

const outFile = path.join(LOGS_FOLDER, `${args[0]}.out.log`)
const errFile = path.join(LOGS_FOLDER, `${args[0]}.err.log`)
debug(`Writing stdout to ${outFile}, stderr to ${errFile}`)
const outFile = startLog(`${args[0]}.out.log`)
const errFile = startLog(`${args[0]}.err.log`)

debug(`Running command detached: ${JSON.stringify(args)}`)
const child = fork(args[0], args.slice(1), {
const child = fork(commandPath, args.slice(1), {
detached: true,
windowsHide: true,
stdio: [
'ignore',
fs.openSync(outFile, 'a'),
fs.openSync(errFile, 'a'),
outFile.fd,
errFile.fd,
'ipc'
]
})
Expand All @@ -58,8 +88,8 @@ async function run (args = []) {
bin: Object.keys(pkg.bin)[0],
args,
logs: {
stdout: outFile,
stderr: errFile
stdout: outFile.filepath,
stderr: errFile.filepath
},
pid: child.pid
}
Expand Down
27 changes: 24 additions & 3 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ const index = require('../src/index')

jest.mock('fs')
jest.mock('child_process')
jest.mock('lookpath')

const { fork } = require('child_process')
const { lookpath } = require('lookpath')
const fs = require('fs')
const path = require('path')

Expand Down Expand Up @@ -30,19 +33,21 @@ test('run (with args, process.send available)', async () => {
unref: jest.fn()
}
fork.mockReturnValueOnce(forkMockReturn)
lookpath.mockReturnValueOnce('my/path')

const args = ['command', 'arg1']
process.send = jest.fn()
await index.run(args)

expect(forkMockReturn.unref).toHaveBeenCalled()
expect(lookpath).toHaveBeenCalled()
expect(process.send).toHaveBeenCalledWith({
data: {
args,
bin: 'aio-run-detached',
logs: {
stdout: path.join('logs', `${args[0]}.out.log`),
stderr: path.join('logs', `${args[0]}.err.log`)
stdout: expect.stringContaining(path.join('logs', `${args[0]}.out.log`)),
stderr: expect.stringContaining(path.join('logs', `${args[0]}.err.log`))
},
pid
},
Expand All @@ -57,26 +62,42 @@ test('run (with args, process.send not available)', async () => {
unref: jest.fn()
}
fork.mockReturnValueOnce(forkMockReturn)
lookpath.mockReturnValueOnce('my/path')

const args = ['command', 'arg1']
process.send = undefined
await index.run(args)

expect(forkMockReturn.unref).toHaveBeenCalled()
expect(lookpath).toHaveBeenCalled()
})

test('run (with args, logs folder exists', async () => {
test('run (with args, logs folder exists)', async () => {
const pid = 789
const forkMockReturn = {
pid,
unref: jest.fn()
}
fork.mockReturnValueOnce(forkMockReturn)
lookpath.mockReturnValueOnce('my/path')
fs.existsSync.mockReturnValueOnce(true)

const args = ['command', 'arg1']
process.send = undefined
await index.run(args)

expect(forkMockReturn.unref).toHaveBeenCalled()
expect(lookpath).toHaveBeenCalled()
})

test('run (with args, command not found or not executable)', async () => {
lookpath.mockReturnValueOnce(undefined)

const args = ['command', 'arg1']
process.send = undefined
await expect(index.run(args))
.rejects.toEqual(new Error(`Command "${args[0]}" was not found in the path, or is not executable.`))

expect(fork).not.toHaveBeenCalled()
expect(lookpath).toHaveBeenCalled()
})

0 comments on commit 87c1ba9

Please sign in to comment.