├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── coffee ├── lib │ ├── create-pipes.coffee │ ├── proxy.coffee │ ├── read-pipes.coffee │ └── timeout.coffee └── sync-exec.coffee ├── js ├── lib │ ├── create-pipes.js │ ├── proxy.js │ ├── read-pipes.js │ └── timeout.js └── sync-exec.js ├── package.json └── test ├── example.coffee └── sh ├── err.sh └── out.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.kdev4 2 | *.log* 3 | *.kate-swp 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean compile npm test 2 | 3 | compile: clean npm 4 | node_modules/.bin/coffee -o js/ coffee/ 5 | 6 | npm: 7 | npm install 8 | 9 | clean: 10 | rm -rf js/ 11 | 12 | test: compile 13 | node_modules/.bin/coffee test/example.coffee 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sync-exec 2 | ========= 3 | 4 | An fs.execSync replacement until you get it natively from node 0.12+ 5 | 6 | Upgrading to 0.12.x is usually safe. At that point it will use child_process.execSync. 7 | 8 | You can still force the emulated version passing `{forceEmulated: true}` to the `options` argument. 9 | 10 | 11 | # Advantages 12 | Inspired by [exec-sync](https://www.npmjs.org/package/exec-sync) but comes with a few advantages: 13 | - no libc requirement (no node-gyp compilation) 14 | - no external dependencies 15 | - returns the exit status code 16 | - you can pass [execSync options](http://nodejs.org/api/child_process.html#child_process_child_process_execsync_command_options) 17 | - multiple commands should work pretty safely 18 | 19 | # Installation 20 | [sudo] npm install sync-exec 21 | 22 | # Signature 23 | exec(cmd[, timeout][, options]); 24 | 25 | # Examples 26 | var exec = require('sync-exec'); 27 | 28 | // { stdout: '1\n', 29 | // stderr: '', 30 | // status: 0 } 31 | console.log(exec('echo 1')); 32 | 33 | // You can even pass options, just like for [child_process.exec](http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 34 | console.log(exec('ls -la', {cwd: '/etc'})); 35 | 36 | // Times out after 1 second, throws an error 37 | exec('sleep 3; echo 1', 1000); 38 | 39 | # How it works (if you care) 40 | Your commands STDOUT and STDERR outputs will be channeled to files, also the exit code will be saved. Synchronous file readers will start listening to these files right after. Once outputting is done, values get picked up, tmp files get deleted and values are returned to your code. 41 | -------------------------------------------------------------------------------- /coffee/lib/create-pipes.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | 4 | timeout = require './timeout' 5 | 6 | 7 | # creates tmp files to pipe process info 8 | # 9 | # @return String path to tmp directory 10 | 11 | module.exports = -> 12 | 13 | t_limit = Date.now() + 1000 # 1 second timeout 14 | 15 | tmp_dir = '/tmp' 16 | for name in ['TMPDIR', 'TMP', 'TEMP'] 17 | tmp_dir = dir.replace /\/$/, '' if (dir = process.env[name])? 18 | 19 | until created 20 | try 21 | dir = tmp_dir + '/sync-exec-' + Math.floor Math.random() * 1000000000 22 | fs.mkdir dir 23 | created = true 24 | 25 | timeout t_limit, 'Can not create sync-exec directory' 26 | 27 | # return process-tracking dir name 28 | dir 29 | -------------------------------------------------------------------------------- /coffee/lib/proxy.coffee: -------------------------------------------------------------------------------- 1 | 2 | child_process = require 'child_process' 3 | 4 | 5 | # Use 0.12 native functionality when available (instead of emulated blocking) 6 | # 7 | # @param cmd String command to execute 8 | # @param max_wait Number millisecond timeout value 9 | # @param options Object child_process.execSync options (like: encoding) 10 | # 11 | # @return Object identical to emulated: {stderr, stdout, status} 12 | 13 | module.exports = (cmd, max_wait, options) -> 14 | 15 | options.timeout = max_wait 16 | stdout = stderr = '' 17 | status = 0 18 | 19 | t0 = Date.now() 20 | 21 | orig_write = process.stderr.write 22 | process.stderr.write = -> 23 | try 24 | stdout = child_process.execSync cmd, options 25 | process.stderr.write = orig_write 26 | catch err 27 | process.stderr.write = orig_write 28 | if err.signal is 'SIGTERM' and t0 <= Date.now() - max_wait 29 | throw new Error 'Timeout' 30 | {stdout, stderr, status} = err 31 | 32 | {stdout, stderr, status} 33 | -------------------------------------------------------------------------------- /coffee/lib/read-pipes.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | 4 | timeout = require './timeout' 5 | 6 | 7 | # Read from pipe files until they get closed/deleted 8 | # 9 | # @param dir String path to tmp files 10 | # @param max_wait Number millisecond timeout value 11 | # 12 | # @return Object {stderr, stdout, status} 13 | 14 | module.exports = (dir, max_wait) -> 15 | 16 | t_limit = Date.now() + max_wait 17 | 18 | until read 19 | try 20 | read = true if fs.readFileSync(dir + '/done').length 21 | timeout t_limit, 'Process execution timeout or exit flag read failure' 22 | 23 | until deleted 24 | try 25 | fs.unlinkSync dir + '/done' 26 | deleted = true 27 | timeout t_limit, 'Can not delete exit code file' 28 | 29 | result = {} 30 | for pipe in ['stdout', 'stderr', 'status'] 31 | result[pipe] = fs.readFileSync dir + '/' + pipe, encoding: 'utf-8' 32 | read = true 33 | fs.unlinkSync dir + '/' + pipe 34 | 35 | try 36 | fs.rmdirSync dir 37 | 38 | result.status = Number result.status 39 | 40 | result 41 | -------------------------------------------------------------------------------- /coffee/lib/timeout.coffee: -------------------------------------------------------------------------------- 1 | 2 | # throw error if time limit has been exceeded 3 | 4 | module.exports = (limit, msg) -> 5 | 6 | if Date.now() > limit 7 | throw new Error msg 8 | -------------------------------------------------------------------------------- /coffee/sync-exec.coffee: -------------------------------------------------------------------------------- 1 | 2 | child_process = require 'child_process' 3 | 4 | create_pipes = require './lib/create-pipes' 5 | proxy = require './lib/proxy' 6 | read_pipes = require './lib/read-pipes' 7 | timeout = require './lib/timeout' 8 | 9 | 10 | # Blocking exec 11 | # 12 | # @param cmd String command to execute 13 | # @param max_wait Number millisecond timeout value 14 | # @param options Object execution options (like: encoding) 15 | # 16 | # @return Object {String stderr, String stdout, Number status} 17 | 18 | module.exports = (cmd, max_wait, options) -> 19 | 20 | if max_wait and typeof max_wait is 'object' 21 | [options, max_wait] = [max_wait, null] 22 | 23 | options ?= {} 24 | 25 | unless options.hasOwnProperty 'encoding' 26 | options.encoding = 'utf8' 27 | 28 | unless typeof options is 'object' and options 29 | throw new Error 'options must be an object' 30 | 31 | max_wait ?= options.timeout or options.max_wait or 3600000 # 1hr default 32 | unless not max_wait? or max_wait >= 1 33 | throw new Error '`options.timeout` must be >=1 millisecond' 34 | delete options.max_wait 35 | 36 | # use native child_process.execSync if available (from node v0.12+) 37 | if options.forceEmulation 38 | delete options.forceEmulation 39 | else if child_process.execSync 40 | return proxy cmd, max_wait, options 41 | 42 | delete options.timeout 43 | 44 | dir = create_pipes() 45 | cmd = '((((' + cmd + ' > ' + dir + '/stdout 2> ' + dir + '/stderr ) ' + 46 | '&& echo $? > ' + dir + '/status) || echo $? > ' + dir + '/status) &&' + 47 | ' echo 1 > ' + dir + '/done) || echo 1 > ' + dir + '/done' 48 | child_process.exec cmd, options, -> 49 | 50 | read_pipes dir, max_wait 51 | -------------------------------------------------------------------------------- /js/lib/create-pipes.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | var fs, timeout; 4 | 5 | fs = require('fs'); 6 | 7 | timeout = require('./timeout'); 8 | 9 | module.exports = function() { 10 | var created, dir, i, len, name, ref, t_limit, tmp_dir; 11 | t_limit = Date.now() + 1000; 12 | tmp_dir = '/tmp'; 13 | ref = ['TMPDIR', 'TMP', 'TEMP']; 14 | for (i = 0, len = ref.length; i < len; i++) { 15 | name = ref[i]; 16 | if ((dir = process.env[name]) != null) { 17 | tmp_dir = dir.replace(/\/$/, ''); 18 | } 19 | } 20 | while (!created) { 21 | try { 22 | dir = tmp_dir + '/sync-exec-' + Math.floor(Math.random() * 1000000000); 23 | fs.mkdir(dir); 24 | created = true; 25 | } catch (_error) {} 26 | timeout(t_limit, 'Can not create sync-exec directory'); 27 | } 28 | return dir; 29 | }; 30 | 31 | }).call(this); 32 | -------------------------------------------------------------------------------- /js/lib/proxy.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | var child_process; 4 | 5 | child_process = require('child_process'); 6 | 7 | module.exports = function(cmd, max_wait, options) { 8 | var err, orig_write, status, stderr, stdout, t0; 9 | options.timeout = max_wait; 10 | stdout = stderr = ''; 11 | status = 0; 12 | t0 = Date.now(); 13 | orig_write = process.stderr.write; 14 | process.stderr.write = function() {}; 15 | try { 16 | stdout = child_process.execSync(cmd, options); 17 | process.stderr.write = orig_write; 18 | } catch (_error) { 19 | err = _error; 20 | process.stderr.write = orig_write; 21 | if (err.signal === 'SIGTERM' && t0 <= Date.now() - max_wait) { 22 | throw new Error('Timeout'); 23 | } 24 | stdout = err.stdout, stderr = err.stderr, status = err.status; 25 | } 26 | return { 27 | stdout: stdout, 28 | stderr: stderr, 29 | status: status 30 | }; 31 | }; 32 | 33 | }).call(this); 34 | -------------------------------------------------------------------------------- /js/lib/read-pipes.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | var fs, timeout; 4 | 5 | fs = require('fs'); 6 | 7 | timeout = require('./timeout'); 8 | 9 | module.exports = function(dir, max_wait) { 10 | var deleted, i, len, pipe, read, ref, result, t_limit; 11 | t_limit = Date.now() + max_wait; 12 | while (!read) { 13 | try { 14 | if (fs.readFileSync(dir + '/done').length) { 15 | read = true; 16 | } 17 | } catch (_error) {} 18 | timeout(t_limit, 'Process execution timeout or exit flag read failure'); 19 | } 20 | while (!deleted) { 21 | try { 22 | fs.unlinkSync(dir + '/done'); 23 | deleted = true; 24 | } catch (_error) {} 25 | timeout(t_limit, 'Can not delete exit code file'); 26 | } 27 | result = {}; 28 | ref = ['stdout', 'stderr', 'status']; 29 | for (i = 0, len = ref.length; i < len; i++) { 30 | pipe = ref[i]; 31 | result[pipe] = fs.readFileSync(dir + '/' + pipe, { 32 | encoding: 'utf-8' 33 | }); 34 | read = true; 35 | fs.unlinkSync(dir + '/' + pipe); 36 | } 37 | try { 38 | fs.rmdirSync(dir); 39 | } catch (_error) {} 40 | result.status = Number(result.status); 41 | return result; 42 | }; 43 | 44 | }).call(this); 45 | -------------------------------------------------------------------------------- /js/lib/timeout.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | module.exports = function(limit, msg) { 4 | if (Date.now() > limit) { 5 | throw new Error(msg); 6 | } 7 | }; 8 | 9 | }).call(this); 10 | -------------------------------------------------------------------------------- /js/sync-exec.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | var child_process, create_pipes, proxy, read_pipes, timeout; 4 | 5 | child_process = require('child_process'); 6 | 7 | create_pipes = require('./lib/create-pipes'); 8 | 9 | proxy = require('./lib/proxy'); 10 | 11 | read_pipes = require('./lib/read-pipes'); 12 | 13 | timeout = require('./lib/timeout'); 14 | 15 | module.exports = function(cmd, max_wait, options) { 16 | var dir, ref; 17 | if (max_wait && typeof max_wait === 'object') { 18 | ref = [max_wait, null], options = ref[0], max_wait = ref[1]; 19 | } 20 | if (options == null) { 21 | options = {}; 22 | } 23 | if (!options.hasOwnProperty('encoding')) { 24 | options.encoding = 'utf8'; 25 | } 26 | if (!(typeof options === 'object' && options)) { 27 | throw new Error('options must be an object'); 28 | } 29 | if (max_wait == null) { 30 | max_wait = options.timeout || options.max_wait || 3600000; 31 | } 32 | if (!((max_wait == null) || max_wait >= 1)) { 33 | throw new Error('`options.timeout` must be >=1 millisecond'); 34 | } 35 | delete options.max_wait; 36 | if (options.forceEmulation) { 37 | delete options.forceEmulation; 38 | } else if (child_process.execSync) { 39 | return proxy(cmd, max_wait, options); 40 | } 41 | delete options.timeout; 42 | dir = create_pipes(); 43 | cmd = '((((' + cmd + ' > ' + dir + '/stdout 2> ' + dir + '/stderr ) ' + '&& echo $? > ' + dir + '/status) || echo $? > ' + dir + '/status) &&' + ' echo 1 > ' + dir + '/done) || echo 1 > ' + dir + '/done'; 44 | child_process.exec(cmd, options, function() {}); 45 | return read_pipes(dir, max_wait); 46 | }; 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sync-exec", 3 | "version": "0.6.2", 4 | "description": "Synchronous exec with status code support. Requires no external dependencies, no need for node-gyp compilations etc.", 5 | "main": "js/sync-exec.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/gvarsanyi/sync-exec.git" 12 | }, 13 | "keywords": [ 14 | "exec", 15 | "execSync", 16 | "fs", 17 | "sync", 18 | "synchronous", 19 | "status code", 20 | "status" 21 | ], 22 | "author": "Greg Varsanyi", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/gvarsanyi/sync-exec/issues" 26 | }, 27 | "homepage": "https://github.com/gvarsanyi/sync-exec", 28 | "devDependencies": { 29 | "coffee-script": "^1.9.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/example.coffee: -------------------------------------------------------------------------------- 1 | exec = require '../js/sync-exec' 2 | 3 | 4 | delay_count = 0 5 | delay_check = -> 6 | now = (new Date).getTime() 7 | unless res1 and res2 and t + 10000 <= now < t + 12000 8 | throw new Error 'Timing error' 9 | console.log 'DONE' 10 | 11 | t = (new Date).getTime() 12 | setTimeout delay_check, 10 13 | 14 | process.stdout.write 'Test #1 (takes ~3 seconds) ... ' 15 | # { stdout: '1\n', 16 | # stderr: '', 17 | # status: 0 } 18 | res1 = exec __dirname + '/sh/out.sh', {forceEmulation: true} 19 | unless res1.stdout is '1\n' and res1.stderr is '' and res1.status is 0 20 | throw new Error 'Result #1 error:\n' + JSON.stringify res1, null, 2 21 | console.log 'DONE' 22 | 23 | # { stdout: '2\n', 24 | # stderr: '3\n', 25 | # status: 1 } 26 | process.stdout.write 'Test #2 (takes ~3 seconds) ... ' 27 | res2 = exec __dirname + '/sh/err.sh', {forceEmulation: true} 28 | unless res2.stdout is '2\n' and res2.stderr is '3\n' and res2.status is 1 29 | throw new Error 'Result #2 error:\n' + JSON.stringify res2, null, 2 30 | console.log 'DONE' 31 | 32 | process.stdout.write 'Test #3 (takes ~1 second) ... ' 33 | try 34 | exec __dirname + '/sh/out.sh', 1000, {forceEmulation: true} 35 | failed_to_stop = true 36 | if failed_to_stop 37 | throw new Error 'Failed timeout' 38 | console.log 'DONE' 39 | 40 | process.stdout.write 'Test #4 (takes ~3 second) ... ' 41 | exec './out.sh', {cwd: __dirname + '/sh', forceEmulation: true} 42 | console.log 'DONE' 43 | 44 | process.stdout.write 'Test #5 ... ' # Timeout order test 45 | -------------------------------------------------------------------------------- /test/sh/err.sh: -------------------------------------------------------------------------------- 1 | sleep 3 2 | echo 3 1>&2 3 | echo 2 4 | false 5 | -------------------------------------------------------------------------------- /test/sh/out.sh: -------------------------------------------------------------------------------- 1 | sleep 3 2 | echo 1 3 | --------------------------------------------------------------------------------