diff --git a/package-lock.json b/package-lock.json index 298d798..34ab63b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2466,6 +2466,17 @@ "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + } } }, "exit": { @@ -5156,6 +5167,11 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "lookpath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lookpath/-/lookpath-1.1.0.tgz", + "integrity": "sha512-B9NM7XpVfkyWqfOBI/UW0kVhGw7pJztsduch+1wkbYDi90mYK6/InFul3lG0hYko/VEcVMARVBJ5daFRc5aKCw==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5386,12 +5402,18 @@ "dev": true }, "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "requires": { - "path-key": "^2.0.0" + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + } } }, "nwsapi": { diff --git a/package.json b/package.json index f4cea01..3a7f525 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.js b/src/index.js index eaf2024..82708a6 100755 --- a/src/index.js +++ b/src/index.js @@ -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. * @@ -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' ] }) @@ -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 } diff --git a/test/index.test.js b/test/index.test.js index 8d7ac36..0aae2cc 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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') @@ -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 }, @@ -57,21 +62,24 @@ 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'] @@ -79,4 +87,17 @@ test('run (with args, logs folder exists', async () => { 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() })