├── .gitignore ├── index.js ├── lib ├── win32 │ ├── nssm.exe │ ├── nssm.js │ ├── index.js │ └── service.js ├── linux │ ├── template.systemd │ ├── template.upstart │ ├── env.js │ ├── template.sysvinit │ ├── backends │ │ ├── systemd.js │ │ ├── upstart.js │ │ └── sysvinit.js │ ├── which.js │ └── index.js ├── helpers.js ├── darwin │ ├── template.plist │ └── index.js └── builder.js ├── package.json ├── bin └── satan ├── README.md └── test └── test_options.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | sandbox 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/' + process.platform); 2 | -------------------------------------------------------------------------------- /lib/win32/nssm.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomas/satan/HEAD/lib/win32/nssm.exe -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "satan", 3 | "version": "0.5.0", 4 | "description": "Bring daemons to life or destroy them forever. OSX/Linux/Windows.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test" 8 | }, 9 | "bin": { 10 | "satan": "./bin/satan" 11 | }, 12 | "author": "Tomas Pollak ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "async": "^0.9.0", 16 | "launchd": "0.0.5", 17 | "linus": "0.0.6", 18 | "minstache": "^1.2.0", 19 | "optimist": "^0.6.1", 20 | "whenever": "0.0.4", 21 | "which": "^1.0.5" 22 | }, 23 | "devDependencies": { 24 | "should": "", 25 | "sinon": "" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/linux/template.systemd: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description={{desc}} 3 | After={{sd_start_after}} 4 | 5 | [Service] 6 | ExecStart={{bin}} 7 | {{#path}} 8 | WorkingDirectory={{path}} 9 | {{/path}} 10 | Restart={{sd_restart}} 11 | 12 | {{#sd_respawn_wait}} 13 | RestartSec={{sd_respawn_wait}} 14 | {{/sd_respawn_wait}} 15 | 16 | {{#user}} 17 | User={{user}} 18 | {{/user}} 19 | 20 | {{#has_env}} 21 | Environment={{#env}}"{{key}}={{value}}" {{/env}} 22 | {{/has_env}} 23 | 24 | {{#sd_kill_mode}} 25 | KillMode={{sd_kill_mode}} 26 | {{/sd_kill_mode}} 27 | 28 | {{#kill_timeout}} 29 | TimeoutStopSec={{kill_timeout}} 30 | {{/kill_timeout}} 31 | 32 | {{#kill_signal}} 33 | KillSignal={{kill_signal}} 34 | {{/kill_signal}} 35 | 36 | [Install] 37 | WantedBy=multi-user.target 38 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'); 3 | 4 | exports.ensure_dir = function(dir, cb) { 5 | fs.exists(dir, function(exists) { 6 | if (exists) return cb(); 7 | 8 | fs.mkdir(dir, cb); 9 | }); 10 | } 11 | 12 | exports.copy = function(source, target, cb) { 13 | 14 | fs.stat(target, function(err, stat) { 15 | if (stat && stat.isDirectory()) 16 | target = path.join(target, path.basename(source)); 17 | 18 | var is = fs.createReadStream(source), 19 | os = fs.createWriteStream(target), 20 | out = 0; 21 | 22 | var done = function(err) { 23 | if (out++ > 0) return; 24 | cb(err); 25 | }; 26 | 27 | is.on('end', done); 28 | is.on('error', done); 29 | os.on('error', done); 30 | 31 | is.pipe(os); 32 | }) 33 | 34 | }; 35 | 36 | // don't really know why I'm not using rename 37 | exports.move = function(source, target, cb) { 38 | exports.copy(source, target, function(err) { 39 | if (err) return cb(err); 40 | 41 | fs.unlink(source, cb); 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /lib/linux/template.upstart: -------------------------------------------------------------------------------- 1 | description "{{name}}" 2 | author "{{author}}" 3 | 4 | start on {{up_start_on}} 5 | stop on {{up_stop_on}} 6 | kill signal {{kill_signal}} 7 | kill timeout {{kill_timeout}} 8 | 9 | respawn 10 | 11 | {{#user}} 12 | setuid {{user}} 13 | {{/user}} 14 | 15 | {{#reload_signal}} 16 | reload signal {{reload_signal}} 17 | {{/reload_signal}} 18 | 19 | {{#up_respawn_wait}} 20 | post-stop exec sleep {{up_respawn_wait}} 21 | {{/up_respawn_wait}} 22 | 23 | {{#up_pre_start_script}} 24 | pre-start script 25 | {{!up_pre_start_script}} 26 | end script 27 | {{/up_pre_start_script}} 28 | 29 | {{#up_post_start_script}} 30 | post-start script 31 | {{!up_post_start_script}} 32 | end script 33 | {{/up_post_start_script}} 34 | 35 | {{#up_pre_stop_script}} 36 | pre-stop script 37 | {{!up_pre_stop_script}} 38 | end script 39 | {{/up_pre_stop_script}} 40 | 41 | {{#up_post_stop_script}} 42 | post-stop script 43 | {{!up_post_stop_script}} 44 | end script 45 | {{/up_post_stop_script}} 46 | 47 | {{#has_env}} 48 | {{#env}} 49 | env {{key}}={{value}}{{/env}} 50 | {{/has_env}} 51 | 52 | script 53 | {{#path}} 54 | chdir {{path}} 55 | {{/path}} 56 | exec {{bin}} 57 | end script 58 | -------------------------------------------------------------------------------- /lib/darwin/template.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | KeepAlive 6 | 7 | SuccessfulExit 8 | 9 | 10 | Label 11 | {{key}} 12 | ProgramArguments 13 | 14 | {{bin}} 15 | 16 | {{#path}} 17 | WorkingDirectory 18 | {{path}} 19 | {{/path}} 20 | RunAtLoad 21 | 22 | ExitTimeOut 23 | {{kill_timeout}} 24 | {{#user}} 25 | UserName 26 | {{user}} 27 | {{/user}} 28 | {{#start_interval}} 29 | StartInterval 30 | {{start_interval}} 31 | {{/start_interval}} 32 | {{#ld_respawn_wait}} 33 | ThrottleInterval 34 | {{ld_respawn_wait}} 35 | {{/ld_respawn_wait}} 36 | {{#watch_paths}} 37 | WatchPaths 38 | 39 | {{#watch_paths_array}} 40 | {{path}} 41 | {{/watch_paths_array}} 42 | 43 | {{/watch_paths}} 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/linux/env.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | which = require('./which'), 3 | distro_name = require('linus').name; 4 | 5 | var res; 6 | 7 | // returns downcased, sanitized name of distro, 8 | // e.g. 'elementary_os' instead of Elementary OS 9 | function get_name(cb) { 10 | distro_name(function(err, name) { 11 | if (err) return cb(err); 12 | 13 | // log('Distro detected: ' + name); 14 | var lowercase = name.toLowerCase().replace(/ /g, '_'); 15 | 16 | cb(null, lowercase); 17 | }) 18 | } 19 | 20 | function get_init_location() { 21 | var bin = ''; 22 | try { 23 | bin = fs.readlinkSync('/sbin/init'); 24 | } catch(e) { 25 | // console.log('No /sbin/init found. Is this possible?') 26 | } 27 | return bin; 28 | } 29 | 30 | function detect_init_system() { 31 | // allow overriding init system 32 | if (process.env['INIT_SYSTEM']) 33 | return process.env['INIT_SYSTEM']; 34 | 35 | // var sbin_init = get_init_location(); 36 | 37 | if (fs.existsSync('/run/systemd/system')) { 38 | return 'systemd'; 39 | } else if (which('initctl')) { // sbin_init.match('upstart') 40 | return 'upstart'; 41 | } else { 42 | return 'sysvinit'; 43 | } 44 | } 45 | 46 | exports.detect = function(cb) { 47 | if (res) 48 | return cb(null, res); 49 | 50 | get_name(function(err, name) { 51 | if (err) return cb(err); 52 | 53 | res = {}; 54 | res.name = name; 55 | res.init_system = detect_init_system(); 56 | cb(null, res); 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /lib/linux/template.sysvinit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # {{key}} init script 4 | # 5 | # chkconfig: 2345 20 80 6 | # description: starts {{name}} 7 | # 8 | 9 | ### BEGIN INIT INFO 10 | # Provides: {{key}} 11 | # Required-Start: $local_fs $remote_fs $syslog 12 | # Required-Stop: $local_fs $remote_fs $syslog 13 | # Default-Start: 2 3 4 5 14 | # Default-Stop: 0 1 6 15 | # Short-Description: {{name}} 16 | # Description: {{desc}} 17 | ### END INIT INFO 18 | 19 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 20 | DAEMON="{{bin}}" 21 | DAEMON_OPTS="{{args}}" 22 | WORKING_DIR="{{path}}" 23 | NAME="{{key}}" 24 | DESC="{{name}}" 25 | 26 | {{#user}} 27 | USER="{{user}}" 28 | {{/user}} 29 | 30 | {{^user}} 31 | USER="root" 32 | {{/user}} 33 | 34 | # Include defaults if available 35 | if [ -f /etc/default/{{key}} ]; then 36 | . /etc/default/{{key}} 37 | fi 38 | 39 | test -x $DAEMON || exit 0 40 | 41 | set -e 42 | . /lib/lsb/init-functions 43 | 44 | start() { 45 | start-stop-daemon --start --quiet -d $WORKING_DIR -c $USER --pidfile /var/run/$NAME.pid --exec $DAEMON --background --make-pidfile -- $DAEMON_OPTS || true 46 | } 47 | 48 | stop() { 49 | start-stop-daemon --stop --signal QUIT --quiet --pidfile /var/run/$NAME.pid || true 50 | } 51 | 52 | case "$1" in 53 | start) 54 | echo -n "Starting $DESC: " 55 | start 56 | echo "$NAME." 57 | ;; 58 | 59 | stop) 60 | echo -n "Stopping $DESC: " 61 | stop 62 | echo "$NAME." 63 | ;; 64 | 65 | restart|force-reload) 66 | echo -n "Restarting $DESC: " 67 | stop 68 | sleep 1 69 | start 70 | echo "$NAME." 71 | ;; 72 | 73 | status) 74 | status_of_proc -p /var/run/$NAME.pid "$DAEMON" "$NAME" && exit 0 || exit $? 75 | ;; 76 | 77 | *) 78 | echo "Usage: $NAME {start|stop|restart|force-reload|status}" >&2 79 | exit 1 80 | ;; 81 | esac 82 | 83 | exit 0 84 | -------------------------------------------------------------------------------- /lib/linux/backends/systemd.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, 2 | join = require('path').join; 3 | 4 | var scripts_path = '/etc/systemd/system'; 5 | 6 | var commands = { 7 | status : 'systemctl status $1.service', 8 | load : 'systemctl enable $1.service', 9 | unload : 'systemctl disable $1.service', 10 | reload : 'systemctl --system daemon-reload', // reloads all configs 11 | start : 'systemctl start $1.service', 12 | stop : 'systemctl stop $1.service' 13 | } 14 | 15 | var run = function(cmd, key, cb) { 16 | var command = commands[cmd].replace('$1', key); 17 | // console.log(command); 18 | exec(command, cb); 19 | } 20 | 21 | exports.name = 'systemd'; 22 | 23 | exports.get_path = function(distro_name, key) { 24 | return join(scripts_path, key + '.service'); // /etc/systemd/foo-service 25 | } 26 | 27 | exports.exists = function(key, cb) { 28 | run('status', key, function(err, out) { 29 | var loaded = out && !out.toString().match('Loaded: not-found'); 30 | cb(null, loaded); 31 | }); 32 | } 33 | 34 | exports.status = function(key, cb) { 35 | run('status', key, function(err, stdout, stderr) { 36 | // code is usually not zero, so err is always present 37 | // if (err) return cb(err); 38 | 39 | process.stdout.write(stdout); 40 | }) 41 | } 42 | 43 | exports.start = function(key, cb) { 44 | run('start', key, cb); 45 | } 46 | 47 | exports.stop = function(key, cb) { 48 | run('stop', key, cb); 49 | } 50 | 51 | exports.load = function(key, cb) { 52 | run('load', key, cb); 53 | } 54 | 55 | exports.unload = function(key, cb) { 56 | run('stop', key, function(err) { 57 | 58 | run('unload', key, cb); 59 | }) 60 | } 61 | 62 | exports.reload = function(key, cb) { 63 | run('unload', key, function(err) { 64 | // if (err) return cb(err); 65 | 66 | run('load', key, cb); 67 | }); 68 | } 69 | 70 | exports.reload_all = function(cb) { 71 | run('reload', null, cb); 72 | } 73 | -------------------------------------------------------------------------------- /lib/win32/nssm.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | helpers = require('../helpers'), 3 | async = require('async'), 4 | exec = require('child_process').exec; 5 | 6 | var nssm = path.join(__dirname, 'nssm.exe'); 7 | 8 | /* 9 | 10 | nssm install Jenkins %PROGRAMFILES%\Java\jre7\bin\java.exe 11 | nssm set Jenkins AppParameters -jar slave.jar -jnlpUrl https://jenkins/computer/%COMPUTERNAME%/slave-agent.jnlp -secret redacted 12 | nssm set Jenkins AppDirectory C:\Jenkins 13 | nssm set Jenkins AppStdout C:\Jenkins\jenkins.log 14 | nssm set Jenkins AppStderr C:\Jenkins\jenkins.log 15 | nssm set Jenkins AppStopMethodSkip 6 16 | nssm set Jenkins AppStopMethodConsole 1000 17 | nssm set Jenkins AppThrottle 5000 18 | nssm start Jenkins 19 | 20 | */ 21 | 22 | exports.install = function(opts, cb) { 23 | var key = opts.key, 24 | bin = opts.bin; 25 | 26 | if (!key || !bin) 27 | return cb(new Error('Both key and bin are required.')) 28 | 29 | var bin_name = path.basename(bin), 30 | bin_path = path.dirname(bin); 31 | 32 | var daemon_path = opts.daemon_path || bin_path, 33 | daemon_bin = path.join(daemon_path, opts.daemon_name || 'nssm.exe'); 34 | 35 | var fx = [ 36 | function(cb) { helpers.ensure_dir(daemon_path, cb) }, 37 | function(cb) { helpers.copy(nssm, daemon_bin, cb) } 38 | ]; 39 | 40 | var cmds = [ 41 | 'install ' + key + ' ' + bin, 42 | 'set ' + key + ' DisplayName ' + '"' + (opts.name || key) + '"' 43 | ]; 44 | 45 | if (opts.args) 46 | cmds.push('set ' + key + ' AppParameters ' + opts.args); 47 | 48 | if (opts.path) 49 | cmds.push('set ' + key + ' AppDirectory "' + opts.path + '"'); 50 | 51 | if (opts.desc) 52 | cmds.push('set ' + key + ' Description "' + opts.desc + '"'); 53 | 54 | cmds.forEach(function(c) { 55 | fx.push(function(cb) { 56 | exec(daemon_bin + ' ' + c, cb); 57 | }) 58 | }) 59 | 60 | async.series(fx, cb); 61 | } 62 | 63 | exports.uninstall = function(key, cb) { 64 | exec('"' + nssm + '" remove ' + key + ' confirm', cb); 65 | } 66 | -------------------------------------------------------------------------------- /lib/linux/backends/upstart.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, 2 | join = require('path').join; 3 | 4 | var scripts_path = '/etc/init'; 5 | 6 | var commands = { 7 | status : 'initctl --system status $1', 8 | // load : 'initctl start $1', 9 | // unload : 'initctl stop $1', 10 | start : 'initctl --system start $1', 11 | stop : 'initctl --system stop $1', 12 | reload : 'initctl --system reload-configuration' 13 | } 14 | 15 | var run = function(cmd, key, cb) { 16 | var command = commands[cmd].replace('$1', key); 17 | exec(command, cb); 18 | } 19 | 20 | exports.name = 'upstart'; 21 | 22 | exports.get_path = function(distro_name, key) { 23 | return join(scripts_path, key + '.conf'); // /etc/init/foo-service.conf 24 | } 25 | 26 | exports.exists = function(key, cb) { 27 | run('status', key, function(err, out) { 28 | if (err) { 29 | if (err.message.match(/unrecognized service|unknown job/i)) 30 | return cb(null, false); 31 | else 32 | return cb(err); 33 | } 34 | 35 | return cb(null, true); 36 | }); 37 | } 38 | 39 | exports.status = function(key, cb) { 40 | run('status', key, function(err, stdout) { 41 | // if (err) return cb(err); 42 | 43 | process.stdout.write(stdout); 44 | }) 45 | } 46 | 47 | exports.start = function(key, cb) { 48 | run('start', key, cb); 49 | } 50 | 51 | exports.stop = function(key, cb) { 52 | run('stop', key, cb); 53 | } 54 | 55 | // upstart detects changes via inotify 56 | // so there's no need to run any command 57 | exports.load = function(key, cb) { 58 | cb(); 59 | } 60 | 61 | // upstart detects changes via inotify, so we'll only call stop() 62 | // to ensure the daemon is not running 63 | exports.unload = function(key, cb) { 64 | run('stop', key, function(err) { 65 | // Unknown instance is returned when not running. 66 | if (err && !err.message.match(/unknown instance/i)) 67 | return cb(err); 68 | 69 | cb(); 70 | }); 71 | } 72 | 73 | exports.reload = function(key, cb) { 74 | run('reload', key, cb); 75 | } 76 | 77 | exports.reload_all = exports.reload; 78 | -------------------------------------------------------------------------------- /lib/linux/which.js: -------------------------------------------------------------------------------- 1 | // the which command from shelljs.org 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | // Cross-platform method for splitting environment PATH variables 7 | function splitPath(p) { 8 | for (i=1;i<2;i++) {} 9 | 10 | if (!p) 11 | return []; 12 | 13 | if (process.platform === 'win32') 14 | return p.split(';'); 15 | else 16 | return p.split(':'); 17 | } 18 | 19 | function checkPath(path) { 20 | return fs.existsSync(path) && fs.statSync(path).isDirectory() == false; 21 | } 22 | 23 | //@ 24 | //@ ### which(command) 25 | //@ 26 | //@ Examples: 27 | //@ 28 | //@ ```javascript 29 | //@ var nodeExec = which('node'); 30 | //@ ``` 31 | //@ 32 | //@ Searches for `command` in the system's PATH. On Windows looks for `.exe`, `.cmd`, and `.bat` extensions. 33 | //@ Returns string containing the absolute path to the command. 34 | function _which(cmd) { 35 | if (!cmd) 36 | new Error('must specify command'); 37 | 38 | var pathEnv = process.env.path || process.env.Path || process.env.PATH, 39 | pathArray = splitPath(pathEnv), 40 | where = null; 41 | 42 | // No relative/absolute paths provided? 43 | if (cmd.search(/\//) === -1) { 44 | // Search for command in PATH 45 | pathArray.forEach(function(dir) { 46 | if (where) 47 | return; // already found it 48 | 49 | var attempt = path.resolve(dir + '/' + cmd); 50 | if (checkPath(attempt)) { 51 | where = attempt; 52 | return; 53 | } 54 | 55 | if (process.platform === 'win32') { 56 | var baseAttempt = attempt; 57 | attempt = baseAttempt + '.exe'; 58 | if (checkPath(attempt)) { 59 | where = attempt; 60 | return; 61 | } 62 | attempt = baseAttempt + '.cmd'; 63 | if (checkPath(attempt)) { 64 | where = attempt; 65 | return; 66 | } 67 | attempt = baseAttempt + '.bat'; 68 | if (checkPath(attempt)) { 69 | where = attempt; 70 | return; 71 | } 72 | } // if 'win' 73 | }); 74 | } 75 | 76 | // Command not found anywhere? 77 | if (!checkPath(cmd) && !where) 78 | return null; 79 | 80 | where = where || path.resolve(cmd); 81 | return where; 82 | } 83 | 84 | module.exports = _which; -------------------------------------------------------------------------------- /lib/darwin/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | join = require('path').join, 3 | launchd = require('launchd'), 4 | exec = require('child_process').exec, 5 | builder = require('./../builder'); 6 | 7 | var plist_template = 'template.plist', 8 | template_path = join(__dirname, plist_template); 9 | 10 | ////////////////////////////////////////////////////// 11 | // helpers 12 | 13 | var debug = false; 14 | 15 | var log = function(str) { 16 | if (debug) log(str); 17 | } 18 | 19 | var build = function(opts, cb) { 20 | builder.generate(opts, template_path, '.plist', cb); 21 | } 22 | 23 | ////////////////////////////////////////////////////// 24 | // the actual hooks 25 | ////////////////////////////////////////////////////// 26 | 27 | exports.exists = function(key, cb) { 28 | launchd.exists(key, cb); 29 | } 30 | 31 | exports.status = function(key, cb) { 32 | exec('sudo launchctl list | grep ' + key, function(err, stdout) { 33 | // if (err) return cb(err); 34 | process.stdout.write(stdout); 35 | }); 36 | } 37 | 38 | exports.test_create = function(opts, cb) { 39 | build(opts, function(err, temp_script) { 40 | if (err) return cb(err); 41 | 42 | var res = fs.readFileSync(temp_script); 43 | fs.unlinkSync(temp_script); 44 | cb(null, res); 45 | }) 46 | } 47 | 48 | exports.create = function(opts, cb) { 49 | build(opts, function(err, temp_script) { 50 | if (err) return cb(err); 51 | 52 | launchd.install(temp_script, function(err) { 53 | try { fs.unlinkSync(temp_script) } catch(e) { /* boo-hoo */ } 54 | 55 | cb(err); 56 | }); 57 | }); 58 | } 59 | 60 | exports.start = function(key, cb) { 61 | launchd.start(key, cb); 62 | } 63 | 64 | exports.stop = function(key, cb) { 65 | launchd.stop(key, cb); 66 | } 67 | 68 | exports.ensure_stopped = exports.stop; 69 | 70 | exports.destroy = function(key, cb) { 71 | launchd.remove(key, cb); 72 | } 73 | 74 | exports.ensure_created = function(opts, cb) { 75 | this.ensure_destroyed(opts.key, function(err) { 76 | if (err) return cb(err); 77 | 78 | exports.create(opts, cb); 79 | }); 80 | } 81 | 82 | exports.ensure_destroyed = function(key, cb) { 83 | launchd.remove(key, function(err) { 84 | if (err && !err.toString().match('Not found:')) 85 | return cb(err); 86 | 87 | cb() 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /bin/satan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var satan = require('..'), 4 | command = process.argv[2], 5 | name = process.argv[3], 6 | dirname = require('path').dirname, 7 | resolve = require('path').resolve, 8 | argv = require('optimist').argv; 9 | 10 | if (!name) 11 | return usage(); 12 | 13 | if (process.getuid && process.getuid() != 0) { 14 | console.log('Almighty satan requires root. What did you expect?'); 15 | process.exit(1); 16 | } 17 | 18 | function usage() { 19 | console.log('Usage: satan [command] [daemon-name]'); 20 | console.log('Where command is one of: exists, status, create, ensure_created, start, stop, ensure_stopped, destroy, ensure_destroyed.') 21 | console.log('\nExamples:\n\tsatan create my-service --bin /path/to/bin --args "some args" --user someone'); 22 | console.log('\tsatan start my-service'); 23 | console.log('\tsatan status my-service'); 24 | console.log('\tsatan stop my-service'); 25 | console.log('\tsatan destroy my-service'); 26 | process.exit(1); 27 | } 28 | 29 | function get_opts() { 30 | var obj = { 31 | key: name, 32 | bin: resolve(argv.bin), 33 | path: argv.path ? resolve(argv.path) : resolve(dirname(argv.bin)), 34 | args: argv.args, 35 | user: argv.user, 36 | name: argv.name, 37 | desc: argv.desc 38 | }; 39 | 40 | if (process.platform == 'win32') 41 | obj.daemon_path = argv.daemon_path 42 | 43 | return obj; 44 | } 45 | 46 | function done(err, res) { 47 | if (err) 48 | console.log('Error: ' + err.message); 49 | else 50 | console.log('Success!'); 51 | } 52 | 53 | switch (command) { 54 | case 'exists': 55 | return satan.exists(name, function(err, bool) { 56 | console.log(err ? 'Error: ' + err.message : 'Exists: ' + bool); 57 | }); 58 | break; 59 | 60 | case 'create': 61 | return satan.create(get_opts(), done); 62 | break; 63 | 64 | case 'ensure_created': 65 | return satan.ensure_created(get_opts(), done); 66 | break; 67 | 68 | case 'start': 69 | return satan.start(name, done); 70 | break; 71 | 72 | case 'stop': 73 | return satan.stop(name, done); 74 | break; 75 | 76 | case 'ensure_stopped': 77 | return satan.ensure_stopped(name, done); 78 | break; 79 | 80 | case 'status': 81 | return satan.status(name, function() { }); 82 | break; 83 | 84 | case 'remove': 85 | case 'delete': 86 | case 'destroy': 87 | return satan.destroy(name, done); 88 | break; 89 | 90 | case 'ensure_destroyed': 91 | return satan.ensure_destroyed(name, done); 92 | break; 93 | 94 | default: 95 | usage(); 96 | } 97 | -------------------------------------------------------------------------------- /lib/linux/backends/sysvinit.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, 2 | detect = require('../env').detect, 3 | join = require('path').join; 4 | 5 | var distros = { 6 | debian: { 7 | status : '/etc/init.d/$1 status', 8 | load : 'update-rc.d $1 defaults', 9 | unload : 'update-rc.d -f $1 remove', 10 | path : '/etc/init.d' 11 | }, 12 | redhat: { 13 | status : 'chkconfig $1 status', // TODO: check this, not really sure 14 | load : 'chkconfig $1 on', 15 | unload : 'chkconfig $1 off', 16 | path : '/etc/rc.d/init.d' 17 | }, 18 | suse: { 19 | status : 'chkconfig --status $1', // TODO: check this, not really sure 20 | load : 'chkconfig --add $1', 21 | unload : 'chkconfig --del $1', 22 | path : '/etc/init.d' 23 | } 24 | }; 25 | 26 | distros.fedora = distros.redhat; 27 | distros.ubuntu = distros.debian; 28 | distros.linuxmint = distros.debian; 29 | distros.elementary_os = distros.debian; 30 | 31 | var run = function(cmd, key, cb) { 32 | detect(function(err, distro) { 33 | if (err) return cb(err); 34 | 35 | var distro_conf = distros[distro.name]; 36 | if (!distro_conf) 37 | return cb(new Error('Unknown distro: ' + distro.name)); 38 | 39 | if (cmd == 'start' || cmd == 'stop') 40 | var command = distro_conf.path + '/' + key + ' ' + cmd; // /etc/init.d/foo-service start 41 | else 42 | var command = distro_conf[cmd].replace('$1', key); 43 | 44 | exec(command, cb); 45 | }) 46 | } 47 | 48 | exports.name = 'sysvinit'; 49 | 50 | exports.get_path = function(distro_name, key) { 51 | var base = distros[distro_name] ? distros[distro_name].path : '/etc/init.d'; // assume default 52 | return join(base, key); // /etc/init.d/foo-service 53 | } 54 | 55 | exports.exists = function(key, cb) { 56 | run('status', key, function(err, out) { 57 | if (err) { 58 | if (err.message.match(/not found/i)) // TODO: improve this 59 | return cb(null, false); 60 | else 61 | return cb(err); 62 | } 63 | 64 | return cb(null, true); 65 | }); 66 | } 67 | 68 | exports.status = function(key, cb) { 69 | run('status', key, function(err, stdout) { 70 | // if (err) return cb(err); 71 | 72 | process.stdout.write(stdout); 73 | }) 74 | } 75 | 76 | exports.load = function(key, cb) { 77 | run('load', key, cb); 78 | } 79 | 80 | exports.unload = function(key, cb) { 81 | run('unload', key, cb); 82 | } 83 | 84 | exports.start = function(key, cb) { 85 | run('start', key, cb); 86 | } 87 | 88 | exports.stop = function(key, cb) { 89 | run('stop', key, cb); 90 | } 91 | 92 | exports.reload = function(key, cb) { 93 | run('unload', key, function(err) { 94 | // if (err) return cb(err); 95 | 96 | run('load', key, cb); 97 | }); 98 | } 99 | 100 | exports.reload_all = function(cb) { 101 | cb(); // TODO: find a way around this 102 | } 103 | -------------------------------------------------------------------------------- /lib/win32/index.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, 2 | basename = require('path').basename, 3 | nssm = require('./nssm'), 4 | service = require('./service'); 5 | 6 | /* 7 | var get_pid_of_process = function(exe, cb) { 8 | var cmd = 'tasklist /nh /fi "imagename eq ' + exe + '"'; 9 | 10 | exec(cmd, function(err, stdout) { 11 | if (err) return cb(err); 12 | 13 | if (stdout.toString().indexOf(exe) === -1) 14 | return cb(); //service not running 15 | 16 | var cols = stdout.split(/\s+/), 17 | pid = cols[2]; 18 | 19 | cb(null, parseInt(pid)); 20 | }); 21 | } 22 | */ 23 | 24 | var kill_process = function(process_name, cb) { 25 | exec('taskkill /f /im ' + process_name, cb); 26 | } 27 | 28 | var stop_service_process = function(key, cb) { 29 | service.bin_path(key, function(err, bin_path) { 30 | if (err) return cb(err); 31 | 32 | var exe = basename(bin_path); 33 | kill_process(exe, cb); 34 | }) 35 | } 36 | 37 | var delete_service = function(key, cb) { 38 | // use nssm for deleting services, that way we remove 39 | // anything that may have been inserted by them 40 | return nssm.uninstall(key, cb); 41 | 42 | service.delete(key, cb); 43 | } 44 | 45 | exports.exists = function(key, cb) { 46 | service.exists(key, cb); 47 | } 48 | 49 | exports.status = function(key, cb) { 50 | service.status(key, cb); 51 | } 52 | 53 | exports.test_create = function(opts, cb) { 54 | cb(new Error('Not supported, since Windows does not use init scripts.')); 55 | } 56 | 57 | exports.create = function(opts, cb) { 58 | if (opts.daemon_path === null || opts.daemon_path === false) 59 | service.create(opts, cb); 60 | else 61 | nssm.install(opts, cb) 62 | } 63 | 64 | exports.ensure_created = function(opts, cb) { 65 | exports.ensure_destroyed(opts.key, function(err) { 66 | if (err) return cb(err); 67 | 68 | exports.create(opts, cb); 69 | }) 70 | } 71 | 72 | exports.start = function(key, cb) { 73 | service.start(key, cb); 74 | } 75 | 76 | exports.stop = function(key, cb) { 77 | service.stop(key, cb); 78 | } 79 | 80 | // tries to stop. if unstoppable, kills the process 81 | // if service doesn't exist, returns error 82 | exports.ensure_stopped = function(key, cb) { 83 | exports.stop(key, function(err, stdout) { 84 | if (err && err.code != 1052) { // 1052 code means couldn't stop 85 | 86 | // only return error if error isn't 'NOT_RUNNING' 87 | return err.code == 'NOT_RUNNING' ? cb() : cb(err); 88 | } 89 | 90 | stop_service_process(key, cb); 91 | }); 92 | } 93 | 94 | exports.destroy = function(key, cb) { 95 | this.exists(key, function(err, exists) { 96 | if (err || !exists) return cb(new Error('Service not found.')); 97 | 98 | exports.ensure_stopped(key, function(err) { 99 | // if (err) return cb(err); // still? 100 | 101 | delete_service(key, cb); 102 | }) 103 | }); 104 | } 105 | 106 | exports.ensure_destroyed = function(key, cb) { 107 | this.destroy(key, function(err) { 108 | if (err && !err.message.match(/not found/i)) 109 | return cb(err); 110 | 111 | cb(); 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'), 3 | join = require('path').join, 4 | template = require('minstache'); 5 | 6 | var temp_path = function(filename) { 7 | var tempdir = process.env.TMPDIR || '/tmp'; 8 | return join(tempdir, filename); 9 | } 10 | 11 | var check = function(opts) { 12 | var data = {}; 13 | 14 | data.key = opts.key; 15 | data.name = opts.name || opts.key; 16 | data.path = opts.path; 17 | data.bin = opts.bin; 18 | 19 | if (!opts.key || !opts.bin) 20 | throw new Error('Required options: key, bin'); 21 | 22 | // execution options 23 | data.user = opts.user; 24 | data.args = opts.args || ''; 25 | 26 | if (opts.env) { 27 | data.has_env = true; 28 | data.env = []; 29 | // map { foo: 'bar'} to [{ key: 'foo', value: 'bar' }] 30 | for (var key in opts.env) { 31 | var obj = { key: key, value: opts.env[key] }; 32 | data.env.push(obj); 33 | } 34 | } 35 | 36 | data.desc = opts.desc || 'The ' + data.name + ' daemon.'; 37 | data.author = opts.author || 'A nice guy.'; 38 | 39 | // advanced options 40 | 41 | // the number of seconds that the daemon should wait 42 | // before restarting the process when exiting. 43 | // NOTE: on OSX, this only affects when the process exits before 10 seconds have passed. 44 | data.up_respawn_wait = opts.up_respawn_wait || opts.respawn_wait; 45 | data.sd_respawn_wait = opts.sd_respawn_wait || opts.respawn_wait; 46 | data.ld_respawn_wait = opts.ld_respawn_wait || opts.respawn_wait; 47 | 48 | // seconds to wait after TERM, before sending a definitive KILL signal 49 | data.kill_timeout = opts.kill_timeout || 20; 50 | 51 | // start interval for LaunchDaemon 52 | data.start_interval = opts.interval; 53 | 54 | // Upstart/systemd: what signal to send when reload or stop is called. 55 | // by default the SIGHUP signal is sent. 56 | data.reload_signal = opts.reload_signal; // only upstart 57 | data.kill_signal = opts.kill_signal || 'QUIT'; 58 | 59 | // upstart start options 60 | data.up_start_on = opts.up_start_on || 'startup'; 61 | data.up_stop_on = opts.up_stop_on || 'runlevel [016]'; 62 | 63 | // systemd start/monitor options 64 | data.sd_start_after = opts.sd_start_after || 'network.target'; 65 | data.sd_restart = opts.sd_restart || 'always'; 66 | data.sd_kill_mode = opts.sd_kill_mode; // can be 'process', 'mixed', 'none' or 'control-group' (default) 67 | 68 | // custom script support for Upstart 69 | ['pre_start', 'post_start', 'pre_stop', 'post_stop'].forEach(function(stage) { 70 | var script_name = 'up_' + stage + '_script'; 71 | 72 | if (opts[script_name]) 73 | data[script_name] = opts[script_name]; 74 | }) 75 | 76 | // LaunchDaemon: paths to watch for modifications. triggers restart if something changes. 77 | if (opts.watch_paths) { 78 | data.watch_paths = true; 79 | data.watch_paths_array = opts.watch_paths.map(function(dir) { return { path: dir } }); 80 | } 81 | 82 | return data; 83 | } 84 | 85 | exports.generate = function(data, template_path, extension, cb) { 86 | 87 | try { 88 | var data = check(data); 89 | } catch(e) { 90 | return cb(e); 91 | } 92 | 93 | var dest = join(temp_path(data.key + (extension || ''))); 94 | 95 | fs.readFile(template_path, function(err, source) { 96 | if (err) return cb(err); 97 | 98 | var result = template(source.toString(), data).replace(/^\s*\n/gm, '\n'); 99 | 100 | if (result === source.toString() || result.match('{{')) 101 | return cb(new Error('Unable to replace variables in plist template!')) 102 | 103 | fs.writeFile(dest, result, function(err) { 104 | if (err) return cb(err); 105 | 106 | cb(null, dest); 107 | }); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /lib/win32/service.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | exec = require('child_process').exec; 3 | 4 | var log = function(str) { 5 | console.log(str); 6 | } 7 | 8 | var run = function(cmd, cb) { 9 | exec(cmd, cb) 10 | } 11 | 12 | var errors = { 13 | '3' : 'Path not found', 14 | '5' : 'Access denied', 15 | '1060' : 'Does not exist', 16 | '1062' : 'Has not been started', 17 | '1073' : 'Service already exists', 18 | '1639' : 'Argument error' 19 | } 20 | 21 | exports.exists = function(key, cb) { 22 | run('sc qc ' + key, function(err, out) { 23 | if (err) return cb(err); 24 | 25 | var exists = out && !out.toString().match('1060'); 26 | cb(null, exists); 27 | }); 28 | } 29 | 30 | exports.status = function(key, cb) { 31 | run('sc qc ' + key, function(err, stdout) { 32 | // if (err) return cb(err); 33 | 34 | process.stdout.write(stdout); 35 | }) 36 | } 37 | 38 | exports.bin_path = function(key, cb) { 39 | run('sc qc ' + key, function(err, out) { 40 | if (err) return cb(err); 41 | 42 | var match = out.toString().match(/([^\s]+).exe/); 43 | // log('Bin path: ' + match[0]); 44 | 45 | if (match) 46 | return cb(null, match[0].trim()); 47 | 48 | cb(new Error('Unable to get bin path.')) 49 | }) 50 | } 51 | 52 | exports.create = function(opts, cb) { 53 | var key = opts.key, 54 | bin = opts.bin, 55 | name = opts.name, 56 | desc = opts.desc; 57 | 58 | var cmds = []; 59 | 60 | var cmd = 'sc create ' + key + ' binPath= "' + bin + '"'; 61 | cmd += ' start= auto'; // options: boot, system, auto, demand, disabled 62 | cmd += ' error= normal'; // notify if failed. with 'ignore' no message is shown. 63 | cmd += ' DisplayName= "' + name + '"'; 64 | 65 | cmds.push(cmd); 66 | 67 | // restart after 10 seconds (or custom) if failed, and also 68 | // reset restart counter to 0 after 30 seconds of uptime 69 | var restart_wait = opts.restart_wait || 10000; 70 | var reset_after = opts.reset_after || 30; 71 | cmd = 'sc failure ' + key + ' reset= ' + reset_after + ' actions= restart/' + restart_wait; 72 | 73 | cmds.push(cmd); 74 | 75 | // if description is present, set it. 76 | if (desc) { 77 | cmds.push('sc description ' + key + ' "' + desc + '"'); 78 | } 79 | 80 | var codes = Object.keys(errors), 81 | regex = new RegExp(' (' + codes.join('|') + '):'); 82 | 83 | var fx = cmds.map(function(cmd) { 84 | return function(cb) { 85 | // log(cmd); 86 | 87 | run(cmd, function(err, stdout, stderr) { 88 | var failed = !!stdout.toString().match(regex); 89 | if (!failed) 90 | return cb(); 91 | 92 | var e = (err && err.message.trim() != 'Command failed:') ? err : new Error(stdout.toString().trim()); 93 | cb(e); 94 | }) 95 | } 96 | }) 97 | 98 | async.series(fx, cb); 99 | } 100 | 101 | exports.start = function(key, cb) { 102 | run('sc start ' + key, function(err, out) { 103 | if (out.match('PID') || out.match('PROCESS-ID')) 104 | cb(); 105 | else 106 | cb(new Error(out.trim())) 107 | }); 108 | } 109 | 110 | exports.stop = function(key, cb) { 111 | run('sc stop ' + key, function(err, out) { 112 | if (out.toString().match('1062')) { // not running 113 | var err = new Error('Not running.'); 114 | err.code = 'NOT_RUNNING'; // used by satan.ensure_stopped() to check 115 | } else if (out.toString().match('1060')) { 116 | cb(new Error('Service not found.')); 117 | } 118 | cb(err); 119 | }); 120 | } 121 | 122 | exports.delete = function(key, cb) { 123 | run('sc delete ' + key, function(e, out) { 124 | if (out && out.toString().match('1060')) 125 | return cb(new Error(out.toString())) 126 | 127 | cb(); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /lib/linux/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | join = require('path').join, 3 | resolve = require('path').resolve, 4 | exec = require('child_process').exec, 5 | whenever = require('whenever'), 6 | env = require('./env'), 7 | move = require('../helpers').move, 8 | builder = require('./../builder'); 9 | 10 | var backends = whenever('*', __dirname + '/backends'); 11 | 12 | ////////////////////////////////////////////////////// 13 | // helpers 14 | 15 | var debug = !!process.env.DEBUG, 16 | log = debug ? console.log : function() { }; 17 | 18 | //////////////////////////////////////////////// 19 | // init commands 20 | 21 | var get_backend = function(distro) { 22 | return backends[distro.init_system]; 23 | } 24 | 25 | var call_backend = function(command, key, cb) { 26 | env.detect(function(err, distro) { 27 | if (err) return cb(err); 28 | 29 | var backend = get_backend(distro); 30 | log('Running ' + command + ' in ' + backend.name); 31 | backend[command](key, cb); 32 | }) 33 | } 34 | 35 | var build = function(opts, cb) { 36 | fs.stat(opts.bin, function(err, stat) { 37 | if (err) return cb(err); 38 | 39 | env.detect(function(err, distro) { 40 | if (err) return cb(err); 41 | 42 | if (distro.init_system == 'systemd') { 43 | if (opts.args) 44 | return cb(new Error('systemd does not support args!')) 45 | else if (opts.bin[0] != '/') 46 | return cb(new Error('bin needs to be an absolute path, not ' + opts.bin)) 47 | } 48 | 49 | var template_path = __dirname + '/template.' + distro.init_system, 50 | extension = distro.init_system == 'upstart' ? '.conf' : null; 51 | 52 | log('Generating init script from ' + template_path); 53 | console.log(opts) 54 | builder.generate(opts, template_path, extension, function(err, file) { 55 | cb(err, file, distro); 56 | }); 57 | 58 | }) 59 | }) 60 | } 61 | 62 | var build_install = function(opts, cb) { 63 | build(opts, function(err, temp_script, distro) { 64 | if (err) return cb(err); 65 | 66 | log('Setting up init script.'); 67 | install_script(distro, opts.key, temp_script, function(err) { 68 | if (err) return cb(err); 69 | 70 | call_backend('reload', opts.key, cb); 71 | }); 72 | }); 73 | } 74 | 75 | var unload_remove = function(key, cb) { 76 | log('Unloading init script: ' + key); 77 | call_backend('unload', key, function(err) { 78 | if (err) return cb(err); 79 | 80 | remove_script(key, function(err) { 81 | // if (err) return cb(err); 82 | 83 | call_backend('reload_all', cb); 84 | }); 85 | }); 86 | } 87 | 88 | var install_script = function(distro, key, file, cb) { 89 | var backend = get_backend(distro), 90 | target = backend.get_path(distro.name, key); 91 | 92 | fs.exists(target, function(exists) { 93 | if (exists) return cb(new Error('Script already exists: ' + target)); 94 | 95 | log('Copying script to ' + target); 96 | move(file, target, function(err) { 97 | if (err) return cb(err); 98 | 99 | // if sysvinit, make sure file is executable 100 | if (distro.init_system == 'sysvinit') 101 | fs.chmodSync(target, 0755); 102 | 103 | cb() 104 | }); 105 | }) 106 | } 107 | 108 | var remove_script = function(key, cb) { 109 | env.detect(function(err, distro) { 110 | if (err) return cb(err); 111 | 112 | var backend = get_backend(distro), 113 | target = backend.get_path(distro.name, key); 114 | 115 | fs.exists(target, function(exists) { 116 | if (!exists) return cb(); 117 | 118 | log('Removing file: ' + target); 119 | fs.unlink(target, cb); 120 | }) 121 | }) 122 | } 123 | 124 | exports.exists = function(key, cb) { 125 | call_backend('exists', key, cb); 126 | } 127 | 128 | exports.status = function(key, cb) { 129 | call_backend('status', key, cb); 130 | } 131 | 132 | exports.test_create = function(opts, cb) { 133 | build(opts, function(err, file) { 134 | if (err) return cb(err); 135 | 136 | var res = fs.readFileSync(file); 137 | fs.unlinkSync(file); 138 | cb(null, res); 139 | }); 140 | } 141 | 142 | exports.create = function(opts, cb) { 143 | call_backend('exists', opts.key, function(err, exists) { 144 | if (err || exists) return cb(err || new Error('Already exists: ' + opts.key)); 145 | 146 | build_install(opts, cb); 147 | }); 148 | } 149 | 150 | exports.start = function(key, cb) { 151 | call_backend('start', key, cb); 152 | } 153 | 154 | exports.stop = function(key, cb) { 155 | call_backend('stop', key, cb); 156 | } 157 | 158 | exports.ensure_stopped = exports.stop; 159 | 160 | // checks if exists, if yes, tries to unload, returs error if failed 161 | exports.destroy = function(key, cb) { 162 | this.exists(key, function(err, exists) { 163 | if (err || !exists) 164 | return cb(new Error('Service not found.')); 165 | 166 | unload_remove(key, cb); 167 | }) 168 | } 169 | 170 | exports.ensure_created = function(opts, cb) { 171 | exports.ensure_destroyed(opts.key, function(err) { 172 | if (err) return cb(err); 173 | 174 | build_install(opts, cb); 175 | }) 176 | } 177 | 178 | exports.ensure_destroyed = function(key, cb) { 179 | this.destroy(key, function(err) { 180 | if (!err || err.message.match(/not found/i)) 181 | return cb(); 182 | 183 | cb(err); 184 | }) 185 | } 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Satan 2 | ===== 3 | 4 | Make daemons out of your processes at will. Then destroy them forever. 5 | 6 | Example 7 | ------- 8 | 9 | var satan = require('satan'); 10 | 11 | var opts = { 12 | bin : '/usr/bin/node', // full path to bin 13 | user : 'deploy', // user to run the service as 14 | args : 'app.js -p 8001', // command line arguments 15 | path : '/home/deploy/apps/secret-project', // working directory 16 | key : 'secret-project' // for identifying the service 17 | } 18 | 19 | satan.create(opts, function(err) { 20 | if (err) 21 | return console.log('Failed to create daemon.') 22 | 23 | console.log('Success! Launching process...'); 24 | satan.start(opts.key, function(err) { 25 | console.log(err || 'Running.') 26 | }); 27 | }) 28 | 29 | You can create a daemon out of any process -- not necessarily a Node.js app. Just point the bin to where it should. 30 | 31 | var opts = { 32 | bin : '/path/to/my/daemon.py', 33 | key : 'com.example.daemon', 34 | user : 'nobody' 35 | } 36 | 37 | satan.create(opts, function(err) { 38 | console.log(err || 'Let me do your bidding, master.'); 39 | }); 40 | 41 | All arguments are optional, except for `bin` and `key`. In Linux, you can also pass both `name` and `desc` options, which will be inserted in the generated init script. Satan supports **sysinitv**, **upstart** and **systemd** and automatically detects which one your Linux uses. 42 | 43 | var opts = { 44 | bin : 'puma -p 8000', 45 | path : '/home/tomas/apps/awesome', 46 | key : 'awesome-app', 47 | name : 'Awesome App', 48 | desc : 'My awesome app.' 49 | } 50 | 51 | satan.ensure_created(opts, function(err) { 52 | // in this case, satan will not return an error if the service already exists. 53 | }); 54 | 55 | Windows 56 | ------- 57 | 58 | To daemonize your processes in Windows, Satan uses a nitfy tool called `nssm` (i.e. the 'non sucking service manager') to spawn and keep your process up and running. So, when calling `create`, Satan basically makes a copy of the `nssm.exe` binary, and creates a new system service that points to it. 59 | 60 | By default the `nssm.exe` binary is copied to the same path as your bin, but you can use a custom location for the nssm.exe binary by passing a `daemon_path` option, like this: 61 | 62 | var opts = { 63 | bin : 'npm start', 64 | path : 'C:\\Users\\tomas\\apps\\static-http' 65 | key : 'StaticHTTP', 66 | name : 'Static HTTP Server', 67 | desc : 'Serves static files from my Public folder to local network users.', 68 | daemon_path : path.join(process.env.WINDIR, 'system32') 69 | } 70 | 71 | If you also want to use a custom name for .exe, just include a `daemon_name` option. 72 | 73 | opts.daemon_name = 'awesome-daemon.exe'; 74 | 75 | Now, if you already _have_ a Windows Service executable, and don't need to use the `nssm.exe` method, set the `daemon_path` option to `null` or `false` when creating the daemon. 76 | 77 | var opts = { 78 | daemon_path : null, 79 | bin : 'C:\\IBN\\Profiles\\QRDX\\corpsvc.exe', 80 | key : 'CorporateService', 81 | name : 'Very Corporate Service', 82 | desc : 'Reminds users that they are part of a very corporate environment.' 83 | } 84 | 85 | satan.create(opts, cb); 86 | 87 | API 88 | --- 89 | 90 | ## satan.create(opts, cb) 91 | 92 | Creates a new daemon. Returns an error if it already exists. 93 | 94 | ## satan.ensure_created(opts, cb) 95 | 96 | Creates a new daemon. Does not return an error if it exists. 97 | 98 | ## satan.start(daemon_key, cb) 99 | 100 | Stats a daemon. Uses the daemon's key for identifying it. Callsback an error if it failed. 101 | 102 | ## satan.stop(daemon_key, cb) 103 | 104 | Stops a daemon. Uses the daemon's key for identifying it. Callsback an error if it failed. 105 | 106 | ## satan.destroy(daemon_key, cb) 107 | 108 | Destroys an existing daemon. Returns an error if not found. 109 | 110 | ## satan.ensure_destroyed(daemon_key, cb) 111 | 112 | Destroys an existing daemon. Does not return an error if not found. 113 | 114 | Options 115 | ------- 116 | 117 | On creation: 118 | 119 | - `key`: Identifier for the service. OSX users should use the `com.example.app` notation. 120 | - `bin`: Absolute or relative path to the executable. If relative, make sure to include the `path` option. 121 | - `args`: Additional arguments to pass to the bin. Optional. Not an array, just a string. 122 | - `path`: Absolute path to set as the current working directory before calling the bin. Somewhat optional (read the `bin` part above). 123 | - `name`: More descriptive name for your daemon, to be included in the init script or the Windows Services list. 124 | - `desc`: Even more descriptive text for your daemon. Not necessary, but makes it look nicer. 125 | 126 | Windows-only options: 127 | 128 | - daemon_path: Custom path to use for the `nssm.exe` binary when setting up your daemon. If `null` or `false`, Satan assumes your bin can run as a Windows Service and will not copy any additional binaries. 129 | - daemon_name: Custom name to use for the `nssm.exe` executable. Optional. 130 | 131 | There are a slew of other options available for fine-tuning OSX and Linux daemons. Take a look at the [lib/builder.js script](https://github.com/tomas/satan/blob/master/lib/builder.js) for more satanic tweaks. 132 | 133 | Final part 134 | ---------- 135 | 136 | Written by Tomás Pollak. 137 | (c) Fork, Ltd. MIT License. 138 | -------------------------------------------------------------------------------- /test/test_options.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'); 3 | distro = require('../lib/linux/distro'), 4 | satan = require('..'); 5 | 6 | describe('generating', function() { 7 | 8 | var prev_platform, 9 | detect_stub; 10 | 11 | function set_platform(os, init_system) { 12 | prev_platform = process.platform; 13 | process.platform = os; 14 | 15 | if (init_system) { 16 | detect_stub = sinon.stub(distro, 'detect', function(cb) { 17 | var obj = { name: 'Ubuntu', init_system: init_system }; 18 | cb(null, obj); 19 | }) 20 | } 21 | } 22 | 23 | function reset_platform() { 24 | process.platform = prev_platform; 25 | prev_platform = null; 26 | 27 | detect_stub.restore(); 28 | detect_stub = null; 29 | } 30 | 31 | describe('linux upstart', function() { 32 | 33 | before(function() { 34 | set_platform('linux', 'upstart'); 35 | }) 36 | 37 | after(reset_platform); 38 | 39 | it('key is required', function(done) { 40 | var opts = { bin: '/bin/true' }; 41 | 42 | satan.test_create(opts, function(err) { 43 | err.should.be.a.Error; 44 | err.message.should.containEql('Required options'); 45 | done(); 46 | }) 47 | }) 48 | 49 | it('defaults are set to their defaults', function(done) { 50 | var opts = { key: 'test-foo', bin: '/bin/true' }; 51 | 52 | satan.test_create(opts, function(err, res) { 53 | res.toString().should.containEql('start on startup'); 54 | res.toString().should.containEql('stop on runlevel [016]'); 55 | res.toString().should.containEql('kill signal QUIT'); 56 | res.toString().should.containEql('kill timeout 20'); 57 | done() 58 | }) 59 | }) 60 | 61 | it('optional paths are optional', function(done) { 62 | var opts = { key: 'test-foo', bin: '/bin/true' }; 63 | 64 | satan.test_create(opts, function(err, res) { 65 | res.toString().should.not.containEql('user'); 66 | res.toString().should.not.containEql('chdir'); 67 | res.toString().should.not.containEql('env'); 68 | res.toString().should.not.containEql('post-stop exec sleep'); // respawn_wait 69 | res.toString().should.not.containEql('reload signal'); 70 | 71 | // pre/post start/stop scripts 72 | res.toString().should.not.containEql('pre-start'); 73 | res.toString().should.not.containEql('post-start'); 74 | res.toString().should.not.containEql('pre-stop'); 75 | res.toString().should.not.containEql('post-stop'); 76 | done() 77 | }) 78 | }) 79 | 80 | it('path is set when passed', function(done) { 81 | var opts = { key: 'test-foo', bin: '/bin/true', path: '/tmp' }; 82 | 83 | satan.test_create(opts, function(err, res) { 84 | res.toString().should.containEql('chdir /tmp'); 85 | done() 86 | }) 87 | }) 88 | 89 | it('respawn wait too', function(done) { 90 | var opts = { key: 'test-foo', bin: '/bin/true', respawn_wait: 9 }; 91 | 92 | satan.test_create(opts, function(err, res) { 93 | res.toString().should.containEql('post-stop exec sleep 9'); 94 | done() 95 | }) 96 | }) 97 | 98 | it('kill timeout is set', function(done) { 99 | var opts = { key: 'test-foo', bin: '/bin/true', kill_timeout: 12 }; 100 | 101 | satan.test_create(opts, function(err, res) { 102 | res.toString().should.containEql('kill timeout 12'); 103 | done() 104 | }) 105 | }) 106 | 107 | }) 108 | 109 | describe('linux systemd', function() { 110 | 111 | before(function() { 112 | set_platform('linux', 'systemd'); 113 | }) 114 | 115 | after(function() { 116 | reset_platform(); 117 | }) 118 | 119 | it('key is required', function(done) { 120 | var opts = { bin: '/bin/true' }; 121 | 122 | satan.test_create(opts, function(err) { 123 | err.should.be.a.Error; 124 | err.message.should.containEql('Required options'); 125 | done(); 126 | }) 127 | }) 128 | 129 | it('defaults are set to their defaults', function(done) { 130 | var opts = { key: 'test-foo', bin: '/bin/true' }; 131 | 132 | satan.test_create(opts, function(err, res) { 133 | res.toString().should.containEql('After=network.target'); 134 | res.toString().should.containEql('WantedBy=multi-user.target'); 135 | res.toString().should.containEql('Restart=always'); 136 | res.toString().should.containEql('TimeoutStopSec=20'); // kill timeout 137 | res.toString().should.containEql('KillSignal=QUIT'); // kill timeout 138 | done() 139 | }) 140 | }) 141 | 142 | it('optional paths are optional', function(done) { 143 | var opts = { key: 'test-foo', bin: '/bin/true' }; 144 | 145 | satan.test_create(opts, function(err, res) { 146 | res.toString().should.not.containEql('User='); 147 | res.toString().should.not.containEql('WorkingDirectory='); 148 | res.toString().should.not.containEql('Environment='); 149 | res.toString().should.not.containEql('RestartSec='); // respawn_wait 150 | done() 151 | }) 152 | }) 153 | 154 | it('path is set when passed', function(done) { 155 | var opts = { key: 'test-foo', bin: '/bin/true', path: '/tmp' }; 156 | 157 | satan.test_create(opts, function(err, res) { 158 | res.toString().should.containEql('WorkingDirectory=/tmp'); 159 | done() 160 | }) 161 | }) 162 | 163 | it('respawn wait too', function(done) { 164 | var opts = { key: 'test-foo', bin: '/bin/true', respawn_wait: 9 }; 165 | 166 | satan.test_create(opts, function(err, res) { 167 | res.toString().should.containEql('RestartSec=9'); 168 | done() 169 | }) 170 | }) 171 | 172 | it('kill timeout is set', function(done) { 173 | var opts = { key: 'test-foo', bin: '/bin/true', kill_timeout: 12 }; 174 | 175 | satan.test_create(opts, function(err, res) { 176 | res.toString().should.containEql('TimeoutStopSec=12'); 177 | done() 178 | }) 179 | }) 180 | }) 181 | }) 182 | --------------------------------------------------------------------------------