UNPKG

webworker

Version:

An implementation of the HTML5 Web Worker API

298 lines (247 loc) 8.98 kB
// WebWorkers implementation. // // The master and workers communite over a UNIX domain socket at // // /tmp/node-webworker-<master PID>.sock // // This socket is used as a full-duplex channel for exchanging messages. // Messages are objects encoded using the MessagePack format. Each message // being exchanged is wrapped in an array envelope, the first element of // which indicates the type of message being sent. For example take the // following message (expresed in JSON) // // [999, {'foo' : 'bar'}] // // This represents a message of type 999 with an object payload. // // o Message Types // // MSGTYPE_NOOP No-op. The payload of this message is discarded. // // MSGTYPE_ERROR An error has occurred. Used for bubbling up // error events from the child process. // // MSGTYPE_CLOSE Graceful shut-down. Used to request that the // child terminate gracefully. // // MSGTYPE_USER A user-specified message. All messages sent // via the WebWorker API generate this type of // message. var assert = require('assert'); var child_process = require('child_process'); var fs = require('fs'); var net = require('net'); var netBinding = process.binding('net'); var path = require('path'); var sys = require('sys'); var wwutil = require('./webworker-util'); var WebSocket = require('./websocket-client').WebSocket; var WebSocketServer = require('./ws').Server; // Directory for our UNIX domain sockets var SOCK_DIR_PATH = '/tmp/node-webworker-' + process.pid; // The number of workers created so far var numWorkersCreated = 0; // A Web Worker // // Each worker communicates with the master over a UNIX domain socket rooted // in SOCK_DIR_PATH. var Worker = function(src, opts) { var self = this; opts = opts || {}; // The timeout ID for killing off this worker if it is unresponsive to a // graceful shutdown request var killTimeoutID = undefined; // Process ID of child process running this worker // // This value persists even once the child process itself has // terminated; it is used as a key into datastructures managed by the // Master object. var pid = undefined; // Child process object // // This value is 'undefined' until the child process itself is spawned // and defined forever after. var cp = undefined; // The stream associated with this worker and wwutil.MsgStream that // wraps it. var stream = undefined; var msgStream = undefined; // Outbound message queue // // This queue is only written to when we don't yet have a stream to // talk to the worker. It contains [type, data, fd] tuples. var msgQueue = []; // The path to our socket var sockPath = path.join(SOCK_DIR_PATH, '' + numWorkersCreated++); // Server instance for our communication socket with the child process // // Doesn't begin listening until start() is called. var wsSrv = new WebSocketServer(); wsSrv.addListener('connection', function(s) { assert.equal(stream, undefined); assert.equal(msgStream, undefined); stream = s._req.socket; msgStream = new wwutil.MsgStream(s); // Process any messages waiting to be sent msgQueue.forEach(function(m) { var fd = m.pop(); msgStream.send(m, fd); }); msgQueue = []; // Process incoming messages with handleMessage() msgStream.addListener('msg', handleMessage); }); // Begin worker execution // // First fires up the UNIX socket server, then spawns the child process // and away we go. var start = function() { wsSrv.addListener('listening', function() { var execPath = opts.path || process.execPath || process.argv[0]; var args = [ path.join(__dirname, 'webworker-child.js'), sockPath, 'file://' + src ]; if (opts.args) { if (Array.isArray(opts.args)) { for (var ii = opts.args.length; ii >= 0; ii--) { args.splice(0, 0, opts.args[ii]); } } else { args.splice(0, 0, opts.args.toString()); } } cp = child_process.spawn( execPath, args, undefined, [0, 1, 2] ); // Save off the PID of the child process, as this value gets // undefined once the process exits. pid = cp.pid; wwutil.debug(1, 'Spawned process ' + pid + ' for worker \'' + src + '\': ' + execPath + ' ' + args.join(' ') ); cp.addListener('exit', function(code, signal) { wwutil.debug( 'Process ' + pid + ' for worker \'' + src + '\' exited with status ' + code +', signal ' + signal ); // If we have an outstanding timeout for killing off this process, // abort it. if (killTimeoutID) { clearTimeout(killTimeoutID); } if (stream) { stream.destroy(); } else { wwutil.debug( 'Process ' + pid + ' exited without completing handshaking' ); } wsSrv.close(); if (self.onexit) { process.nextTick(function() { self.onexit(code, signal); }); } }); }); wsSrv.listen(sockPath); }; // The primary message handling function for the worker. // // This is only invoked after handshaking has occurred. var handleMessage = function(msg, fd) { if (!wwutil.isValidMessage(msg)) { wwutil.debug('Received invalid message: ' + sys.inspect(msg)); return; } wwutil.debug( 'Received message type=' + msg[0] + ', data=' + sys.inspect(msg[1]) ); switch (msg[0]) { case wwutil.MSGTYPE_NOOP: break; case wwutil.MSGTYPE_ERROR: if (self.onerror) { self.onerror(msg[1]); } break; case wwutil.MSGTYPE_USER: if (self.onmessage) { e = { data : msg[1] }; if (fd) { e.fd = fd; } self.onmessage(e); } break; default: wwutil.debug( 'Received unexpected message: ' + sys.inspect(msg) ); break; } }; // Do the heavy lifting of posting a message var postMessageImpl = function(msgType, msg, fd) { assert.ok(msgQueue.length == 0 || !msgStream); var m = [msgType, msg]; if (msgStream) { msgStream.send(m, fd); } else { m.push(fd); msgQueue.push(m); } }; // Post a message to the worker self.postMessage = function(msg, fd) { postMessageImpl(wwutil.MSGTYPE_USER, msg, fd); }; // Terminate the worker // // Takes a timeout value for forcibly killing off the worker if it does // not shut down gracefully on its own. By default, this timeout is // 5 seconds. A value of 0 indicates infinite timeout. self.terminate = function(timeout) { assert.notEqual(pid, undefined); assert.ok(cp.pid == pid || !cp.pid); timeout = (timeout === undefined) ? 5000 : timeout; // The child process is already shut down; no-op if (!cp.pid) { return; } // The termination process has already been initiated for this // process if (killTimeoutID) { return; } // Request graceful shutdown of the child process postMessageImpl(wwutil.MSGTYPE_CLOSE); // Optionally set a timer to kill off the child process forcefully if // it has not shut down by itself. if (timeout > 0) { killTimeoutID = setTimeout(function() { // Clear our ID since we're now running killTimeoutID = undefined; if (!cp.pid) { return; } wwutil.debug( 'Forcibily terminating worker process ' + pid + ' with SIGTERM' ); cp.kill('SIGTERM'); }, timeout); } }; // Fire it up start(); }; exports.Worker = Worker; // Perform any one-time initialization fs.mkdirSync(SOCK_DIR_PATH, 0700);