UNPKG

weaver

Version:

Interactive process management system

354 lines (262 loc) 6.59 kB
assert = require('assert') resolve = require('path').resolve fork = require('child_process').spawn EventEmitter = require('events').EventEmitter Watcher = require('./watcher') ###* #* Status codes #* R - restart #* E - error #* D - done (clean exit) #* W - work in progress #* S - stopped ### # Which subtask parameters can be changed without restart mutable = count : yes source : no cwd : no env : no persistent : yes executable : no timeout : yes runtime : yes watch : yes arguments : no class Task extends EventEmitter @tasks: Object.create(null) @create: (name) -> return @tasks[name] ?= new Task(name) @destroy: (name) -> if name of @tasks Watcher.stop(@tasks[name].watchHandler) delete @tasks[name] return @status: -> now = Date.now() status = {} for name, task of @tasks status[name] = count : task.count source : task.source restart : task.restart subtasks : task.subtasks.map (subtask) -> pid : subtask.pid args : subtask.args status : subtask.status uptime : now - subtask.start return status log: -> active: yes constructor: (@name) -> @subtasks = [] @watchHandler = (error) => if error @emit('error', error) else @restartSubtasks() return @on('exit', @exitHandler) return @ # Upgrade task upgrade: (options = {}) -> restartRequired = no for own key of mutable try assert.deepEqual(@[key], options[key]) catch change @upgradeParameter(key, options[key]) # Force restart when one of non-mutable keys gets modified restartRequired = restartRequired or not mutable[key] # Restart existing if restartRequired and @subtasks.length @log("Restart required for #{@name} task group") @restartSubtasks() # Spawn required for index in [0...(@count or 0)] subtask = @subtasks[index] if not subtask or (subtask.status is 'R' and not subtask.pid) @spawn(index) # Kill redundant while @subtasks.length > (@count or 0) @stopSubtask(@subtasks.pop()) return # Upgrade task parameter with given value upgradeParameter: (key, value) -> if value? @[key] = value else delete @[key] switch key when 'watch' Watcher.stop(@watchHandler) Watcher.start(@cwd, @watch or [], @watchHandler) return # Spawn subtask spawn: (id) -> args = @arguments or [] binary = process.execPath subtask = id : id status : 'W' name : @name start : Date.now() env : @expandEnv() subtask.args = for argument in args if Array.isArray(argument) then argument[id] else argument eargs = subtask.args.slice() if @executable binary = @source else eargs.unshift(@source) subtask.process = fork(binary, eargs, { stdio : 'pipe' cwd : resolve(@cwd) env : subtask.env }) subtask.pid = subtask.process.pid or 0 if subtask.pid # Setup logger subtask.process.stdout.on('data', @logHandler.bind(@, subtask.pid)) subtask.process.stderr.on('data', @logHandler.bind(@, subtask.pid)) # Setup exit handler subtask.process.once('exit', @emit.bind(@, 'exit', subtask)) @log("Task #{subtask.pid} (#{@name}) spawned") subtask.process.once('error', (error) => subtask.status = 'E' subtask.code = 255 subtask.pid = 0 @emit('error', error) @log("Failed to start task (#{@name})") ) @subtasks[id] = subtask return # Call fn for each subtask foreach: (fn, argument) -> for subtask in @subtasks fn.call(@, subtask, argument) return # Kill subtask with signal killSubtask: (subtask, signal) -> if subtask and subtask.pid try subtask.process.kill(signal) catch error @log("Failed to kill #{subtask.pid} (#{subtask.name}) with #{signal}") return # Stop subtask stopSubtask: (subtask) -> if subtask and subtask.pid subtask.process.kill('SIGINT') setTimeout((-> if subtask.pid subtask.process.kill('SIGTERM') ), @timeout or 1000) # Restart subtask restartSubtask: (subtask) -> if subtask subtask.status = 'R' @stopSubtask(subtask) return # Get subtask by pid getPID: (pid) -> for subtask in @subtasks when subtask and subtask.pid is pid return subtask return # Kill subtask by pid with signal killPID: (pid, signal) -> if pid? @killSubtask(@getPID(pid), signal) else @killSubtasks(signal) return # Restart subtask by pid restartPID: (pid) -> if pid? @restartSubtask(@getPID(pid)) else @restartSubtasks() return # Stop subtask by pid stopPID: (pid) -> if pid? @stopSubtask(@getPID(pid)) else @stopSubtasks() return # Kill all subtasks killSubtasks: -> @foreach(@killSubtask) return # Restart all subtasks restartSubtasks: -> @foreach(@restartSubtask) return # Stop all subtasks stopSubtasks: -> @foreach(@stopSubtask) return # Drop task dropSubtasks: -> if @active @active = no unless @activeSubtasks().length Task.destroy(@name) else @stopSubtasks() return activeSubtasks: -> return @subtasks.filter((subtask) -> subtask.pid) exitHandler: (subtask, code, signal) -> restartRequired = @persistent if code is null @log("Task #{subtask.pid} (#{@name}) was killed by #{signal}") else @log("Task #{subtask.pid} (#{@name}) exited with code #{code}") subtask.pid = 0 subtask.code = code subtask.signal = signal delete subtask.process if subtask.status isnt 'R' if code subtask.status = 'E' else if signal subtask.status = 'S' else subtask.status = 'D' if restartRequired and code elapsed = Date.now() - subtask.start if elapsed < (@runtime or 1000) @log("Restart skipped after #{elapsed}ms (#{@name})") restartRequired = no # Restart requested if subtask.status is 'R' restartRequired = yes # Task dropped unless @active restartRequired = no unless @activeSubtasks().length Task.destroy(@name) if restartRequired @spawn(subtask.id) return logHandler: (pid, data) -> @log("#{pid} (#{@name}) #{data}") return expandEnv: -> expanded = {} expanded.HOME = process.env.HOME expanded.PATH = process.env.PATH unless @executable expanded.NODE_PATH = process.env.NODE_PATH for own key, value of @env switch value when true if process.env.hasOwnProperty(key) expanded[key] = process.env[key] when false delete expanded[key] else expanded[key] = @env[key] return expanded module.exports = Task