+51
| # 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) |
+105
| 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) |
+105
| 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(); |
+816
-304
| '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 }); |
+2
-1
| test: compile | ||
| vows --tap -i t/*.js | ||
| vows --spec -i t/*.js | ||
| @echo | ||
@@ -4,0 +5,0 @@ compile: |
+2
-2
| { | ||
| "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", |
+10
-4
| # 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 |
-420
| '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 |
-109
| 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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
72796
43.24%24
33.33%886
13.44%147
4.26%6
100%2
100%