UNPKG

@mh-cbon/launchd-simple-api

Version:

Simple API to manage services via macosx launchd

421 lines (360 loc) 12.2 kB
var spawn = require('child_process').spawn; var fs = require('fs-extra') var path = require('path') var split = require('split') var async = require('async') var through2 = require('through2') var yasudo = require('@mh-cbon/c-yasudo') var sudoFs = require('@mh-cbon/sudo-fs') var pkg = require('./package.json') var debug = require('debug')(pkg.name); var dStream = require('debug-stream')(debug) var LaunchdSimpleApi = function (version) { var that = this; var elevationEnabled = false; var pwd = false; this.enableElevation = function (p) { if (p===false){ elevationEnabled = false; pwd = false; return; } elevationEnabled = true; pwd = p; } var getFs = function () { return elevationEnabled ? sudoFs : fs; } var spawnAChild = function (bin, args, opts) { if (elevationEnabled) { debug('sudo %s %s', bin, args.join(' ')) opts = opts || {}; if (pwd) opts.password = pwd; return yasudo(bin, args, opts); } debug('%s %s', bin, args.join(' ')) return spawn(bin, args, opts); } this.list = function (opts, then) { var results = {} var c = spawnAChild('launchctl', ['list'], {stdio: 'pipe'}) c.stdout .pipe(split()) .pipe(through2(function (chunk, enc, cb) { var d = chunk.toString(); if (d.match(/^PID\s+status\s+label/)) { // skip headers } else if (d.match(/^(-|[0-9\-]+)\s+(-|[0-9\-]+)\s+.+/)) { //1419 - 0x7f97d040ea20.anonymous.launchctl d = d.match(/^(-|[0-9\-]+)\s+(-|[0-9\-]+)\s+(.+)/); var id = d && d[3]; results[id] = { pid: d && d[1], status: d && d[2], id: d && d[3], } } cb(); }, function (cb) { then && then(null, results); cb(); })) c.on('error', then); c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) return c; } this.describe = function (serviceId, then) { var that = this; that.findUnitFile(serviceId, function (err, results) { if(!results.length) return then(new Error('service not found: ' + serviceId)) that.describeFile(results[0], then) }) } this.describeFile = function (file, then) { return this.convertUnitFile(file, 'json', function (err, content){ if (err) return then(err); debug('content=%j', content); try{ return then(null, JSON.parse(content)) }catch(ex){ return then(ex) } }); } this.load = function (serviceId, opts, then) { var that = this; debug('load %s', serviceId); that.findUnitFile(serviceId, function (err, results){ debug('results %j', results); if(!results.length) return then(new Error('service not found: ' + serviceId)); that.loadServiceFile(results[0], opts, then) }) } this.loadServiceFile = function (fileOrDir, opts, then) { var args = ['load'] if (opts.enable || opts.e) args.push('-w') if (opts.force || opts.f) args.push('-F') if (opts.session || opts.s) args = args.concat(['-S', opts.session || opts.s]) if (opts.domain || opts.d) args = args.concat(['-D', opts.domain || opts.d]) args.push(fileOrDir) var c = spawnAChild('launchctl', args, {stdio: 'pipe'}) var stdout = '' var stderr = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) c.stderr.on('data', function (d){ stderr += d.toString(); }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('close', function (code){ // on yosemite, a file not found will not return an exit code>0 // so, we shall apply some patch. if (code===0 && (stdout+stderr).match(/(No such file or directory)/)) code = 1; then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null) }) c.on('error', then); return c; } this.unload = function (serviceId, opts, then) { var that = this; that.findUnitFile(serviceId, function (err, results){ if(!results.length) return then(new Error('service not found: ' + serviceId)) that.unloadServiceFile(results[0], opts, then) }) } this.unloadServiceFile = function (fileOrDir, opts, then) { var args = ['unload'] if (opts.enable || opts.e) args.push('-w') if (opts.session || opts.s) args = args.concat(['-S', opts.session || opts.s]) if (opts.domain || opts.d) args = args.concat(['-D', opts.domain || opts.d]) args.push(fileOrDir) var c = spawnAChild('launchctl', args, {stdio: 'pipe'}) var stdout = '' var stderr = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ // on yosemite, a file not found will not return an exit code>0 // so, we shall apply some patch. if (code===0 && (stdout+stderr).match(/(No such file or directory)/)) code = 1; then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); return c; } this.findUnitFile = function (serviceId, then) { var results = [] paths = [ '/System/Library/LaunchDaemons', '/System/Library/LaunchAgents', '/Library/LaunchDaemons', '/Library/LaunchAgents', process.env['HOME'] + '/Library/LaunchAgents' ] paths.forEach(function (dir, i) { var k = path.join(dir, serviceId + '.plist'); fs.access(k, fs.FS_OK, function (err){ if(!err) results.push(k); if(i===paths.length-1) { debug('findUnitFile %s', results); then(null, results); } }) }) } this.forgePath = function (opts) { var domain = opts.d || opts.domain; var jobType = opts.jobType; var dir = null; if (domain==='user') { dir = process.env['HOME'] + '/Library/LaunchAgents' } else if(domain==='global') { if(jobType==='agent') dir = '/Library/LaunchAgents' else if(!jobType || jobType==='daemon') dir = '/Library/LaunchDaemons' } else if(domain==='system') { if(jobType==='agent') dir = '/System/Library/LaunchAgents' else if(!jobType || jobType==='daemon') dir = '/System/Library/LaunchDaemons' } debug('forgePath %s %s %s', domain, jobType, dir) return dir; } this.forgeStdLogPath = function (opts) { var domain = opts.d || opts.domain; var dir = null; if (domain==='user') { dir = process.env['HOME'] + "/Library/Logs/" } else { dir = "/private/var/log/" } debug('forgeStdLogPath %s %s %s', domain, dir) return dir; } this.listUnitFiles = function (opts, then) { var dir = this.forgePath(opts); fs.readdir(dir, function (err, files) { if (err) return then(err); then(null, files.map(function (name){ return path.join(dir, name) })) }); } this.testUnitFile = function (file, then) { var c = spawnAChild('plutil', [file], {stdio: 'pipe'}) var stdout = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) var stderr = '' c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null, stdout) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); return c; } this.convertUnitFile = function (file, fmt, then) { var c = spawnAChild('plutil', ['-convert', fmt, '-o', '-', file], {stdio: 'pipe'}) var stdout = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) var stderr = '' c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null, stdout) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); return c; } this.convertJsonToPlist = function (obj, then) { var c = spawn('plutil', ['-convert', 'xml1', '-o', '-', '-'], {stdio: 'pipe'}) var stdout = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) var stderr = '' c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null, stdout) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); c.stdin.end(JSON.stringify(obj)) return c; } this.start = function (serviceId, then) { var c = spawnAChild('launchctl', ['start', serviceId], {stdio: 'pipe'}) var stdout = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) var stderr = '' c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); return c; } this.stop = function (serviceId, then) { var c = spawnAChild('launchctl', ['stop', serviceId], {stdio: 'pipe'}) var stdout = '' c.stdout.on('data', function (d){ stdout += d.toString(); }) var stderr = '' c.stderr.on('data', function (d){ stderr += d.toString(); }) c.on('close', function (code){ then(code>0 ? new Error('code='+code+'\n'+stdout+'\n'+stderr) : null) }) c.stdout.pipe(dStream('process.stdout: %s').resume()) c.stderr.pipe(dStream('process.stderr: %s').resume()) c.on('error', then); return c; } this.restart = function (serviceId, then) { var that = this; that.stop(serviceId, function (err) { if (err) return then(err); that.start(serviceId, then) }) } this.install = function (opts, then) { debug('install %j', opts) this.convertJsonToPlist(opts.plist, function(err, plist) { if(err) return then(err); var dir = that.forgePath(opts); var fPath = path.join(dir, opts.plist.Label + '.plist'); debug('dir %s', dir) async.series([ function (next) { if (opts.plist.StandardOutPath) { return (getFs().mkdirs || getFs().mkdir)(path.dirname(opts.plist.StandardOutPath), next); } next(); }, function (next) { if (opts.plist.StandardErrorPath) { return (getFs().mkdirs || getFs().mkdir)(path.dirname(opts.plist.StandardErrorPath), next); } next(); }, function (next) { if ((opts.d || opts.domain)==='user') (getFs().mkdirs || getFs().mkdir)(dir, next); else next(); // don t create the path for a system directory }, function (next) { getFs().writeFile(fPath, plist, next) }, function (next) { getFs().chmod(fPath, 0644, next) }, function (next) { if ((opts.d || opts.domain)==='user') getFs().chmod(dir, 0755, next) else next(); // don t change the mod for a system directory }, ], then) }) } this.uninstall = function (serviceId, then) { var that = this; that.findUnitFile(serviceId, function (err, results) { results.forEach(function (p, i) { that.uninstallUnitFile(p, function (err) { if (i===results.length-1) then(err); }) }) if(!results.length) return then(new Error('service not found: ' + serviceId)) }) } this.uninstallUnitFile = function (file, then) { getFs().unlink(file, then) } } module.exports = LaunchdSimpleApi