Skip to content

Commit a0d7ffb

Browse files
authored
feat: run script with timeout (#8)
1 parent b403585 commit a0d7ffb

File tree

8 files changed

+140
-6
lines changed

8 files changed

+140
-6
lines changed

LICENSE.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
This software is licensed under the MIT License.
22

3-
Copyright (c) 2016 - 2017 node-modules and other contributors
3+
Copyright (c) 2016 - present node-modules and other contributors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ runScript('node -v', { stdio: 'pipe' })
4343
});
4444
```
4545

46+
### run with timeout
47+
48+
Run user script for a maximum of 10 seconds.
49+
50+
```js
51+
const runScript = require('runscript');
52+
53+
runScript('node user-script.js', { stdio: 'pipe' }, { timeout: 10000 })
54+
.then(stdio => {
55+
console.log(stdio);
56+
})
57+
.catch(err => {
58+
console.error(err);
59+
});
60+
```
61+
4662
## License
4763

4864
[MIT](LICENSE.txt)

index.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ const spawn = require('child_process').spawn;
1212
* @param {String} script - full script string, like `git clone https://github.com/node-modules/runscript.git`
1313
* @param {Object} [options] - spawn options
1414
* @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
15+
* @param {Object} [extraOptions] - extra options for running
16+
* - {Number} [extraOptions.timeout] - child process running timeout
1517
* @return {Object} stdio object, will contains stdio.stdout and stdio.stderr buffer.
1618
*/
17-
module.exports = function runScript(script, options) {
19+
module.exports = function runScript(script, options, extraOptions) {
1820
return new Promise((resolve, reject) => {
21+
extraOptions = extraOptions || {};
1922
options = options || {};
2023
options.env = options.env || Object.create(process.env);
2124
options.cwd = options.cwd || process.cwd();
@@ -38,12 +41,16 @@ module.exports = function runScript(script, options) {
3841
}
3942
}
4043

41-
debug('%s %s %s, %j', sh, shFlag, script, options);
44+
debug('%s %s %s, %j, %j', sh, shFlag, script, options, extraOptions);
4245
const proc = spawn(sh, [ shFlag, script ], options);
4346
const stdout = [];
4447
const stderr = [];
48+
let isEnd = false;
49+
let timeoutTimer;
50+
4551
if (proc.stdout) {
4652
proc.stdout.on('data', buf => {
53+
debug('stdout %d bytes', buf.length);
4754
stdout.push(buf);
4855
});
4956
if (options.stdout) {
@@ -52,14 +59,29 @@ module.exports = function runScript(script, options) {
5259
}
5360
if (proc.stderr) {
5461
proc.stderr.on('data', buf => {
62+
debug('stderr %d bytes', buf.length);
5563
stderr.push(buf);
5664
});
5765
if (options.stderr) {
5866
proc.stderr.pipe(options.stderr);
5967
}
6068
}
61-
proc.on('error', reject);
69+
70+
proc.on('error', err => {
71+
debug('proc emit error: %s', err);
72+
if (isEnd) return;
73+
isEnd = true;
74+
clearTimeout(timeoutTimer);
75+
76+
reject(err);
77+
});
78+
6279
proc.on('close', code => {
80+
debug('proc emit close: %s', code);
81+
if (isEnd) return;
82+
isEnd = true;
83+
clearTimeout(timeoutTimer);
84+
6385
const stdio = {
6486
stdout: null,
6587
stderr: null,
@@ -72,10 +94,40 @@ module.exports = function runScript(script, options) {
7294
}
7395
if (code !== 0) {
7496
const err = new Error(`Run "${sh} ${shFlag} ${script}" error, exit code ${code}`);
97+
err.name = 'RunScriptError';
7598
err.stdio = stdio;
7699
return reject(err);
77100
}
78101
return resolve(stdio);
79102
});
103+
104+
proc.on('exit', code => {
105+
debug('proc emit exit: %s', code);
106+
});
107+
108+
if (typeof extraOptions.timeout === 'number' && extraOptions.timeout > 0) {
109+
// start timer
110+
timeoutTimer = setTimeout(() => {
111+
debug('proc run timeout: %dms', extraOptions.timeout);
112+
isEnd = true;
113+
debug('kill child process %s', proc.pid);
114+
proc.kill();
115+
116+
const err = new Error(`Run "${sh} ${shFlag} ${script}" timeout in ${extraOptions.timeout}ms`);
117+
err.name = 'RunScriptTimeoutError';
118+
const stdio = {
119+
stdout: null,
120+
stderr: null,
121+
};
122+
if (stdout.length > 0) {
123+
stdio.stdout = Buffer.concat(stdout);
124+
}
125+
if (stderr.length > 0) {
126+
stdio.stderr = Buffer.concat(stderr);
127+
}
128+
err.stdio = stdio;
129+
return reject(err);
130+
}, extraOptions.timeout);
131+
}
80132
});
81133
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"autod": "*",
2424
"egg-bin": "1",
2525
"egg-ci": "^1.8.0",
26-
"eslint": "3",
27-
"eslint-config-egg": "3",
26+
"eslint": "4",
27+
"eslint-config-egg": "6",
2828
"typescript": "^3.5.1"
2929
},
3030
"homepage": "https://github.com/node-modules/runscript",

test/fixtures/timeout-and-exit.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
console.error('timer start');
4+
setInterval(() => {
5+
console.error('echo every 500ms');
6+
}, 500);
7+
8+
setTimeout(() => {
9+
console.error('exit');
10+
process.exit(0);
11+
}, 1100);

test/fixtures/timeout-stderr.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
console.error('timer start');
4+
setInterval(() => {
5+
console.error('echo every 500ms');
6+
}, 500);

test/fixtures/timeout.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
console.log('timer start');
4+
setInterval(() => {
5+
console.log('echo every 500ms');
6+
}, 500);

test/runscript.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,49 @@ describe('test/runscript.test.js', () => {
2222
return runScript('node -e "process.exit(-1)"')
2323
.catch(err => {
2424
console.log(err);
25+
assert(err.name === 'RunScriptError');
26+
});
27+
});
28+
29+
it('should reject on cmd not exists', () => {
30+
return runScript('node-not-exists -e "process.exit(-1)"', {
31+
shell: true,
32+
stdio: 'pipe',
33+
})
34+
.catch(err => {
35+
console.log(err);
36+
assert(err.name === 'RunScriptError');
37+
});
38+
});
39+
40+
it('should reject on timeout (stdout)', () => {
41+
return runScript(`node ${path.join(__dirname, 'fixtures/timeout.js')}`, {
42+
stdio: 'pipe',
43+
}, { timeout: 1200 })
44+
.catch(err => {
45+
console.log(err);
46+
assert(err.name === 'RunScriptTimeoutError');
47+
assert(err.stdio.stdout.toString() === 'timer start\necho every 500ms\necho every 500ms\n');
48+
});
49+
});
50+
51+
it('should reject on timeout (stderr)', () => {
52+
return runScript(`node ${path.join(__dirname, 'fixtures/timeout-stderr.js')}`, {
53+
stdio: 'pipe',
54+
}, { timeout: 1200 })
55+
.catch(err => {
56+
console.log(err);
57+
assert(err.name === 'RunScriptTimeoutError');
58+
assert(err.stdio.stderr.toString() === 'timer start\necho every 500ms\necho every 500ms\n');
59+
});
60+
});
61+
62+
it('should normal exit before timeout', () => {
63+
return runScript(`node ${path.join(__dirname, 'fixtures/timeout-and-exit.js')}`, {
64+
stdio: 'pipe',
65+
}, { timeout: 1300 })
66+
.then(stdio => {
67+
assert(stdio.stderr.toString() === 'timer start\necho every 500ms\necho every 500ms\nexit\n');
2568
});
2669
});
2770

0 commit comments

Comments
 (0)