New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

weaver

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

weaver - npm Package Compare versions

Comparing version
0.0.11
to
0.1.0
+51
changelog.md
# Changelog
## 0.1.0
Released 2014-01-29
* Monitor mode
* Messages on start and exit
* Uptime in status
* Use `weaver.json` from start directory if available
* Code cleanup and many minor fixes
## 0.0.11
Released 2013-06-27
* Use env variables `WEAVER_DEBUG` and `WEAVER_PORT`
* Configurable minimal uptime for persistent tasks to be restarted on error
## 0.0.10
Released 2013-03-26
* Fixes for node 0.10.x
## 0.0.9
Released 2013-03-16
* Fixed daemon path resolution
## 0.0.8
Released 2013-03-16
* Added version to `status` command output
* Removed `node-daemon` dependency
## 0.0.7
Released 2013-03-13
* Added uptime to `status` command output
* Added `dump` command
* Pass env variables to child processes
## 0.0.6
Released 2013-02-10
* Run executable files as child processes
assert = require('assert')
weaver = require('../lib/weaver.js')
emitter = require('events').EventEmitter
methods = [
'define', 'task', 'log', 'validate', 'die', 'upgrade',
'status', 'command'
]
events = ['error', 'config', 'upgrade']
(require 'vows')
.describe('basic')
.addBatch
constructor: ->
assert.instanceOf weaver, weaver.constructor
assert.instanceOf weaver, emitter
properties: ->
# version
assert.equal weaver.version, require('../package').version
# start
assert.isNumber weaver.start
assert weaver.start <= Date.now()
assert weaver.start > 0
# tasks
assert.deepEqual weaver.tasks, {}
# parameters
assert.deepEqual weaver.parameters, {}
# file
assert.equal weaver.file, ''
methods: ->
for method in methods
assert.isFunction weaver[method]
assert not weaver.propertyIsEnumerable method
define: ->
noop = ->
weaver.define 'method', 'noop', noop
# New method defined
assert.equal weaver.noop, noop
assert not weaver.propertyIsEnumerable 'noop'
events: ->
for event in events
assert.equal emitter.listenerCount(weaver, event), 1
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
emitter = require('events').EventEmitter
methods = [
'upgrade', 'upgradeParameter', 'expandEnv', 'get', 'spawn', 'foreach',
'killSubtask', 'stopSubtask', 'restartSubtask',
'killPID', 'stopPID', 'restartPID',
'stopSubtasks', 'killSubtasks', 'restartSubtasks',
'log', 'exitHandler'
]
defaultName = 'test' + Date.now()
weaver.task(defaultName, {})
(require 'vows')
.describe('task')
.addBatch
# Check default properties
properties: ->
name = defaultName
task = weaver.tasks[name]
assert.equal name, task.name
assert.isArray task.subtasks
assert.isArray task.watch
assert.isArray task.arguments
assert.isObject task.env
assert.equal 1000, task.timeout
assert.equal 1000, task.runtime
assert.equal 0, task.count
assert.equal '', task.source
assert.equal false, task.executable
assert.equal false, task.persistent
assert.equal process.cwd(), task.cwd
methods: ->
name = defaultName
task = weaver.tasks[name]
for method in methods
assert.isFunction task[method]
assert not weaver.propertyIsEnumerable task
constructor: ->
name = Math.random()
task = weaver.tasks[name]
assert.isUndefined task
task = weaver.task(name)
assert.equal task.constructor, weaver.task
assert.equal task, weaver.tasks[name]
assert.equal 1000, task.runtime
assert.equal 1000, task.timeout
assert.equal task, weaver.task(name, runtime: 2000)
assert.equal 2000, task.runtime
assert.equal task, weaver.task(name, timeout: 5000)
assert.equal 5000, task.timeout
assert.equal task, weaver.tasks[name]
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
emitter = require('events').EventEmitter
(require 'vows')
.describe('validate')
.addBatch
optional: ->
# One task required
assert.throws ->
weaver.validate
tasks: {}
# Task should be object
assert.throws ->
weaver.validate
tasks: test: null
assert.throws ->
weaver.validate
tasks: test: undefined
# Source and count required
assert.throws ->
weaver.validate
tasks: test: {}
# All required fields present
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
count: 0
# All required fields and one optional
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
cwd: './'
count: 0
path: ->
# Path should be string
assert.throws ->
weaver.validate
path: true
tasks:
test:
source: 'test'
count: 0
# Path ok
assert.doesNotThrow ->
weaver.validate
path: '.'
tasks:
test:
source: 'test'
count: 0
# Count required
'required#count': ->
assert.throws ->
weaver.validate
tasks: test: source: 'test'
# Source required
'required#source': ->
assert.throws ->
weaver.validate
tasks: test: count: 0
# Count should be number
'format#count': ->
assert.throws ->
weaver.validate
tasks:
test:
source: ''
count: ''
assert.throws ->
weaver.validate
tasks:
test:
source: '1739'
count: false
assert.throws ->
weaver.validate
tasks:
test:
source: '1234'
count: null
# Count should be positive or zero
assert.throws ->
weaver.validate
tasks:
test:
source: '1739'
count: -1
# Count should not be fractional
assert.throws ->
weaver.validate
tasks:
test:
source: '1739'
count: 1.1
# Source should be string
'format#source': ->
assert.throws ->
weaver.validate
tasks:
test:
source: false
count: 1
assert.throws ->
weaver.validate
tasks:
test:
source: 0
count: 2
assert.throws ->
weaver.validate
tasks:
test:
source: null
count: 3
# Watch
'format#watch': ->
# Only array allowed
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
watch: null
# Empty is ok
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
watch: []
# Only string patterns allowed
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
watch: [/test/, null]
# All ok
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
watch: ['**/*.js']
# Arguments
'format#arguments': ->
# Only array allowed
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: null
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: {}
# Empty array is ok
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: []
# Values with right type
assert.doesNotThrow ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: ['test', 0, [1,2,3]]
# Null not allowed
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: [null]
# Object not allowed
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: [{}]
# Wrong option count
assert.throws ->
weaver.validate
tasks:
test:
source: 'test'
count: 3
arguments: [[1,2]]
# Unexpected option
'unknown': ->
assert.throws ->
weaver.validate
tasks:
test:
source: false
count: 1
abcef: 92
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
(require 'vows')
.describe('define')
.addBatch
'property#writable': ->
name = "test_#{Math.random()}"
value = Math.random()
weaver.define 'property', name, value
assert name of weaver
assert.equal weaver[name], value
'property#protected': ->
name = "test_#{Math.random()}"
value = Math.random()
weaver.define 'property', name, value, writable: false
assert name of weaver
assert.equal weaver[name], value
assert.throws ->
weaver.define 'property', name, 1 + value
'method#parameters': ->
name = "test_#{Math.random()}"
assert.throws ->
weaver.define 'method', name, null
assert.throws ->
weaver.define 'method', name, {}
weaver.define 'method', name, ->
assert not weaver.propertyIsEnumerable name
assert.equal typeof weaver[name], 'function'
'method#protected': ->
name = "test_#{Math.random()}"
weaver.define 'method', name, (->), writable: false
assert.throws ->
weaver.define 'method', name, ->
handler: ->
listeners = weaver.listeners('error').length
for i in [1 .. 5]
weaver.define 'handler', 'error', ->
assert.equal ++listeners, weaver.listeners('error').length
parameters: ->
assert.throws ->
weaver.define()
assert.throws ->
weaver.define 'event'
target: ->
name = "test_#{Math.random()}"
value = Math.random()
target = Object.create null
weaver.define 'property', name, value, target: target
assert not (name of weaver)
assert name of target
assert.equal target[name], value
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
daemon = '../bin/weaver'
port = 58004
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
(require 'vows')
.describe('daemon')
.addBatch
version:
topic: ->
command = "#{daemon} --version"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) ->
assert.include stdout, weaver.version
help:
topic: ->
command = "#{daemon} --help"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) ->
assert.include stderr, 'Usage'
assert.include stderr, 'Commands'
assert.include stderr, 'Options'
status:
topic: ->
command = "#{daemon} status"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert error?.code
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) ->
assert.include stderr, 'Could not connect'
assert.include stderr, port
start:
topic: ->
exec daemon, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
command = "#{daemon} status --nocolor"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
version: (error, stdout, stderr) -> assert.include stdout, weaver.version
name: (error, stdout, stderr) -> assert.include stdout, 'weaver'
pid: (error, stdout, stderr) -> assert.match stdout, /^\s*\d+\s/
memory: (error, stdout, stderr) -> assert.match stdout, /\s\(\d+K\)/
exit:
topic: ->
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
command = "#{daemon} status"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert error?.code
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) ->
assert.include stderr, 'Could not connect'
assert.include stderr, port
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
spawn = require('child_process').spawn
daemon = '../bin/weaver'
port = 58005
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
log = ''
monitor = spawn daemon, ['monitor'], options
monitor.stdout.on 'data', (data) -> log = String(data)
(require 'vows')
.describe('udp')
.addBatch
start:
topic: ->
exec daemon, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
log: (error, stdout, stderr) -> assert.match log, /started/i
exit:
topic: ->
command = "#{daemon} exit"
exec command, options, (args...) =>
# Wait for message to arrive
setTimeout((=>
# Remove zombie
monitor.kill('SIGTERM')
# Run tests
@callback(args...)
), 50)
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
log: (error, stdout, stderr) -> assert.match log, /terminated/i
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
write = require('fs').writeFileSync
unlink = require('fs').unlinkSync
daemon = '../bin/weaver'
port = 58006
config = "#{__dirname}/weaver_#{port}.json"
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
(require 'vows')
.describe('upgrade')
.addBatch
start:
topic: ->
# Write empty config
write config, JSON.stringify tasks: {}
# Start daemon
command = "#{daemon} --config #{config}"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
upgrade:
topic: ->
# Write new config
write config, JSON.stringify
tasks:
base:
count: 1
executable: yes
source: 'sleep'
arguments: [1000]
# Run upgrade command
command = "#{daemon} upgrade"
exec command, options, @callback
return
status:
topic: ->
# Check status
command = "#{daemon} status --nocolor"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
assert.equal status.length, 2
assert.match status[1], /^ *\d+ +W +\d+s +base +sleep 1000/
upgrade:
topic: ->
# Write new config with increased count and updated arguments
write config, JSON.stringify
tasks:
base:
count: 2
executable: yes
source: 'sleep'
arguments: [2000]
bonus:
count: 1
executable: yes
source: 'uname'
arguments: ['-a']
# Run upgrade command
command = "#{daemon} upgrade"
exec command, options, @callback
return
status:
topic: ->
# Check status
command = "#{daemon} status --nocolor"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
assert.equal status.length, 4
assert.match status[1], /^ *(\d+) +W +\d+s +base +sleep 2000/
pid1 = +RegExp.$1
assert.match status[2], /^ *(\d+) +W +\d+s +base +sleep 2000/
pid2 = +RegExp.$1
assert.match status[3], /^ *(\d+) +D +\d+s +bonus +uname -a/
pid3 = +RegExp.$1
assert.notEqual pid1, pid2
assert.notEqual pid1, pid3
assert.notEqual pid2, pid3
assert.equal pid3, 0
exit:
topic: ->
# Remove config file
unlink(config)
# Stop daemon
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
write = require('fs').writeFileSync
unlink = require('fs').unlinkSync
daemon = '../bin/weaver'
port = 58007
config = "#{__dirname}/weaver_#{port}.json"
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
status = []
(require 'vows')
.describe('restart')
.addBatch
start:
topic: ->
# Write config
write config, JSON.stringify
tasks:
s1:
count: 4
executable: yes
source: 'sleep'
arguments: [2000]
s2:
count: 2
executable: yes
source: 'sleep'
arguments: [4000]
# Start daemon
command = "#{daemon} --config #{config}"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
args[1] = args[1]
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
@callback(args...)
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) -> assert.equal stdout.length, 7
restart:
topic: (pid) ->
# Restart one task from first group by pid
exec "#{daemon} restart #{pid[3]}", options, =>
# Restart second group
exec "#{daemon} restart s2", options, =>
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
@callback(args..., pid)
return
code: (error, stdout, stderr, pid) -> assert not error
stderr: (error, stdout, stderr, pid) -> assert not stderr
stdout: (error, stdout, stderr, pid) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
assert.equal status.length, pid.length
assert.equal pid[1], status[1]
assert.equal pid[2], status[2]
assert.notEqual pid[3], status[3]
assert.equal pid[4], status[4]
assert.notEqual pid[5], status[5]
assert.notEqual pid[6], status[6]
exit:
topic: ->
# Remove config file
unlink(config)
# Stop daemon
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
write = require('fs').writeFileSync
unlink = require('fs').unlinkSync
daemon = '../bin/weaver'
port = 58008
config = "#{__dirname}/weaver_#{port}.json"
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
status = []
(require 'vows')
.describe('stop')
.addBatch
start:
topic: ->
# Write config
write config, JSON.stringify
tasks:
s1:
count: 4
executable: yes
source: 'sleep'
arguments: [2000]
s2:
count: 2
executable: yes
source: 'sleep'
arguments: [4000]
# Start daemon
command = "#{daemon} --config #{config}"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
args[1] = args[1]
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
@callback(args...)
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) -> assert.equal stdout.length, 7
stop:
topic: (pid) ->
# Stop one task from first group by pid
exec "#{daemon} stop #{pid[3]}", options, =>
# Stop second group
exec "#{daemon} stop s2", options, =>
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
@callback(args..., pid)
return
code: (error, stdout, stderr, pid) -> assert not error
stderr: (error, stdout, stderr, pid) -> assert not stderr
stdout: (error, stdout, stderr, pid) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
assert.equal status.length, pid.length
assert.equal stdout.split(' 0 S ').length, 4
assert.equal pid[1], status[1]
assert.equal pid[2], status[2]
assert.equal 0, status[3]
assert.equal pid[4], status[4]
assert.equal 0, status[5]
assert.equal 0, status[6]
exit:
topic: ->
# Remove config file
unlink(config)
# Stop daemon
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
write = require('fs').writeFileSync
unlink = require('fs').unlinkSync
daemon = '../bin/weaver'
port = 58009
config = "#{__dirname}/weaver_#{port}.json"
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
status = []
(require 'vows')
.describe('kill')
.addBatch
start:
topic: ->
# Write config
write config, JSON.stringify
tasks:
s1:
count: 4
executable: yes
source: 'sleep'
arguments: [2000]
s2:
count: 2
executable: yes
source: 'sleep'
arguments: [4000]
# Start daemon
command = "#{daemon} --config #{config}"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
args[1] = args[1]
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
@callback(args...)
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) -> assert.equal stdout.length, 7
kill:
topic: (pid) ->
# Kill one task from first group by pid
exec "#{daemon} kill SIGINT #{pid[2]}", options, =>
# Kill second group
exec "#{daemon} kill SIGTERM s2", options, =>
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
@callback(args..., pid)
return
code: (error, stdout, stderr, pid) -> assert not error
stderr: (error, stdout, stderr, pid) -> assert not stderr
stdout: (error, stdout, stderr, pid) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
assert.equal status.length, pid.length
assert.equal stdout.split(' 0 S ').length, 4
assert.equal pid[1], status[1]
assert.equal 0, status[2]
assert.equal pid[3], status[3]
assert.equal pid[4], status[4]
assert.equal 0, status[5]
assert.equal 0, status[6]
exit:
topic: ->
# Remove config file
unlink(config)
# Stop daemon
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
.export(module)
assert = require('assert')
weaver = require('../lib/weaver.js')
exec = require('child_process').exec
write = require('fs').writeFileSync
unlink = require('fs').unlinkSync
daemon = '../bin/weaver'
port = 58010
config = "#{__dirname}/weaver_#{port}.json"
options =
cwd: __dirname
env:
PATH: process.env.PATH
WEAVER_TEST: 1
WEAVER_PORT: port
configData = JSON.stringify
tasks:
base:
count: 2
executable: yes
source: 'sleep'
watch: [config]
arguments: [4000]
status = []
(require 'vows')
.describe('watch')
.addBatch
start:
topic: ->
# Write config
write config, configData
# Start daemon
command = "#{daemon} --config #{config}"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
status:
topic: ->
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
args[1] = args[1]
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
@callback(args...)
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) -> assert.equal stdout.length, 3
watch:
topic: (pid) ->
# Rewrite config
write config, configData
exec "#{daemon} status --nocolor", options, (args...) => @callback(args..., pid)
return
code: (error, stdout, stderr, pid) -> assert not error
stderr: (error, stdout, stderr, pid) -> assert not stderr
stdout: (error, stdout, stderr, pid) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
assert.equal status.length, pid.length
assert.notEqual pid[1], status[1]
assert.notEqual pid[2], status[2]
assert status[1]
assert status[2]
status:
topic: ->
# Check status
exec "#{daemon} status --nocolor", options, (args...) =>
args[1] = args[1]
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
@callback(args...)
return
code: (error, stdout, stderr) -> assert not error
stderr: (error, stdout, stderr) -> assert not stderr
stdout: (error, stdout, stderr) -> assert.equal stdout.length, 3
watch:
topic: (pid) ->
# Rewrite config
write config, configData
exec "#{daemon} status --nocolor", options, (args...) => @callback(args..., pid)
return
code: (error, stdout, stderr, pid) -> assert not error
stderr: (error, stdout, stderr, pid) -> assert not stderr
stdout: (error, stdout, stderr, pid) ->
status = stdout
.replace(/\n$/, '')
.split(/\n/)
.map(($_) -> +/^\s*(\d+)/.exec($_)[1])
assert.equal status.length, pid.length
assert.notEqual pid[1], status[1]
assert.notEqual pid[2], status[2]
assert status[1]
assert status[2]
exit:
topic: ->
# Remove config file
unlink(config)
# Stop daemon
command = "#{daemon} exit"
exec command, options, @callback
return
code: (error, stdout, stderr) -> assert not error
stdout: (error, stdout, stderr) -> assert not stdout
stderr: (error, stdout, stderr) -> assert not stderr
.export(module)
+2
-0

@@ -0,1 +1,3 @@

'use strict';
module.exports = __dirname + '/../bin/daemon';
+35
-38

@@ -5,20 +5,20 @@ 'use strict';

fs = require('fs'),
watches = Object.create(null),
watcher = Object.create(null),
Anubseran;
util = require('util'),
events = require('events'),
watches = {},
watcher = {};
Watcher.prototype = new (require('events').EventEmitter)();
function handler (weaver, file, event) {
weaver.log('File ' + file + ' changed');
function Watcher () {
this.start = start;
this.stop = stop;
return this;
(watches[file] || []).forEach(function (callback) {
callback();
});
}
function watch (cwd, callback, error, files) {
function watch (weaver, cwd, callback, error, files) {
var cbs, file, i, l;
if (error) {
Anubseran.emit('error', error);
weaver.emit('error', error);
return;

@@ -43,3 +43,3 @@ }

watches[file] = [callback];
watcher[file] = fs.watch(file, handler.bind(undefined, file));
watcher[file] = fs.watch(file, handler.bind(undefined, weaver, file));
}

@@ -49,4 +49,4 @@ }

function start (cwd, patterns, callback) {
var fn = watch.bind(undefined, cwd, callback),
function start (weaver, cwd, patterns, callback) {
var fn = watch.bind(undefined, weaver, cwd, callback),
i, l;

@@ -63,18 +63,21 @@

function stop (callback) {
var file, i, cbs;
var cbs, i, file;
for (file in watches) {
cbs = watches[file];
i = cbs.length;
while (i--) {
if (cbs[i] === callback) {
cbs.splice(i, 1);
if (watches.hasOwnProperty(file)) {
cbs = watches[file];
i = cbs.length;
while (i--) {
if (cbs[i] === callback) {
cbs.splice(i, 1);
}
}
}
if (!cbs.length) {
/* Stop watching file completely */
watcher[file].close();
delete watches[file];
delete watcher[file];
if (!cbs.length) {
/* Stop watching file completely */
watcher[file].close();
delete watches[file];
delete watcher[file];
}
}

@@ -84,17 +87,11 @@ }

function handler (file, event) {
var cbs = watches[file] || [],
i, l;
function Watcher () {
this.start = start;
this.stop = stop;
if (!Anubseran) {
Anubseran = new require('./weaver')();
}
return this;
}
Anubseran.log('File ' + file + ' changed');
util.inherits(Watcher, events.EventEmitter);
for (i = 0, l = cbs.length; i < l; i++) {
cbs[i]();
}
}
module.exports = new Watcher();
'use strict';
var fs = require('fs'),
assert = require('assert'),
dirname = require('path').dirname,
resolve = require('path').resolve,
Task = require('./task'),
Anubseran = null,
var fs = require('fs'),
assert = require('assert'),
util = require('util'),
events = require('events'),
assert = require('assert'),
dirname = require('path').dirname,
resolve = require('path').resolve,
fork = require('child_process').spawn,
Watcher = require('./watcher'),
sprintf = require('sprintf').sprintf,
/* Task options format */
/* Subtask parameters */
format = {

@@ -24,3 +28,3 @@ count : 'number',

/* Which task parameters are optional */
/* Which subtask parameters are optional */
optional = {

@@ -37,411 +41,919 @@ count : false,

arguments : true
},
/* Which subtask parameters can be changed without restart */
mutable = {
count : true,
source : false,
cwd : false,
env : false,
persistent : true,
executable : false,
timeout : true,
runtime : true,
watch : true,
arguments : false
};
/**
* Nerubian Weaver
* Weaver
* @class Weaver
* @constructor
*/
function Weaver (file, options) {
if (Anubseran) {
return Anubseran;
}
function Weaver () {}
if (!(this instanceof Weaver)) {
return new Weaver(file, options);
}
util.inherits(Weaver, events.EventEmitter);
Anubseran = this;
var weaver
= module.exports
= new Weaver();
/**
* Weaver version
* @property version
* @type String
*/
Anubseran.version = require('../package').version;
function define (type, name, value, descriptor) {
descriptor = descriptor || {};
descriptor.value = value;
descriptor.enumerable = true;
options = options || {};
var target = descriptor.target || weaver;
/**
* Write something in log
* @method log
*/
this.log = options.log || this.noop;
if (!('writable' in descriptor)) {
descriptor.writable = true;
}
/**
* Tasks by name
* @property tasks
* @type Object
*/
this.tasks = Object.create(null);
switch (type) {
case 'handler':
target.on(name, value);
return;
/**
* Parsed configuration file
* @property parameters
* @type Object
*/
this.parameters = {};
case 'method':
if (typeof value !== 'function') {
throw new Error('Method must be a function');
}
/**
* Configuration filename
* @property file
* @type String
*/
this.file = file;
descriptor.enumerable = false;
break;
/* Hide helpers */
this.$ = this.noop;
this._ = this.noop;
case 'property':
break;
/* Read configuration file for the first time */
this.config();
default:
throw new Error('Unsupported definition type ' + type);
}
return this;
Object.defineProperty(target, name, descriptor);
}
/*
* Helpers
/**
* Weaver version
* @property version
* @type String
*/
(function (Emitter) {
var proto = new Emitter();
define('property', 'version', require('../package').version, { writable: false });
/**
* Extend Weaver.prototype with property or method
* @method _
* @protected
* @param {String} name Property name
* @param value Property value
* @chainable
*/
Weaver._ = function (name, value) {
assert.ok(!proto.hasOwnProperty(name), 'Property ' + name + 'already exists');
/**
* Start timestamp
* @property start
* @type Number
*/
define('property', 'start', Date.now(), { writable: false });
proto[name] = value;
return this;
};
/**
* Tasks by name
* @property tasks
* @type Object
*/
define('property', 'tasks', Object.create(null), { writable: false });
/**
* Bind handler on event
* @method $
* @protected
* @param {String} event Event name
* @param {Function} handler Event handler
* @chainable
*/
Weaver.$ = function (event, handler) {
proto.on(event, handler);
return this;
};
/**
* Parsed configuration file
* @property parameters
* @type Object
*/
define('property', 'parameters', {});
Weaver.prototype = proto;
}(require('events').EventEmitter));
/**
* Path to configuration file
* @property file
* @type String
*/
define('property', 'file', '');
/*
* Methods
/**
* Extend Weaver with property or method
* @method define
*/
Weaver
/**
* Send SIGTERM to all processes and exit
* @method die
* @param {Number} code Exit code
* @chainable
*/
._('die', function (code) {
var tasks = Anubseran.tasks,
timeout = 100,
name;
define('method', 'define', define);
for (name in tasks) {
if (tasks[name].timeout > timeout) {
timeout = tasks[name].timeout;
/**
* Update or create new task group with given options
* @method task
* @param {String} name
* @param {Object} options
* @chainable
*/
define('method', 'task', Task);
/**
* Write something to log
* @method log
*/
define('method', 'log', function () {});
/**
* Validate configuration object
* @method validate
* @param {Object} config
*/
define('method', 'validate', function (config) {
var tasks = config.tasks,
task, i, l;
/* No tasks defined */
assert.equal(typeof tasks, 'object', 'Tasks object required');
assert.ok(Object.keys(tasks).length, 'At least one task required');
if ('path' in config) {
assert.equal(typeof config.path, 'string', 'Path should be a string');
}
Object.keys(tasks).forEach(function (name) {
task = tasks[name];
assert.equal(typeof task, 'object', 'Task is not an object');
/* Check presence for mandatory arguments */
Object.keys(optional).forEach(function (key) {
if (!optional[key]) {
assert.ok(key in task, 'Option ' + key + ' required');
}
});
tasks[name].persistent = false;
tasks[name].stop();
Object.keys(task).forEach(function (key) {
var value = task[key],
type = format[key];
assert.ok(type, 'Unknown option ' + key);
if (!(type === 'array' && Array.isArray(value))) {
assert.equal(typeof value, format[key], 'Expected ' + key + ' to be ' + type);
if (type === 'number') {
assert.equal(value, ~~value, 'Expected ' + key + ' to be integer');
assert(value >= 0, 'Expected ' + key + ' to be not negative');
}
return;
}
/* Fall here for arrays */
switch (key) {
case 'arguments':
value.forEach(function (argument) {
switch (typeof argument) {
/* Elementary types */
case 'string':
case 'number':
return;
case 'object':
if (Array.isArray(argument)) {
assert.equal(
task.count, argument.length,
'Options array should contain ' + task.count + ' values'
);
return;
}
break;
}
throw new Error('Unknown type in options');
});
break;
case 'watch':
for (i = 0, l = value.length; i < l; i++) {
assert.equal(typeof value[i], 'string', 'Watch pattern should be string');
}
break;
}
});
task.name = name;
});
return config;
});
/**
* Send SIGTERM to all processes and exit
* @method die
* @param {Number} code Exit code
* @chainable
*/
define('method', 'die', function (code) {
var that = this,
tasks = this.tasks,
timeout = 100,
name;
for (name in tasks) {
if (tasks[name].timeout > timeout) {
timeout = tasks[name].timeout;
}
tasks[name].persistent = false;
tasks[name].stopSubtasks();
}
setTimeout(function () {
code = null == code? 1 : code;
that.log(sprintf('Terminated with code %u', code));
setTimeout(function () {
process.exit(null == code? 1 : code);
}, timeout);
process.exit(code);
}, 100);
}, timeout);
return Anubseran;
})
return this;
});
/**
* Upgrade current state
* @method upgrade
* @param {String} data Configuration data
*/
._('upgrade', function (data) {
var parameters;
/**
* Upgrade current state
* @method upgrade
* @param {String} data Configuration data
* @chainable
*/
define('method', 'upgrade', function (data) {
var parameters;
try {
/* Try to parse JSON */
data = JSON.parse(data);
try {
/* Try to parse JSON */
data = JSON.parse(data);
/* Validate new state */
parameters = validate(data);
} catch (error) {
error.message = 'Config error: ' + error.message;
Anubseran.emit('error', error);
}
/* Validate new state */
parameters = this.validate(data);
} catch (error) {
error.message = 'Config error: ' + error.message;
this.emit('error', error);
}
if (parameters) {
Anubseran.parameters = parameters;
Anubseran.emit('upgrade');
if (parameters) {
this.parameters = parameters;
this.emit('upgrade');
}
return this;
});
/**
* Get status report
* @method status
* @return {Object} Task groups with subtasks data
*/
define('method', 'status', function () {
var tasks = this.tasks,
now = Date.now(),
result = {},
i, l, name, task, subtask, subtasks;
for (name in tasks) {
result[name] = task = {
count : tasks[name].count,
source : tasks[name].source,
restart : tasks[name].restart,
subtasks : []
};
subtasks = tasks[name].subtasks;
for (i = 0, l = subtasks.length; i < l; i++) {
subtask = subtasks[i];
task.subtasks.push(subtask? {
pid : subtask.pid,
args : subtask.args,
status : subtask.status,
uptime : now - subtask.start
} : null);
}
}
return Anubseran;
})
return result;
});
._('check', function () {
Anubseran.emit('upgrade');
return Anubseran;
})
/**
* Execute command with given arguments
* @method command
* @param {String} action Action name
* @param {String|Number} name Task group name or subtask pid
* @param {Array} args Arguments
* @chainable
*/
define('method', 'command', function (action, name, args) {
var tasks = this.tasks,
fn = Task.prototype[action + 'PID'],
task;
/**
* Emit config event
* @method config
* @chainable
*/
._('config', function () {
Anubseran.emit('config');
if (!Array.isArray(args)) {
args = [];
}
return Anubseran;
})
if (typeof fn !== 'function') {
throw new Error('Unknown action ' + action);
}
/**
* Get status report
* @method status
* @return {Object} Task groups with subtasks data
*/
._('status', function () {
var tasks = Anubseran.tasks,
result = {},
i, l, name, task, subtask, subtasks;
/* Execute command for all tasks */
if (null == name) {
for (name in tasks) {
result[name] = task = {
count : tasks[name].count,
source : tasks[name].source,
restart : tasks[name].restart,
subtasks : []
};
fn.apply(tasks[name], args);
}
} else {
task = tasks[name];
subtasks = tasks[name].subtasks;
if (task) {
if (action === 'kill') {
args.unshift(null);
}
for (i = 0, l = subtasks.length; i < l; i++) {
subtask = subtasks[i];
task.subtasks.push(subtask? {
pid : subtask.pid,
args : subtask.args,
status : subtask.status,
time : subtask.time
} : null);
}
fn.apply(task, args);
} else if (Number(name) == name) {
args.unshift(Number(name));
this.command(action, null, args);
} else {
this.log('Task ' + name + ' was not found');
}
}
return result;
})
return this;
});
._('restart', function (name) {
command('restart', name);
return Anubseran;
})
/**
* Fired when error occured
* @event error
* @param {Error} error Object with error details
*/
define('handler', 'error', function (error) {
this.log(error.message);
});
._('stop', function (name) {
command('stop', name);
return Anubseran;
})
/**
* Fired when configuration file should be re-read
* @event config
*/
define('handler', 'config', function () {
var that = this;
._('kill', function (name, signal) {
command('kill', name, [signal]);
return Anubseran;
})
if (this.file) {
fs.readFile(this.file, function (error, data) {
if (error) {
that.emit('error', error);
} else {
that.upgrade(data);
}
});
}
});
/**
* Empty function
* @method noop
* @chainable
*/
._('noop', function () { return this; });
/**
* Fired when tasks should be checked and upgraded
* @event upgrade
*/
define('handler', 'upgrade', function () {
var tasks = this.parameters.tasks,
path = this.parameters.path || '',
name;
if (path[0] !== '/') {
path = dirname(this.file) + '/' + path;
}
this.path = resolve(path);
for (name in tasks) {
/* Set cwd for tasks */
tasks[name].cwd = tasks[name].cwd || this.path;
/* Create or update task */
this.task(name, tasks[name]);
}
});
/*
* Events
* Task status codes
* R - restart
* E - error
* D - done (clean exit)
* W - work in progress
* S - stopped
*/
Weaver
/**
* Task singleton (by name)
* @class Task
* @constructor
*/
function Task (name, options) {
var env, task;
if (!(this instanceof Task)) {
return new Task(name, options);
}
task = weaver.tasks[name];
if (task) {
task.upgrade(options);
return task;
}
task = weaver.tasks[name] = this;
/**
* Fired when error occured
* @event error
* @param {Error} error Object with error details
* Task name
* @property name
* @type String
*/
.$('error', function (error) {
Anubseran.log(error.message);
})
define('property', 'name', name, {
writable : false,
target : this
});
/**
* Fired when configuration file should be re-read
* @event config
* Array with sub-process related data
* @property subtasks
* @type Array
*/
.$('config', function () {
if (Anubseran.file) {
fs.readFile(Anubseran.file, onRead);
}
})
define('property', 'subtasks', [], {
writable : false,
target : this
});
/**
* Fired when tasks should be checked and upgraded
* @event upgrade
* Watched patterns
* @property watch
* @type Array
*/
.$('upgrade', function () {
var tasks = Anubseran.parameters.tasks,
path = Anubseran.parameters.path || '',
name;
define('property', 'watch', [], { target: this });
if (path[0] !== '/') {
path = dirname(Anubseran.file) + '/' + path;
}
/**
* Watch callback
* @method watchHandler
*/
define('method', 'watchHandler', this.restartSubtasks.bind(this), { target: this });
Anubseran.path = resolve(path);
if (options) {
this.upgrade(options);
}
for (name in tasks) {
if (!tasks.hasOwnProperty(name)) {
continue;
return this;
}
/**
* Timeout between SIGINT and SIGTERM for stop and restart
* @property timeout
* @type Number
* @default 1000
*/
define('property', 'timeout', 1000, { target: Task.prototype });
/**
* Minimal runtime required for persistent task
* to be restarted after unclean exit
* @property runtime
* @type Number
* @default 1000
*/
define('property', 'runtime', 1000, { target: Task.prototype });
/**
* Subtasks count
* @property count
* @type Number
* @default 0
*/
define('property', 'count', 0, { target: Task.prototype });
/**
* Source to execute in subtask
* @property source
* @type String
*/
define('property', 'source', '', { target: Task.prototype });
/**
* Source file is executable
* @property executable
* @type Boolean
* @default false
*/
define('property', 'executable', false, { target: Task.prototype });
/**
* Working directory for subtasks
* @property cwd
* @type String
*/
define('property', 'cwd', process.cwd(), { target: Task.prototype });
/**
* Arguments for subtasks
* @property arguments
* @type Array
*/
define('property', 'arguments', [], { target: Task.prototype });
/**
* Restart subtask on dirty exit
* @property persistent
* @type Boolean
* @default false
*/
define('property', 'persistent', false, { target: Task.prototype });
/**
* Environment variables for subtasks
* @property env
* @type Object
*/
define('property', 'env', {}, { target: Task.prototype });
/**
* Upgrade task group
* @method upgrade
* @param {Object} parameters
* @chainable
*/
define('method', 'upgrade', function (parameters) {
var restart = false,
i, l, pid, subtask, key;
/* Change parameters */
if (parameters) {
for (key in mutable) {
try {
if (this.hasOwnProperty(key) || key in parameters) {
assert.deepEqual(this[key], parameters[key]);
}
} catch (change) {
this.upgradeParameter(key, parameters);
if (!mutable[key]) {
restart = true;
}
}
}
}
/* Set cwd for tasks to Anubseran.path */
tasks[name].cwd = tasks[name].cwd || Anubseran.path;
/* Restart on demand */
if (restart && this.subtasks.length) {
weaver.log(sprintf('Restart required for %s task group', this.name));
this.restartSubtasks();
}
Anubseran.tasks[name] = new Task(name, tasks[name]);
/* Check count */
for (i = 0, l = this.count; i < l; i++) {
subtask = this.subtasks[i];
if (!subtask || (subtask.status === 'R' && !subtask.pid)) {
this.spawn(i);
}
});
}
function validate (config) {
var tasks = config.tasks,
task, name, field, value, type, i, l;
/* Kill redundant */
while (this.subtasks.length > this.count) {
this.stopSubtask(this.subtasks.pop());
}
}, { target: Task.prototype });
/* No tasks defined */
assert.equal(typeof tasks, 'object', 'Tasks object required');
assert.ok(Object.keys(tasks).length, 'At least one task required');
/**
* Upgrade task parameter with value from parameters object
* @method upgradeParameter
* @param {String} key
* @param {Object} parameters
*/
define('method', 'upgradeParameter', function (key, parameters) {
switch (key) {
case 'watch':
this.watch = parameters.watch || [];
Watcher.stop(this.watchHandler);
Watcher.start(weaver, this.cwd, this.watch, this.watchHandler);
break;
if ('path' in config) {
assert.equal(typeof config.path, 'string', 'Path should be a string');
default:
if (key in parameters) {
this[key] = parameters[key];
} else {
delete this[key];
}
}
}, { target: Task.prototype });
for (name in tasks) {
if (!tasks.hasOwnProperty(name)) {
continue;
/**
* Expand variables in this.env
* @method expandEnv
* @return {Object} Object with expanded env variables
*/
define('method', 'expandEnv', function () {
var env = {
HOME: process.env.HOME,
PATH: process.env.PATH
}, key;
if (!this.executable) {
env.NODE_PATH = process.env.NODE_PATH;
}
for (key in this.env) {
switch (this.env[key]) {
case true:
env[key] = process.env[key];
break;
case false:
delete env[key];
break;
default:
env[key] = this.env[key];
}
}
task = tasks[name];
return env;
}, { target: Task.prototype });
assert.equal(typeof task, 'object', 'Task is not an object');
/**
* Get subtask by pid
* @method get
* @param {Number} pid
*/
define('method', 'get', function (pid) {
var subtasks = this.subtasks,
subtask, i, l;
for (field in optional) {
if (!optional.hasOwnProperty(field) || optional[field]) {
continue;
}
for (i = 0, l = subtasks.length; i < l; i++) {
subtask = subtasks[i];
assert.ok(task.hasOwnProperty(field), 'Option ' + field + ' required');
if (subtask && subtask.pid === pid) {
return subtask;
}
}
}, { target: Task.prototype });
for (field in task) {
if (!task.hasOwnProperty(field)) {
continue;
}
/**
* Spawn subtask with given id
* @method spawn
* @param {Number} id
*/
define('method', 'spawn', function (id) {
var args = this.arguments || [],
binary = process.execPath,
subtask = {
id : id,
args : [],
status : 'W',
name : this.name,
start : Date.now(),
env : this.expandEnv()
}, i, l, p1, p2, eargs;
value = task[field];
type = format[field];
/* Prepare arguments */
for (i = 0, l = args.length; i < l; i++) {
if (Array.isArray(args[i])) {
subtask.args.push(args[i][id]);
} else {
subtask.args.push(args[i]);
}
}
assert.ok(type, 'Unknown option ' + field);
eargs = subtask.args.slice();
if (!(type === 'array' && Array.isArray(value))) {
assert.equal(typeof value, format[field], 'Expected ' + field + ' to be ' + type);
/* Prepare binary/source */
if (this.executable) {
binary = this.source;
} else {
eargs.unshift(this.source);
}
if (type === 'number') {
assert.equal(value, ~~value, 'Expected ' + field + ' to be integer');
assert(value >= 0, 'Expected ' + field + ' to be not negative');
}
/* Create new process */
subtask.process = fork(binary, eargs, {
stdio : 'pipe',
cwd : resolve(this.cwd),
env : subtask.env
});
continue;
}
/* Get subtask pid */
subtask.pid = subtask.process.pid;
/* Fall here for arrays */
switch (field) {
case 'arguments':
for (i = 0, l = value.length; i < l; i++) {
switch (typeof value[i]) {
/* Elementary types */
case 'string':
case 'number':
continue;
p1 = sprintf('%u (%s) ', subtask.pid, subtask.name);
p2 = sprintf('%u [%s] ', subtask.pid, subtask.name);
case 'object':
if (Array.isArray(value[i])) {
break;
}
/* Setup logger */
subtask.process.stdout.on('data', this.log.bind(this, p1));
subtask.process.stderr.on('data', this.log.bind(this, p2));
default:
throw new Error('Unknown type in options');
}
/* Setup exit handler */
subtask.process.once('exit', this.exitHandler.bind(this, subtask));
assert.equal(task.count, value[i].length, 'Options array should contain ' + task.count + ' values');
}
weaver.log(sprintf('Task %u (%s) spawned', subtask.pid, subtask.name));
break;
this.subtasks[id] = subtask;
}, { target: Task.prototype });
case 'watch':
for (i = 0, l = value.length; i < l; i++) {
assert.equal(typeof value[i], 'string', 'Watch pattern should be string');
}
break;
}
}
/**
* Do something for each subtask
* @method foreach
* @param {Function} fn
* @param {Number|String} argument
*/
define('method', 'foreach', function (fn, argument) {
var subtasks = this.subtasks,
i, l;
task.name = name;
for (i = 0, l = subtasks.length; i < l; i++) {
fn.call(this, subtasks[i], argument);
}
}, { target: Task.prototype });
return config;
}
/**
* Kill given subtask with signal
* @method killSubtask
* @param {Object} subtask
* @param {String} signal
*/
define('method', 'killSubtask', function (subtask, signal) {
if (subtask && subtask.pid) {
try {
subtask.process.kill(signal);
} catch (error) {
weaver.log(sprintf(
'Failed to kill %u (%s) with %s',
subtask.pid, subtask.name, signal
));
}
}
}, { target: Task.prototype });
function onRead (error, data) {
var file, watches;
/**
* Stop given subtask
* @method stopSubtask
* @param {Object} subtask
*/
define('method', 'stopSubtask', function (subtask) {
if (subtask && subtask.pid) {
subtask.process.kill('SIGINT');
if (error) {
Anubseran.emit('error', error);
return;
setTimeout(function () {
if (subtask.pid) {
subtask.process.kill('SIGTERM');
}
}, this.timeout);
}
}, { target: Task.prototype });
Anubseran.upgrade(data);
}
/**
* Restart given subtask
* @method restartSubtask
* @param {Object} subtask
*/
define('method', 'restartSubtask', function (subtask) {
if (subtask) {
subtask.status = 'R';
this.stopSubtask(subtask);
}
}, { target: Task.prototype });
function command (action, name, options) {
var tasks = Anubseran.tasks,
task, fn;
/**
* Kill subtask with signal by PID
* @method killPID
* @param {Number} pid
* @param {String} signal
*/
define('method', 'killPID', function (pid, signal) {
if (null == pid) {
this.killSubtasks(signal);
} else {
this.killSubtask(this.get(pid), signal);
}
}, { target: Task.prototype });
if (!Array.isArray(options)) {
options = [];
/**
* Restart subtask by pid
* @method restartPID
* @param {Number} pid
*/
define('method', 'restartPID', function (pid) {
if (null == pid) {
this.restartSubtasks();
} else {
this.restartSubtask(this.get(pid));
}
}, { target: Task.prototype });
if (name === null) {
fn = Task.prototype[action];
/**
* Stop subtask by pid
* @method restartPID
* @param {Number} pid
*/
define('method', 'stopPID', function (pid) {
if (null == pid) {
this.stopSubtasks();
} else {
this.stopSubtask(this.get(pid));
}
}, { target: Task.prototype });
for (name in tasks) {
fn.apply(tasks[name], options);
/**
* Stop all subtasks
* @method stopSubtasks
*/
define('method', 'stopSubtasks', function () {
this.foreach(this.stopSubtask);
}, { target: Task.prototype });
/**
* Restart all subtasks
* @method restartSubtasks
*/
define('method', 'restartSubtasks', function () {
this.foreach(this.restartSubtask);
}, { target: Task.prototype });
/**
* Kill all subtasks with signal
* @method killSubtasks
* @param {String} signal
*/
define('method', 'killSubtasks', function (signal) {
this.foreach(this.killSubtask, signal);
}, { target: Task.prototype });
/**
* Output data from buffer to weaver.log
* @method log
* @param {String} prefix
* @param {Buffer} data
*/
define('method', 'log', function (prefix, data) {
var messages = data.toString().split('\n'),
i, l;
for (i = 0, l = messages.length; i < l; i++) {
if (messages[i]) {
weaver.log.call(null, prefix + messages[i]);
}
}
}, { target: Task.prototype });
return;
/**
* @method exitHandler
* @param {Object} subtask
* @param {Number} code
* @param {String} signal
*/
define('method', 'exitHandler', function (subtask, code, signal) {
var elapsed;
if (code === null) {
weaver.log(sprintf(
'Task %u (%s) was killed by %s',
subtask.pid, subtask.name, signal
));
} else {
weaver.log(sprintf(
'Task %u (%s) exited with code %u',
subtask.pid, subtask.name, code
));
}
task = tasks[name];
subtask.pid = 0;
subtask.code = code;
subtask.signal = signal;
if (task) {
task[action].apply(task, options);
delete subtask.process;
if (subtask.status !== 'R') {
if (code) {
subtask.status = 'E';
} else if (signal) {
subtask.status = 'S';
} else {
subtask.status = 'D';
}
}
if (name.match(/^\d+$/)) {
command(action, null, options.concat([Number(name)]));
if (this.persistent && code) {
elapsed = Date.now() - subtask.start;
if (elapsed < this.runtime) {
weaver.log(sprintf(
'Restart skipped after %ums (%s)',
elapsed, subtask.name
));
return;
}
}
}
module.exports = Weaver;
if (this.persistent || subtask.status === 'R') {
this.spawn(subtask.id);
}
}, { target: Task.prototype });
test: compile
vows --tap -i t/*.js
vows --spec -i t/*.js
@echo

@@ -4,0 +5,0 @@ compile:

{
"name": "weaver",
"version": "0.0.11",
"version": "0.1.0",
"description": "Interactive process management system",
"keywords": ["daemon", "process", "udp", "log", "sysadmin", "tools"],
"keywords": ["daemon", "process", "udp", "log", "tools"],
"main": "lib/weaver.js",

@@ -7,0 +7,0 @@ "license": "LGPL-3.0",

# Weaver
Interactive process management system for node.js
Interactive process management system for node.js

@@ -20,3 +20,3 @@ # Installation

weaver [--port <number>] [--config <path>] [--debug] [start]
weaver [--port <number>] [--config <path>] [--debug]
weaver [--port <number>] [--config <path>] upgrade

@@ -27,2 +27,3 @@ weaver [--port <number>] <restart|stop> [[task|pid], ...]

weaver [--port <number>] [--nocolor] dump
weaver [--port <number>] monitor
weaver [--port <number>] exit

@@ -39,2 +40,3 @@

- `dump` Show current weaver configuration
- `monitor` Show log messages from running weaver
- `exit` Stop all tasks and exit

@@ -132,4 +134,8 @@

In debug mode this functionality is disabled and logs are printed to stdout.
To do something with this logs you can simply say
To do something with this logs you can use monitor mode
weaver monitor
Or any other program capable to capture udp
socat udp4-listen:8092 stdout

@@ -139,3 +145,3 @@

Copyright 2012, 2013 Alexander Nazarov. All rights reserved.
Copyright 2012-2014 Alexander Nazarov. All rights reserved.

@@ -142,0 +148,0 @@ This program is free software: you can redistribute it and/or modify

'use strict';
var fork = require('child_process').spawn,
resolve = require('path').resolve,
assert = require('assert'),
Watcher = require('./watcher'),
tasks = Object.create(null),
Anubseran;
/*
* Task status codes
* R - restart
* E - error
* D - done (clean exit)
* W - work in progress
* S - stopped
*/
/**
* Task singleton (by name)
* @class Task
* @constructor
*/
function Task (name, options) {
var key, env;
if (!Anubseran) {
Anubseran = new require('./weaver')();
}
if (!(name in tasks)) {
tasks[name] = this;
options = options || {};
/**
* Subtasks count
* @property count
* @type Number
*/
this.count = options.count || 0;
/**
* Array with sub-process related data
* @property subtasks
* @private
* @type Array
*/
this.subtasks = [];
/**
* Source to execute in subtask
* @property source
* @type String
*/
this.source = options.source;
/**
* Source file is executable
* @property executable
* @type Boolean
*/
this.executable = options.executable || false;
/**
* Working directory for subtasks
* @property cwd
* @type String
*/
this.cwd = options.cwd || process.cwd();
/**
* Arguments for subtasks
* @property arguments
* @type Array
*/
this.arguments = options.arguments;
/**
* Timeout between SIGINT and SIGTERM for stop and restart
* @property timeout
* @type Number
* @default 1000
*/
this.timeout = options.timeout || 1000;
/**
* Minimal runtime required for persistent task
* to be restarted after unclean exit
* @property runtime
* @type Number
* @default 1000
*/
this.runtime = options.runtime || 1000;
/**
* Restart subtask on dirty exit
* @property persistent
* @type Boolean
* @default false
*/
this.persistent = options.persistent || false;
/**
* Environment variables for subtasks
* @property env
* @type Object
*/
env = this.env = options.env || {};
/* Default environment variables for subtasks */
if (!('HOME' in env)) env.HOME = true;
if (!('PATH' in env)) env.PATH = true;
/* NODE_PATH for node.js subtasks */
if (!('NODE_PATH' in env) && !this.executable) env.NODE_PATH = true;
for (key in env) {
if (env.hasOwnProperty(key)) {
/* Expand environment variables */
switch (env[key]) {
case true:
env[key] = process.env[key];
break;
case false:
delete env[key];
break;
}
}
}
/**
* Task name
* @property name
* @type String
*/
this.name = name;
/**
* Watch callback
* @property handler
* @private
* @type Function
*/
this.handler = restartall.bind(this);
/**
* Watched patterns
* @property watch
* @type Array
*/
this.watch = [];
}
if (!options) {
return tasks[name];
}
return tasks[name].upgrade(options);
}
Task.prototype = {
upgrade: upgrade,
kill: function (signal, pid) {
if (pid === undefined) {
killall.call(this, signal);
} else {
kill.call(this, get.call(this, pid), signal);
}
return this;
},
restart: function (pid) {
if (pid === undefined) {
restartall.call(this);
} else {
restart.call(this, get.call(this, pid));
}
return this;
},
stop: function (pid) {
if (pid === undefined) {
stopall.call(this);
} else {
stop.call(this, get.call(this, pid));
}
return this;
}
};
module.exports = Task;
function get (pid) {
var tasks = this.subtasks,
i, l;
for (i = 0, l = tasks.length; i < l; i++) {
if ((tasks[i] || {}).pid === pid) {
return tasks[i];
}
}
return null;
}
function kill (task, signal) {
if ((task || {}).pid) {
try {
task.process.kill(signal);
} catch (error) {
Anubseran.log('Failed to kill ' + task.pid + ' (' + task.name + ') with ' + signal);
}
}
return this;
}
function stop (task) {
if (task && task.pid) {
task.process.kill('SIGINT');
setTimeout(function () {
if (task.pid) {
task.process.kill('SIGTERM');
}
}, this.timeout);
}
}
function restart (task) {
if (task) {
task.status = 'R';
stop.call(this, task);
}
}
function _all (fn, arg) {
var tasks = this.subtasks,
task, i, l;
for (i = 0, l = tasks.length; i < l; i++) {
fn.call(this, tasks[i], arg);
}
}
function stopall () { _all.call(this, stop) }
function restartall () { _all.call(this, restart) }
function killall (signal) { _all.call(this, kill, signal) }
function spawn (id) {
var args = this.arguments || [],
binary = process.execPath,
subtask = {
id : id,
args : [],
status : 'W',
name : this.name,
time : Date.now()
}, i, l, p1, p2, eargs;
for (i = 0, l = args.length; i < l; i++) {
if (Array.isArray(args[i])) {
subtask.args.push(args[i][id]);
continue;
}
subtask.args.push(args[i]);
}
eargs = subtask.args.slice();
if (this.executable) {
binary = this.source;
} else {
eargs.unshift(this.source);
}
subtask.process = fork(binary, eargs, {
stdio : 'pipe',
cwd : resolve(this.cwd),
env : this.env
});
subtask.pid = subtask.process.pid;
subtask.process.stdout.on('data', logger.bind(Anubseran, subtask.pid + ' (' + subtask.name + ') '));
subtask.process.stderr.on('data', logger.bind(Anubseran, subtask.pid + ' [' + subtask.name + '] '));
subtask.process.once('exit', onExit.bind(this, subtask));
Anubseran.log('Task ' + subtask.pid + ' (' + subtask.name + ') spawned');
this.subtasks[id] = subtask;
}
function upgrade (parameters) {
var restart = false,
i, l, pid, subtask, key;
parameters = parameters || {};
for (key in parameters) {
if (parameters.hasOwnProperty(key)) {
switch (key) {
/* No restart needed */
case 'persistent':
case 'timeout':
case 'count':
case 'runtime':
this[key] = parameters[key];
break;
/* Restart required */
case 'source':
case 'executable':
case 'arguments':
case 'env':
case 'cwd':
try {
assert.deepEqual(this[key], parameters[key]);
} catch (change) {
this[key] = parameters[key];
restart = true;
}
break;
}
}
}
/* Full restart needed */
if (restart) {
Anubseran.log('Restart required for ' + this.name + ' task group');
this.restart();
}
/* Check watches */
if ('watch' in parameters) {
try {
assert.deepEqual(parameters.watch, this.watch);
} catch (change) {
this.watch = parameters.watch;
Watcher.stop(this.handler);
Watcher.start(this.cwd, this.watch, this.handler);
}
}
/* Check count */
for (i = 0, l = this.count; i < l; i++) {
subtask = this.subtasks[i];
if (!subtask || (subtask.status === 'R' && !subtask.pid)) {
spawn.call(this, i);
}
}
/* Kill redundant */
while (this.subtasks.length > this.count) {
stop.call(this, this.subtasks.pop());
}
return this;
}
function onExit (task, code, signal) {
var elapsed;
if (code === null) {
Anubseran.log('Task ' + task.pid + ' (' + task.name + ') was killed by ' + signal);
} else {
Anubseran.log('Task ' + task.pid + ' (' + task.name + ') exited with code ' + code);
}
task.pid = 0;
task.code = code;
task.signal = signal;
delete task.process;
if (task.status !== 'R') {
if (code) {
task.status = 'E';
} else if (signal) {
task.status = 'S';
} else {
task.status = 'D';
}
}
if (this.persistent && code) {
elapsed = Date.now() - task.time;
if (elapsed < this.runtime) {
Anubseran.log('Restart skipped after ' + elapsed + 'ms (' + task.name + ')');
return;
}
}
if (this.persistent || task.status === 'R') {
spawn.call(this, task.id);
}
}
function logger (prefix, data) {
var messages = data.toString().split('\n'),
i, l;
for (i = 0, l = messages.length; i < l; i++) {
if (!messages[i]) {
continue;
}
this.log.call(null, prefix + messages[i] + '\n');
}
}
assert = require 'assert'
Task = require '../lib/task.js'
sigint = './t/bin/sigint'
plain = './t/bin/plain'
(vows = require 'vows')
.describe('task')
.addBatch
constructor:
topic:
new Task 'constructor',
count: 1
source: sigint
timeout: 100
singleton: (task) ->
assert.equal task, new Task('constructor')
assert.notEqual task, new Task('_constructor')
assert.equal task.count, 1
assert.equal task.source, sigint
assert.isArray task.subtasks
properties: (task) ->
subtask = task.subtasks[0]
assert.isNumber subtask.pid
assert.isNumber subtask.time
assert.notEqual subtask.pid, 0
assert.equal subtask.id, 0
assert.equal subtask.name, 'constructor'
assert.equal subtask.status, 'W'
assert.isObject subtask.process
assert.isNotNull subtask.process
assert.isUndefined subtask.code
assert.isUndefined subtask.signal
assert.notEqual subtask.process, process
assert.equal subtask.pid, subtask.process.pid
assert.notEqual subtask.pid, process.pid
assert.equal task.subtasks.length, task.count
parameters:
topic:
new Task 'parameters',
count: 2
source: sigint
arguments: [[1000, 2000], '--test', ['--first', '--second']]
check: (task) ->
assert.equal task.subtasks.length, 2
assert.deepEqual task.subtasks[0].args, [1000, '--test', '--first']
assert.deepEqual task.subtasks[1].args, [2000, '--test', '--second']
.export module
assert = require 'assert'
Task = require '../lib/task.js'
sigint = './t/bin/sigint'
plain = './t/bin/plain'
_ = (name, fn) ->
return ->
fn(new Task name)
(vows = require 'vows')
.describe('stop')
.addBatch
'by pid':
topic: ->
task = new Task 21,
count: 2
source: plain
timeout: 50
task._pid1 = task.subtasks[0].pid
task._pid2 = task.subtasks[1].pid
setTimeout((->
task.stop(task._pid1)
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 21, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 21, (task) ->
assert.equal task.subtasks[0].pid, 0
assert.equal task.subtasks[1].pid, task._pid2
status: _ 21, (task) ->
assert.equal task.subtasks[0].status, 'E'
assert.equal task.subtasks[1].status, 'W'
identifiers: _ 21, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'all running':
topic: ->
task = new Task 22,
count: 2
source: plain
timeout: 100
setTimeout((->
task.stop()
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 22, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 22, (task) ->
assert.equal task.subtasks[1].pid, 0
assert.equal task.subtasks[0].pid, 0
status: _ 22, (task) ->
assert.equal task.subtasks[0].status, 'E'
assert.equal task.subtasks[1].status, 'E'
identifiers: _ 22, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'with sigterm':
topic: ->
task = new Task 23,
count: 1
source: sigint
timeout: 100
setTimeout((->
task.stop()
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 23, (task) ->
assert.equal task.subtasks.length, 1
processes: _ 23, (task) ->
assert.equal task.subtasks[0].pid, 0
status: _ 23, (task) ->
assert.equal task.subtasks[0].status, 'E'
identifiers: _ 23, (task) ->
assert.equal task.subtasks[0].id, 0
.export module
assert = require 'assert'
Task = require '../lib/task.js'
sigint = './t/bin/sigint'
plain = './t/bin/plain'
_ = (name, fn) ->
return ->
fn(new Task name)
(vows = require 'vows')
.describe('restart')
.addBatch
'by pid':
topic: ->
task = new Task 31,
count: 2
source: plain
timeout: 100
task._pid1 = task.subtasks[0].pid
task._pid2 = task.subtasks[1].pid
setTimeout((->
task.restart(task._pid1)
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 31, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 31, (task) ->
assert.notEqual task.subtasks[1].pid, 0
assert.notEqual task.subtasks[0].pid, 0
assert.notEqual task.subtasks[0].pid, task._pid1
assert.notEqual task.subtasks[0].pid, task._pid2
assert.equal task.subtasks[1].pid, task._pid2
status: _ 31, (task) ->
assert.equal task.subtasks[0].status, 'W'
assert.equal task.subtasks[1].status, 'W'
identifiers: _ 31, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'all running':
topic: ->
task = new Task 32,
count: 2
source: plain
timeout: 100
setTimeout((->
task.restart()
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 32, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 32, (task) ->
assert.notEqual task.subtasks[1].pid, 0
assert.notEqual task.subtasks[0].pid, 0
assert.notEqual task.subtasks[0].pid, task._pid1
assert.notEqual task.subtasks[0].pid, task._pid2
assert.notEqual task.subtasks[1].pid, task._pid1
assert.notEqual task.subtasks[1].pid, task._pid2
status: _ 32, (task) ->
assert.equal task.subtasks[0].status, 'W'
assert.equal task.subtasks[1].status, 'W'
identifiers: _ 32, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'with sigterm':
topic: ->
task = new Task 33,
count: 1
source: sigint
timeout: 100
task._pid1 = task.subtasks[0].pid
setTimeout((->
task.restart()
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 33, (task) ->
assert.equal task.subtasks.length, 1
processes: _ 33, (task) ->
assert.notEqual task.subtasks[0].pid, 0
assert.notEqual task.subtasks[0].pid, task._pid1
status: _ 33, (task) ->
assert.equal task.subtasks[0].status, 'W'
identifiers: _ 33, (task) ->
assert.equal task.subtasks[0].id, 0
.export module
assert = require 'assert'
Task = require '../lib/task.js'
sigint = './t/bin/sigint'
plain = './t/bin/plain'
_ = (name, fn) ->
return ->
fn(new Task name)
(vows = require 'vows')
.describe('upgrade')
.addBatch
'partial':
topic: ->
task = new Task 41,
count: 1
source: plain
timeout: 50
task._pid = task.subtasks[0].pid
setTimeout((->
task.upgrade
count: 2
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 41, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 41, (task) ->
assert.equal task.subtasks[0].pid, task._pid
assert.notEqual task.subtasks[1].pid, 0
assert.notEqual task.subtasks[1].pid, task._pid
status: _ 41, (task) ->
assert.equal task.subtasks[0].status, 'W'
assert.equal task.subtasks[1].status, 'W'
identifiers: _ 41, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'full':
topic: ->
task = new Task 42,
count: 2
source: plain
timeout: 50
task._pid1 = task.subtasks[0].pid
task._pid2 = task.subtasks[1].pid
setTimeout((->
task.upgrade
arguments: [1000]
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 42, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 42, (task) ->
assert.notEqual task.subtasks[0].pid, 0
assert.notEqual task.subtasks[1].pid, 0
assert.notEqual task.subtasks[0].pid, task._pid1
assert.notEqual task.subtasks[0].pid, task._pid2
assert.notEqual task.subtasks[1].pid, task._pid1
assert.notEqual task.subtasks[1].pid, task._pid2
status: _ 42, (task) ->
assert.equal task.subtasks[0].status, 'W'
assert.equal task.subtasks[1].status, 'W'
identifiers: _ 42, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
'with sigterm':
topic: ->
task = new Task 43,
count: 1
source: sigint
timeout: 100
task._pid1 = task.subtasks[0].pid
setTimeout((->
task.upgrade
arguments: [1000]
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 43, (task) ->
assert.equal task.subtasks.length, 1
processes: _ 43, (task) ->
assert.notEqual task.subtasks[0].pid, 0
assert.notEqual task.subtasks[0].pid, task._pid1
status: _ 43, (task) ->
assert.equal task.subtasks[0].status, 'W'
identifiers: _ 43, (task) ->
assert.equal task.subtasks[0].id, 0
.export module
assert = require 'assert'
Task = require '../lib/task.js'
sigint = './t/bin/sigint'
plain = './t/bin/plain'
_ = (name, fn) ->
return ->
fn(new Task name)
(vows = require 'vows')
.describe('kill')
.addBatch
'by pid':
topic: ->
task = new Task 51,
count: 2
source: plain
timeout: 50
task._pid1 = task.subtasks[0].pid
task._pid2 = task.subtasks[1].pid
setTimeout((->
task.kill('SIGKILL', task._pid2)
), 50)
setTimeout((=>
@callback()
), 200)
undefined
count: _ 51, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 51, (task) ->
assert.equal task.subtasks[1].pid, 0
assert.equal task.subtasks[0].pid, task._pid1
status: _ 51, (task) ->
assert.equal task.subtasks[1].status, 'S'
assert.equal task.subtasks[0].status, 'W'
identifiers: _ 51, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
signal: _ 51, (task) ->
assert.isNull task.subtasks[1].code
assert.equal task.subtasks[1].signal, 'SIGKILL'
'all running':
topic: ->
task = new Task 52,
count: 2
source: plain
timeout: 50
setTimeout((->
task.kill('SIGKILL')
), 100)
setTimeout((=>
@callback()
), 500)
undefined
count: _ 52, (task) ->
assert.equal task.subtasks.length, 2
processes: _ 52, (task) ->
assert.equal task.subtasks[1].pid, 0
assert.equal task.subtasks[0].pid, 0
status: _ 52, (task) ->
assert.equal task.subtasks[0].status, 'S'
assert.equal task.subtasks[1].status, 'S'
identifiers: _ 52, (task) ->
assert.equal task.subtasks[0].id, 0
assert.equal task.subtasks[1].id, 1
signal: _ 52, (task) ->
assert.isNull task.subtasks[0].code
assert.equal task.subtasks[0].signal, 'SIGKILL'
assert.isNull task.subtasks[1].code
assert.equal task.subtasks[1].signal, 'SIGKILL'
.export module

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet