diff --git a/benchmark/http_simple.js b/benchmark/http_simple.js index 9025e5d9406..40515779928 100644 --- a/benchmark/http_simple.js +++ b/benchmark/http_simple.js @@ -1,8 +1,5 @@ path = require("path"); -libDir = path.join(path.dirname(__filename), "../lib"); -require.paths.unshift(libDir); - var puts = require("sys").puts; http = require("http"); diff --git a/doc/api.txt b/doc/api.txt index 0885b679971..e6d43ecaf5b 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -192,23 +192,6 @@ The default is to only recurse twice. To make it recurse indefinitely, pass in +null+ for +depth+. -+exec(command, callback)+:: -Executes the command as a child process, buffers the output and returns it -in a callback. -+ ----------------------------------------- -var sys = require("sys"); -sys.exec("ls /", function (err, stdout, stderr) { - if (err) throw err; - sys.puts(stdout); -}); ----------------------------------------- -+ -The callback gets the arguments +(err, stdout, stderr)+. On success +err+ -will be +null+. On error +err+ will be an instance of +Error+ and +err.code+ -will be the exit code of the child process. - - == Events Many objects in Node emit events: a TCP server emits an event each time @@ -399,25 +382,18 @@ Stops a interval from triggering. == Child Processes Node provides a tridirectional +popen(3)+ facility through the class -+process.ChildProcess+. It is possible to stream data through the child's +stdin+, -+stdout+, and +stderr+ in a fully non-blocking way. ++ChildProcess+ class. It is possible to stream data through the child's ++stdin+, +stdout+, and +stderr+ in a fully non-blocking way. + +To create a child process use +require("child_process").spawn()+. + +Child processes always have three streams associated with them. ++child.stdin+, +child.stdout+, and +child.stderr+. -=== +process.ChildProcess+ [cols="1,2,10",options="header"] |========================================================= | Event | Parameters |Notes - -| +"output"+ | +data+ | Each time the child process - sends data to its +stdout+, this event is - emitted. +data+ is a string. If the child - process closes its +stdout+ stream (a common - thing to do on exit), this event will be emitted - with +data === null+. - -| +"error"+ | +data+ | Identical to the +"output"+ event except for - +stderr+ instead of +stdout+. - | +"exit"+ | +code+ | This event is emitted after the child process ends. +code+ is the final exit code of the process. One can be assured that after this @@ -425,19 +401,18 @@ Node provides a tridirectional +popen(3)+ facility through the class +"error"+ callbacks will no longer be made. |========================================================= -+process.createChildProcess(command, args=[], env=process.env)+:: ++require("child_process").spawn(command, args=[], env=process.env)+:: Launches a new process with the given +command+, command line arguments, and environmental variables. For example: + ---------------------------------------- -var ls = process.createChildProcess("ls", ["-lh", "/usr"]); -ls.addListener("output", function (data) { - sys.puts(data); +// Pipe a child process output to +// parent process output +var ls = spawn("ls", ["-lh", "/usr"]); +ls.stdout.addListener("data", function (data) { + process.stdout.write(data); }); ---------------------------------------- -+ -Note, if you just want to buffer the output of a command and return it, then -+exec()+ in +/sys.js+ might be better. +child.pid+ :: @@ -459,6 +434,23 @@ Send a signal to the child process. If no argument is given, the process will be sent +"SIGTERM"+. See signal(7) for a list of available signals. ++require("child_process").exec(command, callback)+:: +High-level way to executes a command as a child process and buffer the +output and return it in a callback. ++ +---------------------------------------- +var exec = require("child_process").exec; +exec("ls /", function (err, stdout, stderr) { + if (err) throw err; + sys.puts(stdout); +}); +---------------------------------------- ++ +The callback gets the arguments +(err, stdout, stderr)+. On success +err+ +will be +null+. On error +err+ will be an instance of +Error+ and +err.code+ +will be the exit code of the child process. + + == File System diff --git a/lib/buffer.js b/lib/buffer.js new file mode 100644 index 00000000000..6b570eed049 --- /dev/null +++ b/lib/buffer.js @@ -0,0 +1,20 @@ +var Buffer = process.binding('buffer').Buffer; + +exports.Buffer = Buffer; + +Buffer.prototype.toString = function () { + return this.utf8Slice(0, this.length); +}; + +Buffer.prototype.toJSON = function () { + return this.utf8Slice(0, this.length); + /* + var s = ""; + for (var i = 0; i < this.length; i++) { + s += this[i].toString(16) + " "; + } + return s; + */ +}; + + diff --git a/lib/child_process.js b/lib/child_process.js new file mode 100644 index 00000000000..db452110d85 --- /dev/null +++ b/lib/child_process.js @@ -0,0 +1,100 @@ +var inherits = require('sys').inherits; +var EventEmitter = require('events').EventEmitter; +var Stream = require('net').Stream; +var InternalChildProcess = process.binding('child_process').ChildProcess; + + +var spawn = exports.spawn = function (path, args, env) { + var child = new ChildProcess(); + child.spawn(path, args, env); + return child; +}; + + +exports.exec = function (command, callback) { + var child = spawn("/bin/sh", ["-c", command]); + var stdout = ""; + var stderr = ""; + + child.stdout.setEncoding('utf8'); + child.stdout.addListener("data", function (chunk) { stdout += chunk; }); + + child.stderr.setEncoding('utf8'); + child.stderr.addListener("data", function (chunk) { stderr += chunk; }); + + child.addListener("exit", function (code) { + if (code == 0) { + if (callback) callback(null, stdout, stderr); + } else { + var e = new Error("Command failed: " + stderr); + e.code = code; + if (callback) callback(e, stdout, stderr); + } + }); +}; + + +function ChildProcess () { + process.EventEmitter.call(this); + + var self = this; + + var gotCHLD = false; + var exitCode; + var internal = this._internal = new InternalChildProcess(); + + var stdin = this.stdin = new Stream(); + var stdout = this.stdout = new Stream(); + var stderr = this.stderr = new Stream(); + + stderr.onend = stdout.onend = function () { + if (gotCHLD && !stdout.readable && !stderr.readable) { + self.emit('exit', exitCode); + } + }; + + internal.onexit = function (code) { + gotCHLD = true; + exitCode = code; + if (!stdout.readable && !stderr.readable) { + self.emit('exit', exitCode); + } + }; + + this.__defineGetter__('pid', function () { return internal.pid; }); +} +inherits(ChildProcess, EventEmitter); + + +ChildProcess.prototype.kill = function (sig) { + return this._internal.kill(sig); +}; + + +ChildProcess.prototype.spawn = function (path, args, env) { + args = args || []; + env = env || process.env; + var envPairs = []; + for (var key in env) { + if (env.hasOwnProperty(key)) { + envPairs.push(key + "=" + env[key]); + } + } + + var fds = this._internal.spawn(path, args, envPairs); + + this.stdin.open(fds[0]); + this.stdin.writable = true; + this.stdin.readable = false; + + this.stdout.open(fds[1]); + this.stdout.writable = false; + this.stdout.readable = true; + this.stdout.resume(); + + this.stderr.open(fds[2]); + this.stderr.writable = false; + this.stderr.readable = true; + this.stderr.resume(); +}; + diff --git a/lib/http.js b/lib/http.js index 832df6b2cb2..5ec1c6e984d 100644 --- a/lib/http.js +++ b/lib/http.js @@ -1,11 +1,122 @@ +var debugLevel = 0; +if ("NODE_DEBUG" in process.env) debugLevel = 1; + +function debug (x) { + if (debugLevel > 0) { + process.binding('stdio').writeError(x + "\n"); + } +} + var sys = require('sys'); +var net = require('net'); var events = require('events'); -// FIXME: The TCP binding isn't actually used here, but it needs to be -// loaded before the http binding. -process.binding('tcp'); +var HTTPParser = process.binding('http_parser').HTTPParser; + +var parserFreeList = []; + +function newParser (type) { + var parser; + if (parserFreeList.length) { + parser = parserFreeList.shift(); + parser.reinitialize(type); + } else { + parser = new HTTPParser(type); + + parser.onMessageBegin = function () { + parser.incoming = new IncomingMessage(parser.socket); + parser.field = null; + parser.value = null; + }; + + // Only servers will get URL events. + parser.onURL = function (b, start, len) { + var slice = b.asciiSlice(start, start+len); + if (parser.incoming.url) { + parser.incoming.url += slice; + } else { + // Almost always will branch here. + parser.incoming.url = slice; + } + }; + + parser.onHeaderField = function (b, start, len) { + var slice = b.asciiSlice(start, start+len).toLowerCase(); + if (parser.value) { + parser.incoming._addHeaderLine(parser.field, parser.value); + parser.field = null; + parser.value = null; + } + if (parser.field) { + parser.field += slice; + } else { + parser.field = slice; + } + }; + + parser.onHeaderValue = function (b, start, len) { + var slice = b.asciiSlice(start, start+len); + if (parser.value) { + parser.value += slice; + } else { + parser.value = slice; + } + }; + + parser.onHeadersComplete = function (info) { + if (parser.field && parser.value) { + parser.incoming._addHeaderLine(parser.field, parser.value); + } + + parser.incoming.httpVersionMajor = info.versionMajor; + parser.incoming.httpVersionMinor = info.versionMinor; + + if (info.method) { + // server only + parser.incoming.method = info.method; + } else { + // client only + parser.incoming.statusCode = info.statusCode; + } + + parser.onIncoming(parser.incoming, info.shouldKeepAlive); + }; + + parser.onBody = function (b, start, len) { + // TODO body encoding? + var enc = parser.incoming._encoding; + if (!enc) { + parser.incoming.emit('data', b.slice(start, start+len)); + } else { + var string; + switch (enc) { + case 'utf8': + string = b.utf8Slice(start, start+len); + break; + case 'ascii': + string = b.asciiSlice(start, start+len); + break; + case 'binary': + string = b.binarySlice(start, start+len); + break; + default: + throw new Error('Unsupported encoding ' + enc + '. Use Buffer'); + } + parser.incoming.emit('data', string); + } + }; + + parser.onMessageComplete = function () { + parser.incoming.emit("end"); + }; + } + return parser; +} + +function freeParser (parser) { + if (parserFreeList.length < 1000) parserFreeList.push(parser); +} -var http = process.binding('http'); var CRLF = "\r\n"; var STATUS_CODES = exports.STATUS_CODES = { @@ -56,10 +167,10 @@ var content_length_expression = /Content-Length/i; /* Abstract base class for ServerRequest and ClientResponse. */ -function IncomingMessage (connection) { +function IncomingMessage (socket) { events.EventEmitter.call(this); - this.connection = connection; + this.socket = socket; this.httpVersion = null; this.headers = {}; @@ -70,7 +181,7 @@ function IncomingMessage (connection) { // response (client) only this.statusCode = null; - this.client = this.connection; + this.client = this.socket; } sys.inherits(IncomingMessage, events.EventEmitter); exports.IncomingMessage = IncomingMessage; @@ -80,16 +191,21 @@ IncomingMessage.prototype._parseQueryString = function () { }; IncomingMessage.prototype.setBodyEncoding = function (enc) { - // TODO: Find a cleaner way of doing this. - this.connection.setEncoding(enc); + // TODO deprecation message? + this.setEncoding(enc); +}; + +IncomingMessage.prototype.setEncoding = function (enc) { + // TODO check values, error out on bad, and deprecation message? + this._encoding = enc.toLowerCase(); }; IncomingMessage.prototype.pause = function () { - this.connection.pause(); + this.socket.pause(); }; IncomingMessage.prototype.resume = function () { - this.connection.resume(); + this.socket.resume(); }; IncomingMessage.prototype._addHeaderLine = function (field, value) { @@ -102,10 +218,10 @@ IncomingMessage.prototype._addHeaderLine = function (field, value) { } }; -function OutgoingMessage (connection) { - events.EventEmitter.call(this, connection); +function OutgoingMessage (socket) { + events.EventEmitter.call(this, socket); - this.connection = connection; + this.socket = socket; this.output = []; this.outputEncodings = []; @@ -126,7 +242,7 @@ exports.OutgoingMessage = OutgoingMessage; OutgoingMessage.prototype._send = function (data, encoding) { var length = this.output.length; - if (length === 0) { + if (length === 0 || typeof data != 'string') { this.output.push(data); encoding = encoding || "ascii"; this.outputEncodings.push(encoding); @@ -138,11 +254,7 @@ OutgoingMessage.prototype._send = function (data, encoding) { if ((lastEncoding === encoding) || (!encoding && data.constructor === lastData.constructor)) { - if (lastData.constructor === String) { - this.output[length-1] = lastData + data; - } else { - this.output[length-1] = lastData.concat(data); - } + this.output[length-1] = lastData + data; return; } @@ -228,7 +340,11 @@ OutgoingMessage.prototype.write = function (chunk, encoding) { encoding = encoding || "ascii"; if (this.chunked_encoding) { - this._send(process._byteLength(chunk, encoding).toString(16)); + if (typeof chunk == 'string') { + this._send(process._byteLength(chunk, encoding).toString(16)); + } else { + this._send(chunk.length.toString(16)); + } this._send(CRLF); this._send(chunk, encoding); this._send(CRLF); @@ -259,7 +375,7 @@ OutgoingMessage.prototype.close = function () { function ServerResponse (req) { - OutgoingMessage.call(this, req.connection); + OutgoingMessage.call(this, req.socket); if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { this.use_chunked_encoding_by_default = false; @@ -297,8 +413,8 @@ ServerResponse.prototype.writeHead = function (statusCode) { ServerResponse.prototype.sendHeader = ServerResponse.prototype.writeHead; ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; -function ClientRequest (connection, method, url, headers) { - OutgoingMessage.call(this, connection); +function ClientRequest (socket, method, url, headers) { + OutgoingMessage.call(this, socket); this.should_keep_alive = false; if (method === "GET" || method === "HEAD") { @@ -330,85 +446,19 @@ ClientRequest.prototype.close = function () { }; -function createIncomingMessageStream (connection, incoming_listener) { - var incoming, field, value; - - connection.addListener("messageBegin", function () { - incoming = new IncomingMessage(connection); - field = null; - value = null; - }); - - // Only servers will get URL events. - connection.addListener("url", function (data) { - incoming.url += data; - }); - - connection.addListener("headerField", function (data) { - if (value) { - incoming._addHeaderLine(field, value); - field = null; - value = null; - } - if (field) { - field += data; - } else { - field = data; - } - }); - - connection.addListener("headerValue", function (data) { - if (value) { - value += data; - } else { - value = data; - } - }); - - connection.addListener("headerComplete", function (info) { - if (field && value) { - incoming._addHeaderLine(field, value); - } - - incoming.httpVersion = info.httpVersion; - incoming.httpVersionMajor = info.versionMajor; - incoming.httpVersionMinor = info.versionMinor; - - if (info.method) { - // server only - incoming.method = info.method; - } else { - // client only - incoming.statusCode = info.statusCode; - } - - incoming_listener(incoming, info.should_keep_alive); - }); - - connection.addListener("body", function (chunk) { - incoming.emit('data', chunk); - }); - - connection.addListener("messageComplete", function () { - incoming.emit('end'); - }); -} - -/* Returns true if the message queue is finished and the connection +/* Returns true if the message queue is finished and the socket * should be closed. */ -function flushMessageQueue (connection, queue) { +function flushMessageQueue (socket, queue) { while (queue[0]) { var message = queue[0]; while (message.output.length > 0) { - if (connection.readyState !== "open" && connection.readyState !== "writeOnly") { - return true; - } + if (!socket.writable) return true; var data = message.output.shift(); var encoding = message.outputEncodings.shift(); - connection.write(data, encoding); + socket.write(data, encoding); } if (!message.finished) break; @@ -422,152 +472,173 @@ function flushMessageQueue (connection, queue) { } -exports.createServer = function (requestListener, options) { - var server = new http.Server(); - //server.setOptions(options); - server.addListener("request", requestListener); - server.addListener("connection", connectionListener); - return server; +function Server (requestListener) { + net.Server.call(this); + this.addListener("request", requestListener); + this.addListener("connection", connectionListener); +} +sys.inherits(Server, net.Server); + +exports.Server = Server; + +exports.createServer = function (requestListener) { + return new Server(requestListener); }; -function connectionListener (connection) { - // An array of responses for each connection. In pipelined connections +function connectionListener (socket) { + var self = this; + // An array of responses for each socket. In pipelined connections // we need to keep track of the order they were sent. var responses = []; - connection.resetParser(); + var parser = newParser('request'); + + socket.ondata = function (d, start, end) { + parser.execute(d, start, end - start); + }; + + socket.onend = function () { + parser.finish(); + // unref the parser for easy gc + freeParser(parser); - // is this really needed? - connection.addListener("end", function () { if (responses.length == 0) { - connection.close(); + socket.close(); } else { responses[responses.length-1].closeOnFinish = true; } - }); + }; - - createIncomingMessageStream(connection, function (incoming, should_keep_alive) { + parser.socket = socket; + // The following callback is issued after the headers have been read on a + // new message. In this callback we setup the response object and pass it + // to the user. + parser.onIncoming = function (incoming, shouldKeepAlive) { var req = incoming; - var res = new ServerResponse(req); - res.should_keep_alive = should_keep_alive; - res.addListener("flush", function () { - if (flushMessageQueue(connection, responses)) { - connection.close(); + + res.shouldKeepAlive = shouldKeepAlive; + res.addListener('flush', function () { + if (flushMessageQueue(socket, responses)) { + socket.close(); } }); responses.push(res); - connection.server.emit("request", req, res); - }); + self.emit('request', req, res); + }; } -exports.createClient = function (port, host) { - var client = new http.Client(); - var secure_credentials={ secure : false }; +function Client ( ) { + net.Stream.call(this); + + var self = this; var requests = []; var currentRequest; - client.tcpSetSecure = client.setSecure; - client.setSecure = function(format_type, ca_certs, crl_list, private_key, certificate) { - secure_credentials.secure = true; - secure_credentials.format_type = format_type; - secure_credentials.ca_certs = ca_certs; - secure_credentials.crl_list = crl_list; - secure_credentials.private_key = private_key; - secure_credentials.certificate = certificate; - } + var parser = newParser('response'); + parser.socket = this; - client._reconnect = function () { - if (client.readyState != "opening") { - //sys.debug("HTTP CLIENT: reconnecting readyState = " + client.readyState); - client.connect(port, host); - if (secure_credentials.secure) { - client.tcpSetSecure(secure_credentials.format_type, - secure_credentials.ca_certs, - secure_credentials.crl_list, - secure_credentials.private_key, - secure_credentials.certificate); - } + self._reconnect = function () { + if (self.readyState != "opening") { + debug("HTTP CLIENT: reconnecting readyState = " + self.readyState); + self.connect(self.port, self.host); } }; - client._pushRequest = function (req) { + self._pushRequest = function (req) { req.addListener("flush", function () { - if (client.readyState == "closed") { - //sys.debug("HTTP CLIENT request flush. reconnect. readyState = " + client.readyState); - client._reconnect(); + if (self.readyState == "closed") { + debug("HTTP CLIENT request flush. reconnect. readyState = " + self.readyState); + self._reconnect(); return; } - //sys.debug("client flush readyState = " + client.readyState); - if (req == currentRequest) flushMessageQueue(client, [req]); + + debug("self flush readyState = " + self.readyState); + if (req == currentRequest) flushMessageQueue(self, [req]); }); requests.push(req); }; - client.addListener("connect", function () { - client.resetParser(); - currentRequest = requests.shift(); + this.ondata = function (d, start, end) { + parser.execute(d, start, end - start); + }; + + self.addListener("connect", function () { + parser.reinitialize('response'); + sys.puts('requests: ' + sys.inspect(requests)); + currentRequest = requests.shift() currentRequest.flush(); }); - client.addListener("end", function () { - //sys.debug("client got end closing. readyState = " + client.readyState); - client.close(); + self.addListener("end", function () { + parser.finish(); + freeParser(parser); + + debug("self got end closing. readyState = " + self.readyState); + self.close(); }); - client.addListener("close", function (had_error) { + self.addListener("close", function (had_error) { if (had_error) { - client.emit("error"); + self.emit("error"); return; } - //sys.debug("HTTP CLIENT onClose. readyState = " + client.readyState); + debug("HTTP CLIENT onClose. readyState = " + self.readyState); // If there are more requests to handle, reconnect. if (requests.length > 0) { - client._reconnect(); + self._reconnect(); } }); - createIncomingMessageStream(client, function (res) { - //sys.debug("incoming response!"); + parser.onIncoming = function (res) { + debug("incoming response!"); res.addListener('end', function ( ) { - //sys.debug("request complete disconnecting. readyState = " + client.readyState); - client.close(); + debug("request complete disconnecting. readyState = " + self.readyState); + self.close(); }); currentRequest.emit("response", res); - }); - - return client; + }; }; +sys.inherits(Client, net.Stream); -http.Client.prototype.get = function () { +exports.Client = Client; + +exports.createClient = function (port, host) { + var c = new Client; + c.port = port; + c.host = host; + return c; +} + + +Client.prototype.get = function () { throw new Error("client.get(...) is now client.request('GET', ...)"); }; -http.Client.prototype.head = function () { +Client.prototype.head = function () { throw new Error("client.head(...) is now client.request('HEAD', ...)"); }; -http.Client.prototype.post = function () { +Client.prototype.post = function () { throw new Error("client.post(...) is now client.request('POST', ...)"); }; -http.Client.prototype.del = function () { +Client.prototype.del = function () { throw new Error("client.del(...) is now client.request('DELETE', ...)"); }; -http.Client.prototype.put = function () { +Client.prototype.put = function () { throw new Error("client.put(...) is now client.request('PUT', ...)"); }; -http.Client.prototype.request = function (method, url, headers) { +Client.prototype.request = function (method, url, headers) { if (typeof(url) != "string") { // assume method was omitted, shift arguments headers = url; url = method; @@ -580,7 +651,7 @@ http.Client.prototype.request = function (method, url, headers) { exports.cat = function (url, encoding_, headers_) { - var encoding = 'utf8', + var encoding = 'utf8', headers = {}, callback = null; diff --git a/lib/http_old.js b/lib/http_old.js new file mode 100644 index 00000000000..832df6b2cb2 --- /dev/null +++ b/lib/http_old.js @@ -0,0 +1,641 @@ +var sys = require('sys'); +var events = require('events'); + +// FIXME: The TCP binding isn't actually used here, but it needs to be +// loaded before the http binding. +process.binding('tcp'); + +var http = process.binding('http'); + +var CRLF = "\r\n"; +var STATUS_CODES = exports.STATUS_CODES = { + 100 : 'Continue', + 101 : 'Switching Protocols', + 200 : 'OK', + 201 : 'Created', + 202 : 'Accepted', + 203 : 'Non-Authoritative Information', + 204 : 'No Content', + 205 : 'Reset Content', + 206 : 'Partial Content', + 300 : 'Multiple Choices', + 301 : 'Moved Permanently', + 302 : 'Moved Temporarily', + 303 : 'See Other', + 304 : 'Not Modified', + 305 : 'Use Proxy', + 400 : 'Bad Request', + 401 : 'Unauthorized', + 402 : 'Payment Required', + 403 : 'Forbidden', + 404 : 'Not Found', + 405 : 'Method Not Allowed', + 406 : 'Not Acceptable', + 407 : 'Proxy Authentication Required', + 408 : 'Request Time-out', + 409 : 'Conflict', + 410 : 'Gone', + 411 : 'Length Required', + 412 : 'Precondition Failed', + 413 : 'Request Entity Too Large', + 414 : 'Request-URI Too Large', + 415 : 'Unsupported Media Type', + 500 : 'Internal Server Error', + 501 : 'Not Implemented', + 502 : 'Bad Gateway', + 503 : 'Service Unavailable', + 504 : 'Gateway Time-out', + 505 : 'HTTP Version not supported' +}; + +var connection_expression = /Connection/i; +var transfer_encoding_expression = /Transfer-Encoding/i; +var close_expression = /close/i; +var chunk_expression = /chunk/i; +var content_length_expression = /Content-Length/i; + + +/* Abstract base class for ServerRequest and ClientResponse. */ +function IncomingMessage (connection) { + events.EventEmitter.call(this); + + this.connection = connection; + this.httpVersion = null; + this.headers = {}; + + // request (server) only + this.url = ""; + + this.method = null; + + // response (client) only + this.statusCode = null; + this.client = this.connection; +} +sys.inherits(IncomingMessage, events.EventEmitter); +exports.IncomingMessage = IncomingMessage; + +IncomingMessage.prototype._parseQueryString = function () { + throw new Error("_parseQueryString is deprecated. Use require(\"querystring\") to parse query strings.\n"); +}; + +IncomingMessage.prototype.setBodyEncoding = function (enc) { + // TODO: Find a cleaner way of doing this. + this.connection.setEncoding(enc); +}; + +IncomingMessage.prototype.pause = function () { + this.connection.pause(); +}; + +IncomingMessage.prototype.resume = function () { + this.connection.resume(); +}; + +IncomingMessage.prototype._addHeaderLine = function (field, value) { + if (field in this.headers) { + // TODO Certain headers like 'Content-Type' should not be concatinated. + // See https://www.google.com/reader/view/?tab=my#overview-page + this.headers[field] += ", " + value; + } else { + this.headers[field] = value; + } +}; + +function OutgoingMessage (connection) { + events.EventEmitter.call(this, connection); + + this.connection = connection; + + this.output = []; + this.outputEncodings = []; + + this.closeOnFinish = false; + this.chunked_encoding = false; + this.should_keep_alive = true; + this.use_chunked_encoding_by_default = true; + + this.flushing = false; + this.headWritten = false; + + this.finished = false; +} +sys.inherits(OutgoingMessage, events.EventEmitter); +exports.OutgoingMessage = OutgoingMessage; + +OutgoingMessage.prototype._send = function (data, encoding) { + var length = this.output.length; + + if (length === 0) { + this.output.push(data); + encoding = encoding || "ascii"; + this.outputEncodings.push(encoding); + return; + } + + var lastEncoding = this.outputEncodings[length-1]; + var lastData = this.output[length-1]; + + if ((lastEncoding === encoding) || + (!encoding && data.constructor === lastData.constructor)) { + if (lastData.constructor === String) { + this.output[length-1] = lastData + data; + } else { + this.output[length-1] = lastData.concat(data); + } + return; + } + + this.output.push(data); + encoding = encoding || "ascii"; + this.outputEncodings.push(encoding); +}; + +OutgoingMessage.prototype.sendHeaderLines = function (first_line, headers) { + var sent_connection_header = false; + var sent_content_length_header = false; + var sent_transfer_encoding_header = false; + + // first_line in the case of request is: "GET /index.html HTTP/1.1\r\n" + // in the case of response it is: "HTTP/1.1 200 OK\r\n" + var message_header = first_line; + var field, value; + for (var i in headers) { + if (headers[i] instanceof Array) { + field = headers[i][0]; + value = headers[i][1]; + } else { + if (!headers.hasOwnProperty(i)) continue; + field = i; + value = headers[i]; + } + + message_header += field + ": " + value + CRLF; + + if (connection_expression.test(field)) { + sent_connection_header = true; + if (close_expression.test(value)) this.closeOnFinish = true; + + } else if (transfer_encoding_expression.test(field)) { + sent_transfer_encoding_header = true; + if (chunk_expression.test(value)) this.chunked_encoding = true; + + } else if (content_length_expression.test(field)) { + sent_content_length_header = true; + + } + } + + // keep-alive logic + if (sent_connection_header == false) { + if (this.should_keep_alive && + (sent_content_length_header || this.use_chunked_encoding_by_default)) { + message_header += "Connection: keep-alive\r\n"; + } else { + this.closeOnFinish = true; + message_header += "Connection: close\r\n"; + } + } + + if (sent_content_length_header == false && sent_transfer_encoding_header == false) { + if (this.use_chunked_encoding_by_default) { + message_header += "Transfer-Encoding: chunked\r\n"; + this.chunked_encoding = true; + } + else { + this.closeOnFinish = true; + } + } + + message_header += CRLF; + + this._send(message_header); + // wait until the first body chunk, or close(), is sent to flush. +}; + + +OutgoingMessage.prototype.sendBody = function () { + throw new Error("sendBody() has been renamed to write(). " + + "The 'body' event has been renamed to 'data' and " + + "the 'complete' event has been renamed to 'end'."); +}; + + +OutgoingMessage.prototype.write = function (chunk, encoding) { + if ( (this instanceof ServerResponse) && !this.headWritten) { + throw new Error("writeHead() must be called before write()") + } + + encoding = encoding || "ascii"; + if (this.chunked_encoding) { + this._send(process._byteLength(chunk, encoding).toString(16)); + this._send(CRLF); + this._send(chunk, encoding); + this._send(CRLF); + } else { + this._send(chunk, encoding); + } + + if (this.flushing) { + this.flush(); + } else { + this.flushing = true; + } +}; + +OutgoingMessage.prototype.flush = function () { + this.emit("flush"); +}; + +OutgoingMessage.prototype.finish = function () { + throw new Error("finish() has been renamed to close()."); +}; + +OutgoingMessage.prototype.close = function () { + if (this.chunked_encoding) this._send("0\r\n\r\n"); // last chunk + this.finished = true; + this.flush(); +}; + + +function ServerResponse (req) { + OutgoingMessage.call(this, req.connection); + + if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { + this.use_chunked_encoding_by_default = false; + this.should_keep_alive = false; + } +} +sys.inherits(ServerResponse, OutgoingMessage); +exports.ServerResponse = ServerResponse; + + +ServerResponse.prototype.writeHead = function (statusCode) { + var reasonPhrase, headers, headerIndex; + + if (typeof arguments[1] == 'string') { + reasonPhrase = arguments[1]; + headerIndex = 2; + } else { + reasonPhrase = STATUS_CODES[statusCode] || "unknown"; + headerIndex = 1; + } + + if (typeof arguments[headerIndex] == 'object') { + headers = arguments[headerIndex]; + } else { + headers = {}; + } + + var status_line = "HTTP/1.1 " + statusCode.toString() + " " + + reasonPhrase + CRLF; + this.sendHeaderLines(status_line, headers); + this.headWritten = true; +}; + +// TODO eventually remove sendHeader(), writeHeader() +ServerResponse.prototype.sendHeader = ServerResponse.prototype.writeHead; +ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; + +function ClientRequest (connection, method, url, headers) { + OutgoingMessage.call(this, connection); + + this.should_keep_alive = false; + if (method === "GET" || method === "HEAD") { + this.use_chunked_encoding_by_default = false; + } else { + this.use_chunked_encoding_by_default = true; + } + this.closeOnFinish = true; + + this.sendHeaderLines(method + " " + url + " HTTP/1.1\r\n", headers); +} +sys.inherits(ClientRequest, OutgoingMessage); +exports.ClientRequest = ClientRequest; + +ClientRequest.prototype.finish = function () { + throw new Error( "finish() has been renamed to close() and no longer takes " + + "a response handler as an argument. Manually add a 'response' listener " + + "to the request object." + ); +}; + +ClientRequest.prototype.close = function () { + if (arguments.length > 0) { + throw new Error( "ClientRequest.prototype.close does not take any arguments. " + + "Add a response listener manually to the request object." + ); + } + OutgoingMessage.prototype.close.call(this); +}; + + +function createIncomingMessageStream (connection, incoming_listener) { + var incoming, field, value; + + connection.addListener("messageBegin", function () { + incoming = new IncomingMessage(connection); + field = null; + value = null; + }); + + // Only servers will get URL events. + connection.addListener("url", function (data) { + incoming.url += data; + }); + + connection.addListener("headerField", function (data) { + if (value) { + incoming._addHeaderLine(field, value); + field = null; + value = null; + } + if (field) { + field += data; + } else { + field = data; + } + }); + + connection.addListener("headerValue", function (data) { + if (value) { + value += data; + } else { + value = data; + } + }); + + connection.addListener("headerComplete", function (info) { + if (field && value) { + incoming._addHeaderLine(field, value); + } + + incoming.httpVersion = info.httpVersion; + incoming.httpVersionMajor = info.versionMajor; + incoming.httpVersionMinor = info.versionMinor; + + if (info.method) { + // server only + incoming.method = info.method; + } else { + // client only + incoming.statusCode = info.statusCode; + } + + incoming_listener(incoming, info.should_keep_alive); + }); + + connection.addListener("body", function (chunk) { + incoming.emit('data', chunk); + }); + + connection.addListener("messageComplete", function () { + incoming.emit('end'); + }); +} + +/* Returns true if the message queue is finished and the connection + * should be closed. */ +function flushMessageQueue (connection, queue) { + while (queue[0]) { + var message = queue[0]; + + while (message.output.length > 0) { + if (connection.readyState !== "open" && connection.readyState !== "writeOnly") { + return true; + } + + var data = message.output.shift(); + var encoding = message.outputEncodings.shift(); + + connection.write(data, encoding); + } + + if (!message.finished) break; + + message.emit("sent"); + queue.shift(); + + if (message.closeOnFinish) return true; + } + return false; +} + + +exports.createServer = function (requestListener, options) { + var server = new http.Server(); + //server.setOptions(options); + server.addListener("request", requestListener); + server.addListener("connection", connectionListener); + return server; +}; + +function connectionListener (connection) { + // An array of responses for each connection. In pipelined connections + // we need to keep track of the order they were sent. + var responses = []; + + connection.resetParser(); + + // is this really needed? + connection.addListener("end", function () { + if (responses.length == 0) { + connection.close(); + } else { + responses[responses.length-1].closeOnFinish = true; + } + }); + + + createIncomingMessageStream(connection, function (incoming, should_keep_alive) { + var req = incoming; + + var res = new ServerResponse(req); + res.should_keep_alive = should_keep_alive; + res.addListener("flush", function () { + if (flushMessageQueue(connection, responses)) { + connection.close(); + } + }); + responses.push(res); + + connection.server.emit("request", req, res); + }); +} + + +exports.createClient = function (port, host) { + var client = new http.Client(); + var secure_credentials={ secure : false }; + + var requests = []; + var currentRequest; + + client.tcpSetSecure = client.setSecure; + client.setSecure = function(format_type, ca_certs, crl_list, private_key, certificate) { + secure_credentials.secure = true; + secure_credentials.format_type = format_type; + secure_credentials.ca_certs = ca_certs; + secure_credentials.crl_list = crl_list; + secure_credentials.private_key = private_key; + secure_credentials.certificate = certificate; + } + + client._reconnect = function () { + if (client.readyState != "opening") { + //sys.debug("HTTP CLIENT: reconnecting readyState = " + client.readyState); + client.connect(port, host); + if (secure_credentials.secure) { + client.tcpSetSecure(secure_credentials.format_type, + secure_credentials.ca_certs, + secure_credentials.crl_list, + secure_credentials.private_key, + secure_credentials.certificate); + } + } + }; + + client._pushRequest = function (req) { + req.addListener("flush", function () { + if (client.readyState == "closed") { + //sys.debug("HTTP CLIENT request flush. reconnect. readyState = " + client.readyState); + client._reconnect(); + return; + } + //sys.debug("client flush readyState = " + client.readyState); + if (req == currentRequest) flushMessageQueue(client, [req]); + }); + requests.push(req); + }; + + client.addListener("connect", function () { + client.resetParser(); + currentRequest = requests.shift(); + currentRequest.flush(); + }); + + client.addListener("end", function () { + //sys.debug("client got end closing. readyState = " + client.readyState); + client.close(); + }); + + client.addListener("close", function (had_error) { + if (had_error) { + client.emit("error"); + return; + } + + //sys.debug("HTTP CLIENT onClose. readyState = " + client.readyState); + + // If there are more requests to handle, reconnect. + if (requests.length > 0) { + client._reconnect(); + } + }); + + createIncomingMessageStream(client, function (res) { + //sys.debug("incoming response!"); + + res.addListener('end', function ( ) { + //sys.debug("request complete disconnecting. readyState = " + client.readyState); + client.close(); + }); + + currentRequest.emit("response", res); + }); + + return client; +}; + +http.Client.prototype.get = function () { + throw new Error("client.get(...) is now client.request('GET', ...)"); +}; + +http.Client.prototype.head = function () { + throw new Error("client.head(...) is now client.request('HEAD', ...)"); +}; + +http.Client.prototype.post = function () { + throw new Error("client.post(...) is now client.request('POST', ...)"); +}; + +http.Client.prototype.del = function () { + throw new Error("client.del(...) is now client.request('DELETE', ...)"); +}; + +http.Client.prototype.put = function () { + throw new Error("client.put(...) is now client.request('PUT', ...)"); +}; + +http.Client.prototype.request = function (method, url, headers) { + if (typeof(url) != "string") { // assume method was omitted, shift arguments + headers = url; + url = method; + method = null; + } + var req = new ClientRequest(this, method || "GET", url, headers); + this._pushRequest(req); + return req; +}; + + +exports.cat = function (url, encoding_, headers_) { + var encoding = 'utf8', + headers = {}, + callback = null; + + // parse the arguments for the various options... very ugly + if (typeof(arguments[1]) == 'string') { + encoding = arguments[1]; + if (typeof(arguments[2]) == 'object') { + headers = arguments[2]; + if (typeof(arguments[3]) == 'function') callback = arguments[3]; + } else { + if (typeof(arguments[2]) == 'function') callback = arguments[2]; + } + } else { + // didn't specify encoding + if (typeof(arguments[1]) == 'object') { + headers = arguments[1]; + callback = arguments[2]; + } else { + callback = arguments[1]; + } + } + + var url = require("url").parse(url); + + var hasHost = false; + for (var i in headers) { + if (i.toLowerCase() === "host") { + hasHost = true; + break; + } + } + if (!hasHost) headers["Host"] = url.hostname; + + var content = ""; + + var client = exports.createClient(url.port || 80, url.hostname); + var req = client.request((url.pathname || "/")+(url.search || "")+(url.hash || ""), headers); + + req.addListener('response', function (res) { + if (res.statusCode < 200 || res.statusCode >= 300) { + if (callback) callback(res.statusCode); + client.close(); + return; + } + res.setBodyEncoding(encoding); + res.addListener('data', function (chunk) { content += chunk; }); + res.addListener('end', function () { + if (callback) callback(null, content); + }); + }); + + client.addListener("error", function (err) { + // todo an error should actually be passed here... + if (callback) callback(new Error('Connection error')); + }); + + req.close(); +}; diff --git a/lib/net.js b/lib/net.js new file mode 100644 index 00000000000..8a433da3817 --- /dev/null +++ b/lib/net.js @@ -0,0 +1,959 @@ +var sys = require("sys"); +var fs = require("fs"); +var events = require("events"); + +var debugLevel = 0; +if ('NODE_DEBUG' in process.ENV) debugLevel = 1; +function debug () { + if (debugLevel > 0) sys.error.apply(this, arguments); +} + +var binding = process.binding('net'); + +// Note about Buffer interface: +// I'm attempting to do the simplest possible interface to abstracting raw +// memory allocation. This might turn out to be too simple - it seems that +// I always use a buffer.used member to keep track of how much I've filled. +// Perhaps giving the Buffer a file-like interface with a head (which would +// represent buffer.used) that can be seeked around would be easier. I'm not +// yet convinced that every use-case can be fit into that abstraction, so +// waiting to implement it until I get more experience with this. +var Buffer = require('buffer').Buffer; + +var IOWatcher = process.IOWatcher; +var assert = process.assert; + +var socket = binding.socket; +var bind = binding.bind; +var connect = binding.connect; +var listen = binding.listen; +var accept = binding.accept; +var close = binding.close; +var shutdown = binding.shutdown; +var read = binding.read; +var write = binding.write; +var toRead = binding.toRead; +var setNoDelay = binding.setNoDelay; +var socketError = binding.socketError; +var getsockname = binding.getsockname; +var getaddrinfo = binding.getaddrinfo; +var needsLookup = binding.needsLookup; +var errnoException = binding.errnoException; +var EINPROGRESS = binding.EINPROGRESS; +var ENOENT = binding.ENOENT; +var END_OF_FILE = 0; + + +// IDLE TIMEOUTS +// +// Because often many sockets will have the same idle timeout we will not +// use one timeout watcher per socket. It is too much overhead. Instead +// we'll use a single watcher for all sockets with the same timeout value +// and a linked list. This technique is described in the libev manual: +// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts + + +var timeout = new (function () { + // Object containing all lists, timers + // key = time in milliseconds + // value = list + var lists = {}; + + // show the most idle socket + function peek (list) { + if (list._idlePrev == list) return null; + return list._idlePrev; + } + + + // remove the most idle socket from the list + function shift (list) { + var first = list._idlePrev; + remove(first); + return first; + } + + + // remove a socket from its list + function remove (socket) { + socket._idleNext._idlePrev = socket._idlePrev; + socket._idlePrev._idleNext = socket._idleNext; + } + + + // remove a socket from its list and place at the end. + function append (list, socket) { + remove(socket); + socket._idleNext = list._idleNext; + socket._idleNext._idlePrev = socket; + socket._idlePrev = list + list._idleNext = socket; + } + + + function normalize (msecs) { + if (!msecs || msecs <= 0) return 0; + // round up to one sec + if (msecs < 1000) return 1000; + // round down to nearest second. + return msecs - (msecs % 1000); + } + + // the main function - creates lists on demand and the watchers associated + // with them. + function insert (socket, msecs) { + socket._idleStart = process.now; + socket._idleTimeout = msecs; + + if (!msecs) return; + + var list; + + if (lists[msecs]) { + list = lists[msecs]; + } else { + list = new process.Timer(); + list._idleNext = list; + list._idlePrev = list; + + lists[msecs] = list; + + list.callback = function () { + sys.puts('timeout callback ' + msecs); + // TODO - don't stop and start the watcher all the time. + // just set its repeat + var now = process.now; + var first; + while (first = peek(list)) { + var diff = now - first._idleStart; + if (diff < msecs) { + list.again(msecs - diff); + sys.puts(msecs + ' list wait'); + return; + } else { + remove(first); + assert(first != peek(list)); + first.emit('timeout'); + first.forceClose(new Error('idle timeout')); + } + } + sys.puts(msecs + ' list empty'); + assert(list._idleNext == list); // list is empty + list.stop(); + }; + } + + if (list._idleNext == list) { + // if empty (re)start the timer + list.again(msecs); + } + + append(list, socket); + assert(list._idleNext != list); // list is not empty + } + + + var unenroll = this.unenroll = function (socket) { + if (socket._idleNext) { + socket._idleNext._idlePrev = socket._idlePrev; + socket._idlePrev._idleNext = socket._idleNext; + + var list = lists[socket._idleTimeout]; + // if empty then stop the watcher + //sys.puts('unenroll'); + if (list && list._idlePrev == list) { + //sys.puts('unenroll: list empty'); + list.stop(); + } + } + }; + + + // Does not start the time, just sets up the members needed. + this.enroll = function (socket, msecs) { + // if this socket was already in a list somewhere + // then we should unenroll it from that + if (socket._idleNext) unenroll(socket); + + socket._idleTimeout = msecs; + socket._idleNext = socket; + socket._idlePrev = socket; + }; + + // call this whenever the socket is active (not idle) + // it will reset its timeout. + this.active = function (socket) { + var msecs = socket._idleTimeout; + if (msecs) { + var list = lists[msecs]; + if (socket._idleNext == socket) { + insert(socket, msecs); + } else { + // inline append + socket._idleStart = process.now; + socket._idleNext._idlePrev = socket._idlePrev; + socket._idlePrev._idleNext = socket._idleNext; + socket._idleNext = list._idleNext; + socket._idleNext._idlePrev = socket; + socket._idlePrev = list + list._idleNext = socket; + } + } + }; +})(); + + + + + +// This is a free list to avoid creating so many of the same object. + +function FreeList (name, max, constructor) { + this.name = name; + this.constructor = constructor; + this.max = max; + this.list = []; +} + + +FreeList.prototype.alloc = function () { + //debug("alloc " + this.name + " " + this.list.length); + return this.list.length ? this.list.shift() + : this.constructor.apply(this, arguments); +}; + + +FreeList.prototype.free = function (obj) { + //debug("free " + this.name + " " + this.list.length); + if (this.list.length < this.max) { + this.list.push(obj); + } +}; + + +var ioWatchers = new FreeList("iowatcher", 100, function () { + return new IOWatcher(); +}); + + +var nb = 0; +var buffers = new FreeList("buffer", 100, function (l) { + return new Buffer(l); +}); + + +// Allocated on demand. +var recvBuffer = null; +function allocRecvBuffer () { + recvBuffer = new Buffer(40*1024); + recvBuffer.used = 0; +} + + +function _doFlush () { + var socket = this.socket; + // Stream becomes writeable on connect() but don't flush if there's + // nothing actually to write + if (socket._writeQueueSize == 0) { + return; + } + if (socket.flush()) { + assert(socket._writeQueueSize == 0); + if (socket._events && socket._events['drain']) socket.emit("drain"); + if (socket.ondrain) socket.ondrain(); // Optimization + } +} + +function initStream (self) { + self._readWatcher = ioWatchers.alloc(); + self._readWatcher.callback = function () { + // If this is the first recv (recvBuffer doesn't exist) or we've used up + // most of the recvBuffer, allocate a new one. + if (recvBuffer) { + if (recvBuffer.length - recvBuffer.used < 128) { + // discard the old recvBuffer. Can't add to the free list because + // users might have refernces to slices on it. + recvBuffer = null; + allocRecvBuffer(); + } + } else { + allocRecvBuffer(); + } + + //debug('recvBuffer.used ' + recvBuffer.used); + var bytesRead; + + try { + bytesRead = read(self.fd, + recvBuffer, + recvBuffer.used, + recvBuffer.length - recvBuffer.used); + } catch (e) { + self.forceClose(e); + return; + } + + //debug('bytesRead ' + bytesRead + '\n'); + + if (bytesRead == 0) { + self.readable = false; + self._readWatcher.stop(); + + if (self._events && self._events['end']) self.emit('end'); + if (self.onend) self.onend(); + + if (!self.writable) self.forceClose(); + } else if (bytesRead > 0) { + + timeout.active(self); + + var start = recvBuffer.used; + var end = recvBuffer.used + bytesRead; + + if (!self._encoding) { + if (self._events && self._events['data']) { + // emit a slice + self.emit('data', recvBuffer.slice(start, end)); + } + + // Optimization: emit the original buffer with end points + if (self.ondata) self.ondata(recvBuffer, start, end); + } else { + // TODO remove me - we should only output Buffer + + var string; + switch (self._encoding) { + case 'utf8': + string = recvBuffer.utf8Slice(start, end); + break; + case 'ascii': + string = recvBuffer.asciiSlice(start, end); + break; + case 'binary': + string = recvBuffer.binarySlice(start, end); + break; + default: + throw new Error('Unsupported encoding ' + self._encoding + '. Use Buffer'); + } + self.emit('data', string); + } + + + recvBuffer.used += bytesRead; + } + }; + self.readable = false; + + self._writeQueue = []; // queue of buffers that need to be written to socket + // XXX use link list? + self._writeQueueSize = 0; // in bytes, not to be confused with _writeQueue.length! + + self._writeWatcher = ioWatchers.alloc(); + self._writeWatcher.socket = self; + self._writeWatcher.callback = _doFlush; + self.writable = false; +} + +function Stream (fd) { + events.EventEmitter.call(this); + + this.fd = null; + + if (parseInt(fd) >= 0) { + this.open(fd); + } +}; +sys.inherits(Stream, events.EventEmitter); +exports.Stream = Stream; + + +Stream.prototype.open = function (fd) { + initStream(this); + + this.fd = fd; + + this.readable = true; + + this._writeWatcher.set(this.fd, false, true); + this.writable = true; +} + + +exports.createConnection = function (port, host) { + var s = new Stream(); + s.connect(port, host); + return s; +}; + + +Object.defineProperty(Stream.prototype, 'readyState', { + get: function () { + if (this._resolving) { + return 'opening'; + } else if (this.readable && this.writable) { + return 'open'; + } else if (this.readable && !this.writable){ + return 'readOnly'; + } else if (!this.readable && this.writable){ + return 'writeOnly'; + } else { + return 'closed'; + } + } +}); + + +Stream.prototype._allocateSendBuffer = function () { + var b = buffers.alloc(1024); + b.used = 0; + b.sent = 0; + b.isMsg = false; + this._writeQueue.push(b); + return b; +}; + + +Stream.prototype._writeString = function (data, encoding) { + var self = this; + if (!self.writable) throw new Error('Stream is not writable'); + var buffer; + + if (self._writeQueue.length == 0) { + buffer = self._allocateSendBuffer(); + } else { + buffer = self.__writeQueueLast(); + if (buffer.length - buffer.used < 64) { + buffer = self._allocateSendBuffer(); + } + } + + encoding = (encoding || 'utf8').toLowerCase(); // default to utf8 + + var charsWritten; + var bytesWritten; + + + if (encoding == 'utf8') { + charsWritten = buffer.utf8Write(data, buffer.used); + bytesWritten = Buffer.byteLength(data.slice(0, charsWritten)); + } else if (encoding == 'ascii') { + // ascii + charsWritten = buffer.asciiWrite(data, buffer.used); + bytesWritten = charsWritten; + } else { + // binary + charsWritten = buffer.binaryWrite(data, buffer.used); + bytesWritten = charsWritten; + } + + buffer.used += bytesWritten; + self._writeQueueSize += bytesWritten; + + //debug('charsWritten ' + charsWritten); + //debug('buffer.used ' + buffer.used); + + // If we didn't finish, then recurse with the rest of the string. + if (charsWritten < data.length) { + //debug('recursive write'); + self._writeString(data.slice(charsWritten), encoding); + } +}; + + +Stream.prototype.__writeQueueLast = function () { + return this._writeQueue.length > 0 ? this._writeQueue[this._writeQueue.length-1] + : null; +}; + + +Stream.prototype.send = function () { + throw new Error('send renamed to write'); +}; + +Stream.prototype.setEncoding = function (enc) { + // TODO check values, error out on bad, and deprecation message? + this._encoding = enc.toLowerCase(); +}; + +// Returns true if all the data was flushed to socket. Returns false if +// something was queued. If data was queued, then the "drain" event will +// signal when it has been finally flushed to socket. +Stream.prototype.write = function (data, encoding) { + var self = this; + + if (!self.writable) throw new Error('Stream is not writable'); + + if (self._writeQueue && self._writeQueue.length) { + // slow + // There is already a write queue, so let's append to it. + return self._queueWrite(data, encoding); + + } else { + // The most common case. There is no write queue. Just push the data + // directly to the socket. + + var bytesWritten; + var buffer = data, off = 0, len = data.length; + + if (typeof data == 'string') { + encoding = (encoding || 'utf8').toLowerCase(); + var bytes = Buffer.byteLength(data, encoding); + + //debug('write string :' + JSON.stringify(data)); + + if (!recvBuffer) allocRecvBuffer(); + + if (recvBuffer.length - recvBuffer.used < bytes) { + // not enough room - go to slow case + return self._queueWrite(data, encoding); + } + + var charsWritten; + if (encoding == 'utf8') { + recvBuffer.utf8Write(data, recvBuffer.used); + } else if (encoding == 'ascii') { + // ascii + recvBuffer.asciiWrite(data, recvBuffer.used); + } else { + // binary + recvBuffer.binaryWrite(data, recvBuffer.used); + } + + buffer = recvBuffer; + off = recvBuffer.used; + len = bytes; + } + + //debug('write [fd, off, len] =' + JSON.stringify([self.fd, off, len])); + + // Send the buffer. + try { + bytesWritten = write(self.fd, buffer, off, len); + } catch (e) { + self.forceClose(e); + return false; + } + + //debug('wrote ' + bytesWritten); + + // Note: if using the recvBuffer - we don't need to increase + // recvBuffer.used because it was all sent. Just reuse that space. + + if (bytesWritten == len) return true; + + //debug('write incomplete ' + bytesWritten + ' < ' + len); + + if (buffer == data) { + data.sent = bytesWritten || 0; + data.used = data.length; + } else { + // string + recvBuffer.used += bytesWritten; + data = recvBuffer.slice(off+bytesWritten, off+len+bytesWritten); + data.sent = 0; + data.used = data.length; + } + + //if (!self._writeQueue) initWriteStream(self); + self._writeQueue.push(data); + self._writeQueueSize += data.used; + return false; + } +} + +Stream.prototype._queueWrite = function (data, encoding) { + //debug('_queueWrite'); + var self = this; + + if (self.__writeQueueLast() == END_OF_FILE) { + throw new Error('socket.close() called already; cannot write.'); + } + + if (typeof(data) == 'string') { + self._writeString(data, encoding); + } else { + // data is a Buffer + // walk through the _writeQueue, find the first empty buffer + //var inserted = false; + data.sent = 0; + data.used = data.length; + self._writeQueue.push(data); + self._writeQueueSize += data.used; + } + return this.flush(); +}; + +// Flushes the write buffer out. +// Returns true if the entire buffer was flushed. +Stream.prototype.flush = function () { + var self = this; + + var bytesWritten; + while (self._writeQueue.length) { + if (!self.writable) throw new Error('Stream is not writable'); + + var b = self._writeQueue[0]; + + if (b == END_OF_FILE) { + self._shutdown(); + return true; + } + + if (b.sent == b.used) { + // shift! + self._writeQueue.shift(); + buffers.free(b); + continue; + } + + var fdToSend = null; + + try { + bytesWritten = write(self.fd, + b, + b.sent, + b.used - b.sent); + } catch (e) { + self.forceClose(e); + return false; + } + + timeout.active(self); + + + if (bytesWritten === null) { + // EAGAIN + debug('write EAGAIN'); + self._writeWatcher.start(); + assert(self._writeQueueSize > 0); + return false; + } else { + b.sent += bytesWritten; + self._writeQueueSize -= bytesWritten; + //debug('bytes sent: ' + b.sent); + } + } + if (self._writeWatcher) self._writeWatcher.stop(); + return true; +}; + + +function doConnect (socket, port, host) { + try { + connect(socket.fd, port, host); + } catch (e) { + socket.forceClose(e); + } + + // Don't start the read watcher until connection is established + socket._readWatcher.set(socket.fd, true, false); + + // How to connect on POSIX: Wait for fd to become writable, then call + // socketError() if there isn't an error, we're connected. AFAIK this a + // platform independent way determining when a non-blocking connection + // is established, but I have only seen it documented in the Linux + // Manual Page connect(2) under the error code EINPROGRESS. + socket._writeWatcher.set(socket.fd, false, true); + socket._writeWatcher.start(); + socket._writeWatcher.callback = function () { + var errno = socketError(socket.fd); + if (errno == 0) { + // connection established + socket.resume(); + socket.readable = true; + socket.writable = true; + socket._writeWatcher.callback = socket._doFlush; + socket.emit('connect'); + } else if (errno != EINPROGRESS) { + socket.forceClose(errnoException(errno, 'connect')); + } + }; +} + + +// var stream = new Stream(); +// stream.connect(80) - TCP connect to port 80 on the localhost +// stream.connect(80, 'nodejs.org') - TCP connect to port 80 on nodejs.org +// stream.connect('/tmp/socket') - UNIX connect to socket specified by path +Stream.prototype.connect = function () { + var self = this; + initStream(self); + if (self.fd) throw new Error('Stream already opened'); + if (!self._readWatcher) throw new Error('No readWatcher'); + + timeout.active(socket); + + var port = parseInt(arguments[0]); + + if (port >= 0) { + self.fd = socket('tcp'); + //debug('new fd = ' + self.fd); + self.type = 'tcp'; + // TODO dns resolution on arguments[1] + var port = arguments[0]; + self._resolving = true; + lookupDomainName(arguments[1], function (ip) { + self._resolving = false; + doConnect(self, port, ip); + }); + } else { + self.fd = socket('unix'); + self.type = 'unix'; + // TODO check if sockfile exists? + doConnect(self, arguments[0]); + } +}; + + +Stream.prototype.address = function () { + return getsockname(this.fd); +}; + + +Stream.prototype.setNoDelay = function (v) { + if (this.type == 'tcp') setNoDelay(this.fd, v); +}; + + +Stream.prototype.setTimeout = function (msecs) { + timeout.enroll(this, msecs); +}; + + +Stream.prototype.pause = function () { + this._readWatcher.stop(); +}; + + +Stream.prototype.resume = function () { + if (this.fd === null) throw new Error('Cannot resume() closed Stream.'); + this._readWatcher.set(this.fd, true, false); + this._readWatcher.start(); +}; + + +Stream.prototype.forceClose = function (exception) { + // recvBuffer is shared between sockets, so don't need to free it here. + var self = this; + + var b; + while (this._writeQueue.length) { + b = this._writeQueue.shift(); + if (b instanceof Buffer) buffers.free(b); + } + + if (this._writeWatcher) { + this._writeWatcher.stop(); + ioWatchers.free(this._writeWatcher); + this._writeWatcher = null; + } + + if (this._readWatcher) { + this._readWatcher.stop(); + ioWatchers.free(this._readWatcher); + this._readWatcher = null; + } + + timeout.unenroll(this); + + // FIXME Bug when this.fd == 0 + if (this.fd) { + close(this.fd); + //debug('close ' + this.fd); + this.fd = null; + process.nextTick(function () { + if (exception) self.emit('error', exception); + self.emit('close', exception ? true : false); + }); + } +}; + + +Stream.prototype._shutdown = function () { + if (this.writable) { + this.writable = false; + + try { + shutdown(this.fd, 'write') + } catch (e) { + this.forceClose(e); + return; + } + + if (!this.readable) this.forceClose(); + } +}; + + +Stream.prototype.close = function () { + if (this.writable) { + if (this.__writeQueueLast() != END_OF_FILE) { + this._writeQueue.push(END_OF_FILE); + this.flush(); + } + } +}; + + +function Server (listener) { + events.EventEmitter.call(this); + var self = this; + + if (listener) { + self.addListener('connection', listener); + } + + self.watcher = new IOWatcher(); + self.watcher.host = self; + self.watcher.callback = function () { + while (self.fd) { + var peerInfo = accept(self.fd); + if (!peerInfo) return; + + var s = new Stream(peerInfo.fd); + s.remoteAddress = peerInfo.remoteAddress; + s.remotePort = peerInfo.remotePort; + s.type = self.type; + s.server = self; + s.resume(); + + self.emit('connection', s); + // The 'connect' event probably should be removed for server-side + // sockets. It's redundent. + s.emit('connect'); + } + }; +} +sys.inherits(Server, events.EventEmitter); +exports.Server = Server; + + +exports.createServer = function (listener) { + return new Server(listener); +}; + +/* This function does both an ipv4 and ipv6 look up. + * It first tries the ipv4 look up, if that fails, then it does the ipv6. + */ +function lookupDomainName (dn, callback) { + if (!needsLookup(dn)) { + // Always wait until the next tick this is so people can do + // + // server.listen(8000); + // server.addListener('listening', fn); + // + // Marginally slower, but a lot fewer WTFs. + process.nextTick(function () { callback(dn); }) + } else { + debug("getaddrinfo 4 " + dn); + getaddrinfo(dn, 4, function (r4) { + if (r4 instanceof Error) throw r4; + if (r4.length > 0) { + debug("getaddrinfo 4 found " + r4); + callback(r4[0]); + } else { + debug("getaddrinfo 6 " + dn); + getaddrinfo(dn, 6, function (r6) { + if (r6 instanceof Error) throw r6; + if (r6.length < 0) { + throw new Error("No address associated with hostname " + dn); + } + debug("getaddrinfo 6 found " + r6); + callback(r6[0]); + }); + } + }); + } +} + + +// Listen on a UNIX socket +// server.listen("/tmp/socket"); +// +// Listen on port 8000, accept connections from INADDR_ANY. +// server.listen(8000); +// +// Listen on port 8000, accept connections to "192.168.1.2" +// server.listen(8000, "192.168.1.2"); +Server.prototype.listen = function () { + var self = this; + if (self.fd) throw new Error('Server already opened'); + + function doListen () { + listen(self.fd, 128); + self.watcher.set(self.fd, true, false); + self.watcher.start(); + self.emit("listening"); + } + + if (typeof(arguments[0]) == 'string') { + // the first argument specifies a path + self.fd = socket('unix'); + self.type = 'unix'; + var path = arguments[0]; + self.path = path; + // unlink sockfile if it exists + fs.stat(path, function (err, r) { + if (err) { + if (err.errno == ENOENT) { + bind(self.fd, path); + doListen(); + } else { + throw r; + } + } else { + if (!r.isFile()) { + throw new Error("Non-file exists at " + path); + } else { + fs.unlink(path, function (err) { + if (err) { + throw err; + } else { + bind(self.fd, path); + doListen(); + } + }); + } + } + }); + } else if (!arguments[0]) { + // Don't bind(). OS will assign a port with INADDR_ANY. + // The port can be found with server.address() + self.fd = socket('tcp'); + self.type = 'tcp'; + doListen(); + } else { + // the first argument is the port, the second an IP + self.fd = socket('tcp'); + self.type = 'tcp'; + var port = arguments[0]; + lookupDomainName(arguments[1], function (ip) { + bind(self.fd, port, ip); + doListen(); + }); + } +}; + + +Server.prototype.address = function () { + return getsockname(this.fd); +}; + + +Server.prototype.close = function () { + var self = this; + if (!self.fd) throw new Error('Not running'); + + self.watcher.stop(); + + close(self.fd); + self.fd = null; + + if (self.type === "unix") { + fs.unlink(self.path, function () { + self.emit("close"); + }); + } else { + self.emit("close"); + } +}; diff --git a/lib/repl.js b/lib/repl.js index ee7dd9b61c3..1608102b1f2 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -12,13 +12,17 @@ exports.scope = {}; exports.prompt = "node> "; // Can overridden with custom print functions, such as `probe` or `eyes.js` exports.writer = sys.p; + +var stdin; + exports.start = function (prompt) { if (prompt !== undefined) { exports.prompt = prompt; } - process.stdio.open(); - process.stdio.addListener("data", readline); + stdin = process.openStdin(); + stdin.setEncoding('utf8'); + stdin.addListener("data", readline); displayPrompt(); } @@ -96,7 +100,7 @@ function parseREPLKeyword (cmd) { displayPrompt(); return true; case ".exit": - process.stdio.close(); + stdin.close(); return true; case ".help": sys.puts(".break\tSometimes you get stuck in a place you can't get out... This will get you out."); diff --git a/lib/sys.js b/lib/sys.js index a828e5cef5e..c3b5d61304d 100644 --- a/lib/sys.js +++ b/lib/sys.js @@ -2,23 +2,23 @@ var events = require('events'); exports.print = function () { for (var i = 0, len = arguments.length; i < len; ++i) { - process.stdio.write(arguments[i]); + process.stdout.write(arguments[i]); } }; exports.puts = function () { for (var i = 0, len = arguments.length; i < len; ++i) { - process.stdio.write(arguments[i] + '\n'); + process.stdout.write(arguments[i] + '\n'); } }; exports.debug = function (x) { - process.stdio.writeError("DEBUG: " + x + "\n"); + process.binding('stdio').writeError("DEBUG: " + x + "\n"); }; -exports.error = function (x) { +var error = exports.error = function (x) { for (var i = 0, len = arguments.length; i < len; ++i) { - process.stdio.writeError(arguments[i] + '\n'); + process.binding('stdio').writeError(arguments[i] + '\n'); } }; @@ -184,7 +184,7 @@ exports.inspect = function (obj, showHidden, depth) { exports.p = function () { for (var i = 0, len = arguments.length; i < len; ++i) { - exports.error(exports.inspect(arguments[i])); + error(exports.inspect(arguments[i])); } }; @@ -207,29 +207,14 @@ exports.log = function (msg) { exports.puts(timestamp() + ' - ' + msg.toString()); } -exports.exec = function (command, callback) { - var child = process.createChildProcess("/bin/sh", ["-c", command]); - var stdout = ""; - var stderr = ""; - - child.addListener("output", function (chunk) { - if (chunk) stdout += chunk; - }); - - child.addListener("error", function (chunk) { - if (chunk) stderr += chunk; - }); - - child.addListener("exit", function (code) { - if (code == 0) { - if (callback) callback(null, stdout, stderr); - } else { - var e = new Error("Command failed: " + stderr); - e.code = code; - if (callback) callback(e, stdout, stderr); - } - }); -}; +var execWarning; +exports.exec = function () { + if (!execWarning) { + execWarning = 'sys.exec has moved to the "child_process" module. Please update your source code.' + error(execWarning); + } + return require('child_process').exec.apply(this, arguments); +} /** * Inherit the prototype methods from one constructor into another. diff --git a/lib/tcp.js b/lib/tcp.js index a27a32761a8..64080be87b6 100644 --- a/lib/tcp.js +++ b/lib/tcp.js @@ -1,26 +1,11 @@ -var tcp = process.binding('tcp'); +var net = require('net'); +var sys = require('sys'); -var TLS_STATUS_CODES = { - 1 : 'JS_GNUTLS_CERT_VALIDATED', - 0 : 'JS_GNUTLS_CERT_UNDEFINED', +var warning; +if (!warning) { + warning = "The 'tcp' module is now called 'net'. Otherwise it should have a similar interface."; + sys.error(warning); } -TLS_STATUS_CODES[-100] = 'JS_GNUTLS_CERT_SIGNER_NOT_FOUND'; -TLS_STATUS_CODES[-101] = 'JS_GNUTLS_CERT_SIGNER_NOT_CA'; -TLS_STATUS_CODES[-102] = 'JS_GNUTLS_CERT_INVALID'; -TLS_STATUS_CODES[-103] = 'JS_GNUTLS_CERT_NOT_ACTIVATED'; -TLS_STATUS_CODES[-104] = 'JS_GNUTLS_CERT_EXPIRED'; -TLS_STATUS_CODES[-105] = 'JS_GNUTLS_CERT_REVOKED'; -TLS_STATUS_CODES[-106] = 'JS_GNUTLS_CERT_DOES_NOT_MATCH_HOSTNAME'; -exports.createServer = function (on_connection, options) { - var server = new tcp.Server(); - server.addListener("connection", on_connection); - //server.setOptions(options); - return server; -}; - -exports.createConnection = function (port, host) { - var connection = new tcp.Connection(); - connection.connect(port, host); - return connection; -}; +exports.createServer = net.createServer; +exports.createConnection = net.createConnection; diff --git a/lib/tcp_old.js b/lib/tcp_old.js new file mode 100644 index 00000000000..a27a32761a8 --- /dev/null +++ b/lib/tcp_old.js @@ -0,0 +1,26 @@ +var tcp = process.binding('tcp'); + +var TLS_STATUS_CODES = { + 1 : 'JS_GNUTLS_CERT_VALIDATED', + 0 : 'JS_GNUTLS_CERT_UNDEFINED', +} +TLS_STATUS_CODES[-100] = 'JS_GNUTLS_CERT_SIGNER_NOT_FOUND'; +TLS_STATUS_CODES[-101] = 'JS_GNUTLS_CERT_SIGNER_NOT_CA'; +TLS_STATUS_CODES[-102] = 'JS_GNUTLS_CERT_INVALID'; +TLS_STATUS_CODES[-103] = 'JS_GNUTLS_CERT_NOT_ACTIVATED'; +TLS_STATUS_CODES[-104] = 'JS_GNUTLS_CERT_EXPIRED'; +TLS_STATUS_CODES[-105] = 'JS_GNUTLS_CERT_REVOKED'; +TLS_STATUS_CODES[-106] = 'JS_GNUTLS_CERT_DOES_NOT_MATCH_HOSTNAME'; + +exports.createServer = function (on_connection, options) { + var server = new tcp.Server(); + server.addListener("connection", on_connection); + //server.setOptions(options); + return server; +}; + +exports.createConnection = function (port, host) { + var connection = new tcp.Connection(); + connection.connect(port, host); + return connection; +}; diff --git a/src/node.cc b/src/node.cc index 3783b38f245..3fbc8e29ded 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1,6 +1,8 @@ // Copyright 2009 Ryan Dahl #include +#include + #include #include #include @@ -12,12 +14,16 @@ #include #include /* setuid, getuid */ +#include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -533,6 +539,13 @@ static Handle SetUid(const Arguments& args) { return Undefined(); } +Handle +NowGetter (Local property, const AccessorInfo& info) +{ + HandleScope scope; + return scope.Close(Integer::New(ev_now(EV_DEFAULT_UC))); +} + v8::Handle Exit(const v8::Arguments& args) { HandleScope scope; @@ -1070,7 +1083,18 @@ static Handle Binding(const Arguments& args) { Local exports; - if (!strcmp(*module_v, "http")) { + // TODO DRY THIS UP! + + if (!strcmp(*module_v, "stdio")) { + if (binding_cache->Has(module)) { + exports = binding_cache->Get(module)->ToObject(); + } else { + exports = Object::New(); + Stdio::Initialize(exports); + binding_cache->Set(module, exports); + } + + } else if (!strcmp(*module_v, "http")) { if (binding_cache->Has(module)) { exports = binding_cache->Get(module)->ToObject(); } else { @@ -1127,6 +1151,42 @@ static Handle Binding(const Arguments& args) { binding_cache->Set(module, exports); } + } else if (!strcmp(*module_v, "net")) { + if (binding_cache->Has(module)) { + exports = binding_cache->Get(module)->ToObject(); + } else { + exports = Object::New(); + InitNet2(exports); + binding_cache->Set(module, exports); + } + + } else if (!strcmp(*module_v, "http_parser")) { + if (binding_cache->Has(module)) { + exports = binding_cache->Get(module)->ToObject(); + } else { + exports = Object::New(); + InitHttpParser(exports); + binding_cache->Set(module, exports); + } + + } else if (!strcmp(*module_v, "child_process")) { + if (binding_cache->Has(module)) { + exports = binding_cache->Get(module)->ToObject(); + } else { + exports = Object::New(); + ChildProcess::Initialize(exports); + binding_cache->Set(module, exports); + } + + } else if (!strcmp(*module_v, "buffer")) { + if (binding_cache->Has(module)) { + exports = binding_cache->Get(module)->ToObject(); + } else { + exports = Object::New(); + Buffer::Initialize(exports); + binding_cache->Set(module, exports); + } + } else if (!strcmp(*module_v, "natives")) { if (binding_cache->Has(module)) { exports = binding_cache->Get(module)->ToObject(); @@ -1135,19 +1195,24 @@ static Handle Binding(const Arguments& args) { // Explicitly define native sources. // TODO DRY/automate this? exports->Set(String::New("assert"), String::New(native_assert)); + exports->Set(String::New("buffer"), String::New(native_buffer)); + exports->Set(String::New("child_process"),String::New(native_child_process)); exports->Set(String::New("dns"), String::New(native_dns)); exports->Set(String::New("events"), String::New(native_events)); exports->Set(String::New("file"), String::New(native_file)); exports->Set(String::New("fs"), String::New(native_fs)); exports->Set(String::New("http"), String::New(native_http)); + exports->Set(String::New("http_old"), String::New(native_http_old)); exports->Set(String::New("ini"), String::New(native_ini)); exports->Set(String::New("mjsunit"), String::New(native_mjsunit)); exports->Set(String::New("multipart"), String::New(native_multipart)); + exports->Set(String::New("net"), String::New(native_net)); exports->Set(String::New("posix"), String::New(native_posix)); exports->Set(String::New("querystring"), String::New(native_querystring)); exports->Set(String::New("repl"), String::New(native_repl)); exports->Set(String::New("sys"), String::New(native_sys)); exports->Set(String::New("tcp"), String::New(native_tcp)); + exports->Set(String::New("tcp_old"), String::New(native_tcp_old)); exports->Set(String::New("uri"), String::New(native_uri)); exports->Set(String::New("url"), String::New(native_url)); exports->Set(String::New("utils"), String::New(native_utils)); @@ -1169,6 +1234,8 @@ static void Load(int argc, char *argv[]) { Local process_template = FunctionTemplate::New(); node::EventEmitter::Initialize(process_template); + process_template->InstanceTemplate()->SetAccessor(String::NewSymbol("now"), NowGetter, NULL); + process = Persistent::New(process_template->GetFunction()->NewInstance()); // Add a reference to the global object @@ -1247,10 +1314,9 @@ static void Load(int argc, char *argv[]) { // Initialize the C++ modules..................filename of module + IOWatcher::Initialize(process); // io_watcher.cc IdleWatcher::Initialize(process); // idle_watcher.cc - Stdio::Initialize(process); // stdio.cc Timer::Initialize(process); // timer.cc - ChildProcess::Initialize(process); // child_process.cc DefineConstants(process); // constants.cc // Compile, execute the src/node.js file. (Which was included as static C diff --git a/src/node.h b/src/node.h index fd872a09607..44cf0dbd53c 100644 --- a/src/node.h +++ b/src/node.h @@ -58,5 +58,24 @@ ssize_t DecodeWrite(char *buf, v8::Local BuildStatsObject(struct stat * s); +static inline v8::Persistent* cb_persist( + const v8::Local &v) { + v8::Persistent *fn = new v8::Persistent(); + *fn = v8::Persistent::New(v8::Local::Cast(v)); + return fn; +} + +static inline v8::Persistent* cb_unwrap(void *data) { + v8::Persistent *cb = + reinterpret_cast*>(data); + assert((*cb)->IsFunction()); + return cb; +} + +static inline void cb_destroy(v8::Persistent * cb) { + cb->Dispose(); + delete cb; +} + } // namespace node #endif // SRC_NODE_H_ diff --git a/src/node.js b/src/node.js index 4a9a2348b61..84710b06e04 100644 --- a/src/node.js +++ b/src/node.js @@ -25,6 +25,7 @@ process.unwatchFile = removed("process.unwatchFile() has moved to fs.unwatchFile GLOBAL.node = {}; node.createProcess = removed("node.createProcess() has been changed to process.createChildProcess() update your code"); +process.createChildProcess = removed("childProcess API has changed. See doc/api.txt."); node.exec = removed("process.exec() has moved. Use require('sys') to bring it back."); node.inherits = removed("node.inherits() has moved. Use require('sys') to access it."); process.inherits = removed("process.inherits() has moved to sys.inherits."); @@ -94,27 +95,11 @@ function requireNative (id) { } -process.createChildProcess = function (file, args, env) { - var child = new process.ChildProcess(); - args = args || []; - env = env || process.env; - var envPairs = []; - for (var key in env) { - if (env.hasOwnProperty(key)) { - envPairs.push(key + "=" + env[key]); - } - } - // TODO Note envPairs is not currently used in child_process.cc. The PATH - // needs to be searched for the 'file' command if 'file' does not contain - // a '/' character. - child.spawn(file, args, envPairs); - return child; -}; - process.assert = function (x, msg) { if (!(x)) throw new Error(msg || "assertion error"); }; + // From jQuery.extend in the jQuery JavaScript Library v1.3.2 // Copyright (c) 2009 John Resig // Dual licensed under the MIT and GPL licenses. @@ -124,7 +109,7 @@ var mixinMessage; process.mixin = function() { if (!mixinMessage) { mixinMessage = 'deprecation warning: process.mixin will be removed from node-core future releases.\n' - process.stdio.writeError(mixinMessage); + process.binding('stdio').writeError(mixinMessage); } // copy reference to target object var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, source; @@ -162,7 +147,7 @@ process.mixin = function() { else { // Prevent never-ending loop if (target === d.value) { - continue; + return; } if (deep && d.value && typeof d.value === "object") { @@ -343,7 +328,7 @@ if ("NODE_DEBUG" in process.env) debugLevel = 1; function debug (x) { if (debugLevel > 0) { - process.stdio.writeError(x + "\n"); + process.binding('stdio').writeError(x + "\n"); } } @@ -780,6 +765,26 @@ Module.prototype._waitChildrenLoad = function (callback) { }; +var stdout; +process.__defineGetter__('stdout', function () { + if (stdout) return stdout; + var net = requireNative('net'); + stdout = new net.Stream(process.binding('stdio').stdoutFD); + return stdout; +}); + +var stdin; +process.openStdin = function () { + if (stdin) return stdin; + var net = requireNative('net'); + var fd = process.binding('stdio').openStdin(); + stdin = new net.Stream(fd); + stdin.resume(); + stdin.readable = true; + return stdin; +}; + + process.exit = function (code) { process.emit("exit"); process.reallyExit(code); diff --git a/src/node_buffer.cc b/src/node_buffer.cc new file mode 100644 index 00000000000..c16ead365e5 --- /dev/null +++ b/src/node_buffer.cc @@ -0,0 +1,426 @@ +#include + +#include +#include // malloc, free +#include + +#include // htons, htonl + +#include + +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +namespace node { + +using namespace v8; + +#define SLICE_ARGS(start_arg, end_arg) \ + if (!start_arg->IsInt32() || !end_arg->IsInt32()) { \ + return ThrowException(Exception::TypeError( \ + String::New("Bad argument."))); \ + } \ + int32_t start = start_arg->Int32Value(); \ + int32_t end = end_arg->Int32Value(); \ + if (start < 0 || end < 0) { \ + return ThrowException(Exception::TypeError( \ + String::New("Bad argument."))); \ + } \ + if (!(start <= end)) { \ + return ThrowException(Exception::Error( \ + String::New("Must have start <= end"))); \ + } \ + if ((size_t)end > parent->length_) { \ + return ThrowException(Exception::Error( \ + String::New("end cannot be longer than parent.length"))); \ + } + + +static Persistent length_symbol; +Persistent Buffer::constructor_template; + + +// Each javascript Buffer object is backed by a Blob object. +// the Blob is just a C-level chunk of bytes. +// It has a reference count. +struct Blob_ { + unsigned int refs; + size_t length; + char data[1]; +}; +typedef struct Blob_ Blob; + + +static inline Blob * blob_new(size_t length) { + size_t s = sizeof(Blob) - 1 + length; + Blob * blob = (Blob*) malloc(s); + if (!blob) return NULL; + V8::AdjustAmountOfExternalAllocatedMemory(s); + blob->length = length; + blob->refs = 0; + //fprintf(stderr, "alloc %d bytes\n", length); + return blob; +} + + +static inline void blob_ref(Blob *blob) { + blob->refs++; +} + + +static inline void blob_unref(Blob *blob) { + assert(blob->refs > 0); + if (--blob->refs == 0) { + //fprintf(stderr, "free %d bytes\n", blob->length); + size_t s = sizeof(Blob) - 1 + blob->length; + V8::AdjustAmountOfExternalAllocatedMemory(-s); + free(blob); + } +} + +#if 0 +// When someone calls buffer.asciiSlice, data is not copied. Instead V8 +// references in the underlying Blob with this ExternalAsciiStringResource. +class AsciiSliceExt: public String::ExternalAsciiStringResource { + friend class Buffer; + public: + AsciiSliceExt(Buffer *parent, size_t start, size_t end) { + blob_ = parent->blob(); + blob_ref(blob_); + + assert(start <= end); + length_ = end - start; + assert(start + length_ <= parent->length()); + data_ = parent->data() + start; + } + + + ~AsciiSliceExt() { + //fprintf(stderr, "free ascii slice (%d refs left)\n", blob_->refs); + blob_unref(blob_); + } + + + const char* data() const { return data_; } + size_t length() const { return length_; } + + private: + const char *data_; + size_t length_; + Blob *blob_; +}; +#endif + + +Handle Buffer::New(const Arguments &args) { + HandleScope scope; + + Buffer *buffer; + if (args[0]->IsInt32()) { + // var buffer = new Buffer(1024); + size_t length = args[0]->Uint32Value(); + buffer = new Buffer(length); + + } else if (Buffer::HasInstance(args[0]) && args.Length() > 2) { + // var slice = new Buffer(buffer, 123, 130); + // args: parent, start, end + Buffer *parent = ObjectWrap::Unwrap(args[0]->ToObject()); + SLICE_ARGS(args[1], args[2]) + buffer = new Buffer(parent, start, end); + } else { + return ThrowException(Exception::TypeError(String::New("Bad argument"))); + } + + buffer->Wrap(args.This()); + args.This()->SetIndexedPropertiesToExternalArrayData((void*)buffer->data_, + kExternalUnsignedByteArray, + buffer->length_); + args.This()->Set(length_symbol, Integer::New(buffer->length_)); + return args.This(); +} + + +Buffer::Buffer(size_t length) : ObjectWrap() { + blob_ = blob_new(length); + length_ = length; + data_ = blob_->data; + blob_ref(blob_); + + V8::AdjustAmountOfExternalAllocatedMemory(sizeof(Buffer)); +} + + +Buffer::Buffer(Buffer *parent, size_t start, size_t end) : ObjectWrap() { + blob_ = parent->blob_; + assert(blob_->refs > 0); + blob_ref(blob_); + + assert(start <= end); + length_ = end - start; + assert(length_ <= parent->length_); + data_ = parent->data_ + start; + + V8::AdjustAmountOfExternalAllocatedMemory(sizeof(Buffer)); +} + + +Buffer::~Buffer() { + assert(blob_->refs > 0); + //fprintf(stderr, "free buffer (%d refs left)\n", blob_->refs); + blob_unref(blob_); + V8::AdjustAmountOfExternalAllocatedMemory(-static_cast(sizeof(Buffer))); +} + + +Handle Buffer::BinarySlice(const Arguments &args) { + HandleScope scope; + Buffer *parent = ObjectWrap::Unwrap(args.This()); + SLICE_ARGS(args[0], args[1]) + + const char *data = const_cast(parent->data_ + start); + //Local string = String::New(data, end - start); + + Local b = Encode(data, end - start, BINARY); + + return scope.Close(b); +} + + +Handle Buffer::AsciiSlice(const Arguments &args) { + HandleScope scope; + Buffer *parent = ObjectWrap::Unwrap(args.This()); + SLICE_ARGS(args[0], args[1]) + +#if 0 + AsciiSliceExt *ext = new AsciiSliceExt(parent, start, end); + Local string = String::NewExternal(ext); + // There should be at least two references to the blob now - the parent + // and the slice. + assert(parent->blob_->refs >= 2); +#endif + + const char *data = const_cast(parent->data_ + start); + Local string = String::New(data, end - start); + + + return scope.Close(string); +} + + +Handle Buffer::Utf8Slice(const Arguments &args) { + HandleScope scope; + Buffer *parent = ObjectWrap::Unwrap(args.This()); + SLICE_ARGS(args[0], args[1]) + const char *data = const_cast(parent->data_ + start); + Local string = String::New(data, end - start); + return scope.Close(string); +} + + +Handle Buffer::Slice(const Arguments &args) { + HandleScope scope; + Local argv[3] = { args.This(), args[0], args[1] }; + Local slice = + constructor_template->GetFunction()->NewInstance(3, argv); + return scope.Close(slice); +} + + +// var charsWritten = buffer.utf8Write(string, offset); +Handle Buffer::Utf8Write(const Arguments &args) { + HandleScope scope; + Buffer *buffer = ObjectWrap::Unwrap(args.This()); + + if (!args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New( + "Argument must be a string"))); + } + + Local s = args[0]->ToString(); + + size_t offset = args[1]->Int32Value(); + + if (offset >= buffer->length_) { + return ThrowException(Exception::TypeError(String::New( + "Offset is out of bounds"))); + } + + const char *p = buffer->data_ + offset; + + int written = s->WriteUtf8((char*)p, buffer->length_ - offset); + + return scope.Close(Integer::New(written)); +} + + +// var charsWritten = buffer.asciiWrite(string, offset); +Handle Buffer::AsciiWrite(const Arguments &args) { + HandleScope scope; + + Buffer *buffer = ObjectWrap::Unwrap(args.This()); + + if (!args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New( + "Argument must be a string"))); + } + + Local s = args[0]->ToString(); + + size_t offset = args[1]->Int32Value(); + + if (offset >= buffer->length_) { + return ThrowException(Exception::TypeError(String::New( + "Offset is out of bounds"))); + } + + const char *p = buffer->data_ + offset; + + size_t towrite = MIN((unsigned long) s->Length(), buffer->length_ - offset); + + int written = s->WriteAscii((char*)p, 0, towrite); + return scope.Close(Integer::New(written)); +} + + +Handle Buffer::BinaryWrite(const Arguments &args) { + HandleScope scope; + + Buffer *buffer = ObjectWrap::Unwrap(args.This()); + + if (!args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New( + "Argument must be a string"))); + } + + Local s = args[0]->ToString(); + + size_t offset = args[1]->Int32Value(); + + if (offset >= buffer->length_) { + return ThrowException(Exception::TypeError(String::New( + "Offset is out of bounds"))); + } + + char *p = (char*)buffer->data_ + offset; + + size_t towrite = MIN((unsigned long) s->Length(), buffer->length_ - offset); + + int written = DecodeWrite(p, towrite, s, BINARY); + return scope.Close(Integer::New(written)); +} + + +// buffer.unpack(format, index); +// Starting at 'index', unpacks binary from the buffer into an array. +// 'format' is a string +// +// FORMAT RETURNS +// N uint32_t a 32bit unsigned integer in network byte order +// n uint16_t a 16bit unsigned integer in network byte order +// o uint8_t a 8bit unsigned integer +Handle Buffer::Unpack(const Arguments &args) { + HandleScope scope; + Buffer *buffer = ObjectWrap::Unwrap(args.This()); + + if (!args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New( + "Argument must be a string"))); + } + + String::AsciiValue format(args[0]->ToString()); + uint32_t index = args[1]->Uint32Value(); + +#define OUT_OF_BOUNDS ThrowException(Exception::Error(String::New("Out of bounds"))) + + Local array = Array::New(format.length()); + + uint8_t uint8; + uint16_t uint16; + uint32_t uint32; + + for (int i = 0; i < format.length(); i++) { + switch ((*format)[i]) { + // 32bit unsigned integer in network byte order + case 'N': + if (index + 3 >= buffer->length_) return OUT_OF_BOUNDS; + uint32 = htonl(*(uint32_t*)(buffer->data_ + index)); + array->Set(Integer::New(i), Integer::NewFromUnsigned(uint32)); + index += 4; + break; + + // 16bit unsigned integer in network byte order + case 'n': + if (index + 1 >= buffer->length_) return OUT_OF_BOUNDS; + uint16 = htons(*(uint16_t*)(buffer->data_ + index)); + array->Set(Integer::New(i), Integer::NewFromUnsigned(uint16)); + index += 2; + break; + + // a single octet, unsigned. + case 'o': + if (index >= buffer->length_) return OUT_OF_BOUNDS; + uint8 = (uint8_t)buffer->data_[index]; + array->Set(Integer::New(i), Integer::NewFromUnsigned(uint8)); + index += 1; + break; + + default: + return ThrowException(Exception::Error( + String::New("Unknown format character"))); + } + } + + return scope.Close(array); +} + + +// var nbytes = Buffer.byteLength("string", "utf8") +Handle Buffer::ByteLength(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New( + "Argument must be a string"))); + } + + Local s = args[0]->ToString(); + enum encoding e = ParseEncoding(args[1], UTF8); + + Local length = + Integer::New(e == UTF8 ? s->Utf8Length() : s->Length()); + + return scope.Close(length); +} + + +void Buffer::Initialize(Handle target) { + HandleScope scope; + + length_symbol = Persistent::New(String::NewSymbol("length")); + + Local t = FunctionTemplate::New(Buffer::New); + constructor_template = Persistent::New(t); + constructor_template->InstanceTemplate()->SetInternalFieldCount(1); + constructor_template->SetClassName(String::NewSymbol("Buffer")); + + // copy free + NODE_SET_PROTOTYPE_METHOD(constructor_template, "binarySlice", Buffer::BinarySlice); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "asciiSlice", Buffer::AsciiSlice); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "slice", Buffer::Slice); + // TODO NODE_SET_PROTOTYPE_METHOD(t, "utf16Slice", Utf16Slice); + // copy + NODE_SET_PROTOTYPE_METHOD(constructor_template, "utf8Slice", Buffer::Utf8Slice); + + NODE_SET_PROTOTYPE_METHOD(constructor_template, "utf8Write", Buffer::Utf8Write); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "asciiWrite", Buffer::AsciiWrite); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "binaryWrite", Buffer::BinaryWrite); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "unpack", Buffer::Unpack); + + NODE_SET_METHOD(constructor_template->GetFunction(), + "byteLength", + Buffer::ByteLength); + + target->Set(String::NewSymbol("Buffer"), constructor_template->GetFunction()); +} + + +} // namespace node diff --git a/src/node_buffer.h b/src/node_buffer.h new file mode 100644 index 00000000000..9f497c5419f --- /dev/null +++ b/src/node_buffer.h @@ -0,0 +1,73 @@ +#ifndef NODE_BUFFER_H_ +#define NODE_BUFFER_H_ + +#include +#include +#include + +namespace node { + +/* A buffer is a chunk of memory stored outside the V8 heap, mirrored by an + * object in javascript. The object is not totally opaque, one can access + * individual bytes with [] and slice it into substrings or sub-buffers + * without copying memory. + * + * // return an ascii encoded string - no memory iscopied + * buffer.asciiSlide(0, 3) + * + * // returns another buffer - no memory is copied + * buffer.slice(0, 3) + * + * Interally, each javascript buffer object is backed by a "struct buffer" + * object. These "struct buffer" objects are either a root buffer (in the + * case that buffer->root == NULL) or slice objects (in which case + * buffer->root != NULL). A root buffer is only GCed once all its slices + * are GCed. + */ + + +struct Blob_; + +class Buffer : public ObjectWrap { + public: + static void Initialize(v8::Handle target); + static inline bool HasInstance(v8::Handle val) { + if (!val->IsObject()) return false; + v8::Local obj = val->ToObject(); + return constructor_template->HasInstance(obj); + } + + const char* data() const { return data_; } + size_t length() const { return length_; } + struct Blob_* blob() const { return blob_; } + + protected: + static v8::Persistent constructor_template; + static v8::Handle New(const v8::Arguments &args); + static v8::Handle Slice(const v8::Arguments &args); + static v8::Handle BinarySlice(const v8::Arguments &args); + static v8::Handle AsciiSlice(const v8::Arguments &args); + static v8::Handle Utf8Slice(const v8::Arguments &args); + static v8::Handle BinaryWrite(const v8::Arguments &args); + static v8::Handle AsciiWrite(const v8::Arguments &args); + static v8::Handle Utf8Write(const v8::Arguments &args); + static v8::Handle ByteLength(const v8::Arguments &args); + static v8::Handle Unpack(const v8::Arguments &args); + + int AsciiWrite(char *string, int offset, int length); + int Utf8Write(char *string, int offset, int length); + + private: + Buffer(size_t length); + Buffer(Buffer *parent, size_t start, size_t end); + ~Buffer(); + + const char *data_; + size_t length_; + struct Blob_ *blob_; +}; + + +} // namespace node buffer + +#endif // NODE_BUFFER_H_ diff --git a/src/node_child_process.cc b/src/node_child_process.cc index 6c09ee0f009..870269f5fdb 100644 --- a/src/node_child_process.cc +++ b/src/node_child_process.cc @@ -15,56 +15,55 @@ namespace node { using namespace v8; -Persistent ChildProcess::constructor_template; - static Persistent pid_symbol; -static Persistent exit_symbol; -static Persistent output_symbol; -static Persistent error_symbol; +static Persistent onexit_symbol; + + +// TODO share with other modules +static inline int SetNonBlocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + int r = fcntl(fd, F_SETFL, flags | O_NONBLOCK); + if (r != 0) { + perror("SetNonBlocking()"); + } + return r; +} + void ChildProcess::Initialize(Handle target) { HandleScope scope; Local t = FunctionTemplate::New(ChildProcess::New); - constructor_template = Persistent::New(t); - constructor_template->Inherit(EventEmitter::constructor_template); - constructor_template->InstanceTemplate()->SetInternalFieldCount(1); - constructor_template->SetClassName(String::NewSymbol("ChildProcess")); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("ChildProcess")); pid_symbol = NODE_PSYMBOL("pid"); - exit_symbol = NODE_PSYMBOL("exit"); - output_symbol = NODE_PSYMBOL("output"); - error_symbol = NODE_PSYMBOL("error"); + onexit_symbol = NODE_PSYMBOL("onexit"); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "spawn", ChildProcess::Spawn); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "write", ChildProcess::Write); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "close", ChildProcess::Close); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "kill", ChildProcess::Kill); + NODE_SET_PROTOTYPE_METHOD(t, "spawn", ChildProcess::Spawn); + NODE_SET_PROTOTYPE_METHOD(t, "kill", ChildProcess::Kill); - target->Set(String::NewSymbol("ChildProcess"), - constructor_template->GetFunction()); + target->Set(String::NewSymbol("ChildProcess"), t->GetFunction()); } + Handle ChildProcess::New(const Arguments& args) { HandleScope scope; - ChildProcess *p = new ChildProcess(); p->Wrap(args.Holder()); - return args.This(); } + // This is an internal function. The third argument should be an array // of key value pairs seperated with '='. Handle ChildProcess::Spawn(const Arguments& args) { HandleScope scope; - if ( args.Length() != 3 - || !args[0]->IsString() - || !args[1]->IsArray() - || !args[2]->IsArray() - ) - { + if (args.Length() != 3 || + !args[0]->IsString() || + !args[1]->IsArray() || + !args[2]->IsArray()) { return ThrowException(Exception::Error(String::New("Bad argument."))); } @@ -98,7 +97,9 @@ Handle ChildProcess::Spawn(const Arguments& args) { env[i] = strdup(*pair); } - int r = child->Spawn(argv[0], argv, env); + int fds[3]; + + int r = child->Spawn(argv[0], argv, env, fds); for (i = 0; i < argv_length; i++) free(argv[i]); delete [] argv; @@ -110,32 +111,18 @@ Handle ChildProcess::Spawn(const Arguments& args) { return ThrowException(Exception::Error(String::New("Error spawning"))); } - child->handle_->Set(pid_symbol, Integer::New(child->pid_)); + Local a = Array::New(3); - return Undefined(); + assert(fds[0] >= 0); + a->Set(0, Integer::New(fds[0])); // stdin + assert(fds[1] >= 0); + a->Set(1, Integer::New(fds[1])); // stdout + assert(fds[2] >= 0); + a->Set(2, Integer::New(fds[2])); // stderr + + return scope.Close(a); } -Handle ChildProcess::Write(const Arguments& args) { - HandleScope scope; - ChildProcess *child = ObjectWrap::Unwrap(args.Holder()); - assert(child); - - enum encoding enc = ParseEncoding(args[1]); - ssize_t len = DecodeBytes(args[0], enc); - - if (len < 0) { - Local exception = Exception::TypeError(String::New("Bad argument")); - return ThrowException(exception); - } - - char * buf = new char[len]; - ssize_t written = DecodeWrite(buf, len, args[0], enc); - assert(written == len); - int r = child->Write(buf, len); - delete [] buf; - - return r == 0 ? True() : False(); -} Handle ChildProcess::Kill(const Arguments& args) { HandleScope scope; @@ -167,160 +154,59 @@ Handle ChildProcess::Kill(const Arguments& args) { return Undefined(); } -Handle ChildProcess::Close(const Arguments& args) { - HandleScope scope; - ChildProcess *child = ObjectWrap::Unwrap(args.Holder()); - assert(child); - return child->Close() == 0 ? True() : False(); -} -void ChildProcess::reader_closed(evcom_reader *r) { - ChildProcess *child = static_cast(r->data); - if (r == &child->stdout_reader_) { - child->stdout_fd_ = -1; - } else { - assert(r == &child->stderr_reader_); - child->stderr_fd_ = -1; +void ChildProcess::Stop() { + if (ev_is_active(&child_watcher_)) { + ev_child_stop(EV_DEFAULT_UC_ &child_watcher_); + Unref(); } - evcom_reader_detach(r); - child->MaybeShutdown(); + // Don't kill the PID here. We want to allow for killing the parent + // process and reparenting to initd. This is perhaps not going the best + // technique for daemonizing, but I don't want to rule it out. + pid_ = -1; } -void ChildProcess::stdin_closed(evcom_writer *w) { - ChildProcess *child = static_cast(w->data); - assert(w == &child->stdin_writer_); - child->stdin_fd_ = -1; - evcom_writer_detach(w); - child->MaybeShutdown(); -} - -void ChildProcess::on_read(evcom_reader *r, const void *buf, size_t len) { - ChildProcess *child = static_cast(r->data); - HandleScope scope; - - bool isSTDOUT = (r == &child->stdout_reader_); - enum encoding encoding = isSTDOUT ? - child->stdout_encoding_ : child->stderr_encoding_; - - // TODO emit 'end' event instead of null. - - Local data = len ? Encode(buf, len, encoding) : Local::New(Null()); - child->Emit(isSTDOUT ? output_symbol : error_symbol, 1, &data); - child->MaybeShutdown(); -} - -ChildProcess::ChildProcess() : EventEmitter() { - evcom_reader_init(&stdout_reader_); - stdout_reader_.data = this; - stdout_reader_.on_read = on_read; - stdout_reader_.on_close = reader_closed; - - evcom_reader_init(&stderr_reader_); - stderr_reader_.data = this; - stderr_reader_.on_read = on_read; - stderr_reader_.on_close = reader_closed; - - evcom_writer_init(&stdin_writer_); - stdin_writer_.data = this; - stdin_writer_.on_close = stdin_closed; - - ev_init(&child_watcher_, ChildProcess::OnCHLD); - child_watcher_.data = this; - - stdout_fd_ = -1; - stderr_fd_ = -1; - stdin_fd_ = -1; - - stdout_encoding_ = UTF8; - stderr_encoding_ = UTF8; - - got_chld_ = false; - exit_code_ = 0; - - pid_ = 0; -} - -ChildProcess::~ChildProcess() { - Shutdown(); -} - -void ChildProcess::Shutdown() { - if (stdin_fd_ >= 0) { - evcom_writer_close(&stdin_writer_); - } - - if (stdin_fd_ >= 0) close(stdin_fd_); - if (stdout_fd_ >= 0) close(stdout_fd_); - if (stderr_fd_ >= 0) close(stderr_fd_); - - stdin_fd_ = -1; - stdout_fd_ = -1; - stderr_fd_ = -1; - - evcom_writer_detach(&stdin_writer_); - evcom_reader_detach(&stdout_reader_); - evcom_reader_detach(&stderr_reader_); - - ev_child_stop(EV_DEFAULT_UC_ &child_watcher_); - - /* XXX Kill the PID? */ - pid_ = 0; -} - -static inline int SetNonBlocking(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - int r = fcntl(fd, F_SETFL, flags | O_NONBLOCK); - if (r != 0) { - perror("SetNonBlocking()"); - } - return r; -} // Note that args[0] must be the same as the "file" param. This is an // execvp() requirement. -int ChildProcess::Spawn(const char *file, char *const args[], char **env) { - assert(pid_ == 0); - assert(stdout_fd_ == -1); - assert(stderr_fd_ == -1); - assert(stdin_fd_ == -1); +// +int ChildProcess::Spawn(const char *file, + char *const args[], + char **env, + int stdio_fds[3]) { + HandleScope scope; + assert(pid_ == -1); + assert(!ev_is_active(&child_watcher_)); - int stdout_pipe[2], stdin_pipe[2], stderr_pipe[2]; + int stdin_pipe[2], stdout_pipe[2], stderr_pipe[2]; /* An implementation of popen(), basically */ - if (pipe(stdout_pipe) < 0) { + if (pipe(stdin_pipe) < 0 || + pipe(stdout_pipe) < 0 || + pipe(stderr_pipe) < 0) { perror("pipe()"); return -1; } - if (pipe(stderr_pipe) < 0) { - perror("pipe()"); - return -2; - } - - if (pipe(stdin_pipe) < 0) { - perror("pipe()"); - return -3; - } - // Save environ in the case that we get it clobbered // by the child process. char **save_our_env = environ; switch (pid_ = vfork()) { case -1: // Error. - Shutdown(); + Stop(); return -4; case 0: // Child. + close(stdin_pipe[1]); // close write end + dup2(stdin_pipe[0], STDIN_FILENO); + close(stdout_pipe[0]); // close read end dup2(stdout_pipe[1], STDOUT_FILENO); close(stderr_pipe[0]); // close read end dup2(stderr_pipe[1], STDERR_FILENO); - close(stdin_pipe[1]); // close write end - dup2(stdin_pipe[0], STDIN_FILENO); - environ = env; execvp(file, args); @@ -328,81 +214,59 @@ int ChildProcess::Spawn(const char *file, char *const args[], char **env) { _exit(127); } + // Parent. + // Restore environment. environ = save_our_env; - // Parent. - ev_child_set(&child_watcher_, pid_, 0); ev_child_start(EV_DEFAULT_UC_ &child_watcher_); - - close(stdout_pipe[1]); - stdout_fd_ = stdout_pipe[0]; - SetNonBlocking(stdout_fd_); - - close(stderr_pipe[1]); - stderr_fd_ = stderr_pipe[0]; - SetNonBlocking(stderr_fd_); + Ref(); + handle_->Set(pid_symbol, Integer::New(pid_)); close(stdin_pipe[0]); - stdin_fd_ = stdin_pipe[1]; - SetNonBlocking(stdin_fd_); + stdio_fds[0] = stdin_pipe[1]; + SetNonBlocking(stdin_pipe[1]); - evcom_reader_set(&stdout_reader_, stdout_fd_); - evcom_reader_attach(EV_DEFAULT_UC_ &stdout_reader_); + close(stdout_pipe[1]); + stdio_fds[1] = stdout_pipe[0]; + SetNonBlocking(stdout_pipe[0]); - evcom_reader_set(&stderr_reader_, stderr_fd_); - evcom_reader_attach(EV_DEFAULT_UC_ &stderr_reader_); - - evcom_writer_set(&stdin_writer_, stdin_fd_); - evcom_writer_attach(EV_DEFAULT_UC_ &stdin_writer_); - - Ref(); + close(stderr_pipe[1]); + stdio_fds[2] = stderr_pipe[0]; + SetNonBlocking(stderr_pipe[0]); return 0; } -void ChildProcess::OnCHLD(EV_P_ ev_child *watcher, int revents) { - ev_child_stop(EV_A_ watcher); - ChildProcess *child = static_cast(watcher->data); - assert(revents == EV_CHILD); - assert(child->pid_ == watcher->rpid); - assert(&child->child_watcher_ == watcher); +void ChildProcess::OnExit(int code) { + HandleScope scope; - child->got_chld_ = true; - child->exit_code_ = watcher->rstatus; + pid_ = -1; + Stop(); - if (child->stdin_fd_ >= 0) evcom_writer_close(&child->stdin_writer_); + handle_->Set(pid_symbol, Null()); - child->MaybeShutdown(); -} + Local onexit_v = handle_->Get(onexit_symbol); + assert(onexit_v->IsFunction()); + Local onexit = Local::Cast(onexit_v); -int ChildProcess::Write(const char *str, size_t len) { - if (stdin_fd_ < 0 || got_chld_) return -1; - evcom_writer_write(&stdin_writer_, str, len); - return 0; -} + TryCatch try_catch; -int ChildProcess::Close(void) { - if (stdin_fd_ < 0 || got_chld_) return -1; - evcom_writer_close(&stdin_writer_); - return 0; -} + Local argv[1]; + argv[0] = Integer::New(code); -int ChildProcess::Kill(int sig) { - if (got_chld_ || pid_ == 0) return -1; - return kill(pid_, sig); -} + onexit->Call(handle_, 1, argv); -void ChildProcess::MaybeShutdown(void) { - if (stdout_fd_ < 0 && stderr_fd_ < 0 && got_chld_) { - HandleScope scope; - Handle argv[1] = { Integer::New(exit_code_) }; - Emit(exit_symbol, 1, argv); - Shutdown(); - Unref(); + if (try_catch.HasCaught()) { + FatalException(try_catch); } } + +int ChildProcess::Kill(int sig) { + return kill(pid_, sig); +} + } // namespace node diff --git a/src/node_child_process.h b/src/node_child_process.h index b37db9f36ae..14a13ecf422 100644 --- a/src/node_child_process.h +++ b/src/node_child_process.h @@ -1,65 +1,68 @@ // Copyright 2009 Ryan Dahl -#ifndef SRC_CHILD_PROCESS_H_ -#define SRC_CHILD_PROCESS_H_ +#ifndef NODE_CHILD_PROCESS_H_ +#define NODE_CHILD_PROCESS_H_ #include -#include - +#include #include #include -#include + +// ChildProcess is a thin wrapper around ev_child. It has the extra +// functionality that it can spawn a child process with pipes connected to +// its stdin, stdout, stderr. This class is not meant to be exposed to but +// wrapped up in a more friendly EventEmitter with streams for each of the +// pipes. +// +// When the child process exits (when the parent receives SIGCHLD) the +// callback child.onexit will be called. namespace node { -class ChildProcess : EventEmitter { +class ChildProcess : ObjectWrap { public: static void Initialize(v8::Handle target); protected: - static v8::Persistent constructor_template; static v8::Handle New(const v8::Arguments& args); static v8::Handle Spawn(const v8::Arguments& args); - static v8::Handle Write(const v8::Arguments& args); - static v8::Handle Close(const v8::Arguments& args); static v8::Handle Kill(const v8::Arguments& args); - static v8::Handle PIDGetter(v8::Local _, - const v8::AccessorInfo& info); - ChildProcess(); - ~ChildProcess(); + ChildProcess() : ObjectWrap() { + ev_init(&child_watcher_, ChildProcess::on_chld); + child_watcher_.data = this; + pid_ = -1; + } - int Spawn(const char *file, char *const argv[], char **env); - int Write(const char *str, size_t len); - int Close(void); + ~ChildProcess() { + Stop(); + } + + // Returns 0 on success. stdio_fds will contain file desciptors for stdin, + // stdout, and stderr of the subprocess. stdin is writable; the other two + // are readable. + // The user of this class has responsibility to close these pipes after + // the child process exits. + int Spawn(const char *file, char *const argv[], char **env, int stdio_fds[3]); + + // Simple syscall wrapper. Does not disable the watcher. onexit will be + // called still. int Kill(int sig); private: - static void on_read(evcom_reader *r, const void *buf, size_t len); - static void reader_closed(evcom_reader *r); - static void stdin_closed(evcom_writer *w); - static void OnCHLD(EV_P_ ev_child *watcher, int revents); + void OnExit(int code); + void Stop(void); - void MaybeShutdown(void); - void Shutdown(void); - - evcom_reader stdout_reader_; - evcom_reader stderr_reader_; - evcom_writer stdin_writer_; + static void on_chld(EV_P_ ev_child *watcher, int revents) { + ChildProcess *child = static_cast(watcher->data); + assert(revents == EV_CHILD); + assert(child->pid_ == watcher->rpid); + assert(&child->child_watcher_ == watcher); + child->OnExit(watcher->rstatus); + } ev_child child_watcher_; - - int stdout_fd_; - int stderr_fd_; - int stdin_fd_; - - enum encoding stdout_encoding_; - enum encoding stderr_encoding_; - pid_t pid_; - - bool got_chld_; - int exit_code_; }; } // namespace node -#endif // SRC_CHILD_PROCESS_H_ +#endif // NODE_CHILD_PROCESS_H_ diff --git a/src/node_dns.cc b/src/node_dns.cc index 3898c1aff07..e41c2e38adc 100644 --- a/src/node_dns.cc +++ b/src/node_dns.cc @@ -1,5 +1,6 @@ // Copyright 2009 Ryan Dahl #include +#include #include /* exit() */ #include @@ -26,24 +27,6 @@ static Persistent weight_symbol; static Persistent port_symbol; static Persistent name_symbol; -static inline Persistent* cb_persist(const Local &v) { - Persistent *fn = new Persistent(); - *fn = Persistent::New(Local::Cast(v)); - return fn; -} - -static inline Persistent* cb_unwrap(void *data) { - Persistent *cb = - reinterpret_cast*>(data); - assert((*cb)->IsFunction()); - return cb; -} - -static inline void cb_destroy(Persistent * cb) { - cb->Dispose(); - delete cb; -} - static inline void set_timeout() { int maxwait = 20; int wait = dns_timeouts(NULL, maxwait, ev_now(EV_DEFAULT_UC)); diff --git a/src/node_file.cc b/src/node_file.cc index 285ce4bb654..5ca5416506c 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -1,5 +1,6 @@ // Copyright 2009 Ryan Dahl #include +#include #include #include @@ -37,9 +38,7 @@ static inline Local errno_exception(int errorno) { static int After(eio_req *req) { HandleScope scope; - Persistent *callback = - reinterpret_cast*>(req->data); - assert((*callback)->IsFunction()); + Persistent *callback = cb_unwrap(req->data); ev_unref(EV_DEFAULT_UC); @@ -86,7 +85,7 @@ static int After(eio_req *req) { argv[1] = BuildStatsObject(s); break; } - + case EIO_READLINK: { argc = 2; @@ -133,7 +132,7 @@ static int After(eio_req *req) { } } - if (req->type == EIO_WRITE) { + if (req->type == EIO_WRITE && req->int3 == 1) { assert(req->ptr2); delete [] reinterpret_cast(req->ptr2); } @@ -147,21 +146,14 @@ static int After(eio_req *req) { } // Dispose of the persistent handle - callback->Dispose(); - delete callback; + cb_destroy(callback); return 0; } -static Persistent* persistent_callback(const Local &v) { - Persistent *fn = new Persistent(); - *fn = Persistent::New(Local::Cast(v)); - return fn; -} - #define ASYNC_CALL(func, callback, ...) \ eio_req *req = eio_##func(__VA_ARGS__, EIO_PRI_DEFAULT, After, \ - persistent_callback(callback)); \ + cb_persist(callback)); \ assert(req); \ ev_ref(EV_DEFAULT_UC); \ return Undefined(); @@ -456,43 +448,118 @@ static Handle Open(const Arguments& args) { } } -/* node.fs.write(fd, data, position, enc, callback) - * Wrapper for write(2). - * - * 0 fd integer. file descriptor - * 1 data the data to write (string = utf8, array = raw) - * 2 position if integer, position to write at in the file. - * if null, write from the current position - * 3 encoding - */ +// write(fd, data, position, enc, callback) +// Wrapper for write(2). +// +// 0 fd integer. file descriptor +// 1 buffer the data to write +// 2 offset where in the buffer to start from +// 3 length how much to write +// 4 position if integer, position to write at in the file. +// if null, write from the current position +// +// - OR - +// +// 0 fd integer. file descriptor +// 1 string the data to write +// 2 position if integer, position to write at in the file. +// if null, write from the current position +// 3 encoding static Handle Write(const Arguments& args) { HandleScope scope; - if (args.Length() < 4 || !args[0]->IsInt32()) { + if (!args[0]->IsInt32()) { return THROW_BAD_ARGS; } int fd = args[0]->Int32Value(); - off_t offset = args[2]->IsNumber() ? args[2]->IntegerValue() : -1; - enum encoding enc = ParseEncoding(args[3]); - ssize_t len = DecodeBytes(args[1], enc); - if (len < 0) { - Local exception = Exception::TypeError(String::New("Bad argument")); - return ThrowException(exception); + off_t pos; + ssize_t len; + char * buf; + ssize_t written; + + Local cb; + bool legacy; + + if (Buffer::HasInstance(args[1])) { + legacy = false; + // buffer + // + // 0 fd integer. file descriptor + // 1 buffer the data to write + // 2 offset where in the buffer to start from + // 3 length how much to write + // 4 position if integer, position to write at in the file. + // if null, write from the current position + + Buffer * buffer = ObjectWrap::Unwrap(args[1]->ToObject()); + + size_t off = args[2]->Int32Value(); + if (off >= buffer->length()) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + len = args[3]->Int32Value(); + if (off + len > buffer->length()) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + pos = args[4]->IsNumber() ? args[4]->IntegerValue() : -1; + + buf = (char*)buffer->data() + off; + + cb = args[5]; + + } else { + legacy = true; + // legacy interface.. args[1] is a string + + pos = args[2]->IsNumber() ? args[2]->IntegerValue() : -1; + + enum encoding enc = ParseEncoding(args[3]); + + len = DecodeBytes(args[1], enc); + if (len < 0) { + Local exception = Exception::TypeError(String::New("Bad argument")); + return ThrowException(exception); + } + + buf = new char[len]; + written = DecodeWrite(buf, len, args[1], enc); + assert(written == len); + + cb = args[4]; } - char * buf = new char[len]; - ssize_t written = DecodeWrite(buf, len, args[1], enc); - assert(written == len); + if (cb->IsFunction()) { + // WARNING: HACK AHEAD, PROCEED WITH CAUTION + // Normally here I would do + // ASYNC_CALL(write, cb, fd, buf, len, pos) + // however, I'm trying to support a legacy interface; where in the + // String version a buffer is allocated to encode into. In the After() + // function it is freed. However in the other version, we just increase + // the reference count to a buffer. We have to let After() know which + // version is being done so it can know if it needs to free 'buf' or + // not. We do that by using req->int3. + // req->int3 == 1 legacy, String version. Need to free `buf`. + // req->int3 == 0 Buffer version. Don't do anything. + eio_req *req = eio_write(fd, buf, len, pos, + EIO_PRI_DEFAULT, + After, + cb_persist(cb)); + assert(req); + req->int3 = legacy ? 1 : 0; + ev_ref(EV_DEFAULT_UC); + return Undefined(); - if (args[4]->IsFunction()) { - ASYNC_CALL(write, args[4], fd, buf, len, offset) } else { - if (offset < 0) { + if (pos < 0) { written = write(fd, buf, len); } else { - written = pwrite(fd, buf, len, offset); + written = pwrite(fd, buf, len, pos); } if (written < 0) return ThrowException(errno_exception(errno)); return scope.Close(Integer::New(written)); diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc new file mode 100644 index 00000000000..083f496dea9 --- /dev/null +++ b/src/node_http_parser.cc @@ -0,0 +1,362 @@ +#include + +#include +#include +#include + +#include + +#include /* strcasecmp() */ + +// This is a binding to http_parser (http://github.com/ry/http-parser) +// The goal is to decouple sockets from parsing for more javascript-level +// agility. A Buffer is read from a socket and passed to parser.execute(). +// The parser then issues callbacks with slices of the data +// parser.onMessageBegin +// parser.onPath +// parser.onBody +// ... +// No copying is performed when slicing the buffer, only small reference +// allocations. + + #include + #include + #include + +namespace node { + +using namespace v8; + + static int deep = 0; + +static Persistent on_message_begin_sym; +static Persistent on_path_sym; +static Persistent on_query_string_sym; +static Persistent on_url_sym; +static Persistent on_fragment_sym; +static Persistent on_header_field_sym; +static Persistent on_header_value_sym; +static Persistent on_headers_complete_sym; +static Persistent on_body_sym; +static Persistent on_message_complete_sym; + +static Persistent delete_sym; +static Persistent get_sym; +static Persistent head_sym; +static Persistent post_sym; +static Persistent put_sym; +static Persistent connect_sym; +static Persistent options_sym; +static Persistent trace_sym; +static Persistent copy_sym; +static Persistent lock_sym; +static Persistent mkcol_sym; +static Persistent move_sym; +static Persistent propfind_sym; +static Persistent proppatch_sym; +static Persistent unlock_sym; +static Persistent unknown_method_sym; + +static Persistent method_sym; +static Persistent status_code_sym; +static Persistent http_version_sym; +static Persistent version_major_sym; +static Persistent version_minor_sym; +static Persistent should_keep_alive_sym; + +// Callback prototype for http_cb +#define DEFINE_HTTP_CB(name) \ + static int name(http_parser *p) { \ + Parser *parser = static_cast(p->data); \ + Local cb_value = parser->handle_->Get(name##_sym); \ + if (!cb_value->IsFunction()) return 0; \ + Local cb = Local::Cast(cb_value); \ + Local ret = cb->Call(parser->handle_, 0, NULL); \ + return ret.IsEmpty() ? -1 : 0; \ + } + +// Callback prototype for http_data_cb +#define DEFINE_HTTP_DATA_CB(name) \ + static int name(http_parser *p, const char *at, size_t length) { \ + Parser *parser = static_cast(p->data); \ + assert(parser->buffer_); \ + Local cb_value = parser->handle_->Get(name##_sym); \ + if (!cb_value->IsFunction()) return 0; \ + Local cb = Local::Cast(cb_value); \ + Local argv[3] = { Local::New(parser->buffer_->handle_) \ + , Integer::New(at - parser->buffer_->data()) \ + , Integer::New(length) \ + }; \ + Local ret = cb->Call(parser->handle_, 3, argv); \ + assert(parser->buffer_); \ + return ret.IsEmpty() ? -1 : 0; \ + } + + +static inline Persistent +method_to_str(enum http_method m) { + switch (m) { + case HTTP_DELETE: return delete_sym; + case HTTP_GET: return get_sym; + case HTTP_HEAD: return head_sym; + case HTTP_POST: return post_sym; + case HTTP_PUT: return put_sym; + case HTTP_CONNECT: return connect_sym; + case HTTP_OPTIONS: return options_sym; + case HTTP_TRACE: return trace_sym; + case HTTP_COPY: return copy_sym; + case HTTP_LOCK: return lock_sym; + case HTTP_MKCOL: return mkcol_sym; + case HTTP_MOVE: return move_sym; + case HTTP_PROPFIND: return propfind_sym; + case HTTP_PROPPATCH: return proppatch_sym; + case HTTP_UNLOCK: return unlock_sym; + default: return unknown_method_sym; + } +} + + +class Parser : public ObjectWrap { + public: + Parser(enum http_parser_type type) : ObjectWrap() { + buffer_ = NULL; + Init(type); + } + + ~Parser() { + assert(buffer_ == NULL && "Destroying a parser while it's parsing"); + } + + DEFINE_HTTP_CB(on_message_begin) + DEFINE_HTTP_CB(on_message_complete) + + DEFINE_HTTP_DATA_CB(on_path) + DEFINE_HTTP_DATA_CB(on_url) + DEFINE_HTTP_DATA_CB(on_fragment) + DEFINE_HTTP_DATA_CB(on_query_string) + DEFINE_HTTP_DATA_CB(on_header_field) + DEFINE_HTTP_DATA_CB(on_header_value) + DEFINE_HTTP_DATA_CB(on_body) + + static int on_headers_complete(http_parser *p) { + Parser *parser = static_cast(p->data); + + Local cb_value = parser->handle_->Get(on_headers_complete_sym); + if (!cb_value->IsFunction()) return 0; + Local cb = Local::Cast(cb_value); + + + Local message_info = Object::New(); + + // METHOD + if (p->type == HTTP_REQUEST) { + message_info->Set(method_sym, method_to_str(p->method)); + } + + // STATUS + if (p->type == HTTP_RESPONSE) { + message_info->Set(status_code_sym, Integer::New(p->status_code)); + } + + // VERSION + message_info->Set(version_major_sym, Integer::New(p->http_major)); + message_info->Set(version_minor_sym, Integer::New(p->http_minor)); + + message_info->Set(should_keep_alive_sym, + http_should_keep_alive(p) ? True() : False()); + + Local argv[1] = { message_info }; + + Local ret = cb->Call(parser->handle_, 1, argv); + + return ret.IsEmpty() ? -1 : 0; + } + + static Handle New(const Arguments& args) { + HandleScope scope; + + String::Utf8Value type(args[0]->ToString()); + + Parser *parser; + + if (0 == strcasecmp(*type, "request")) { + parser = new Parser(HTTP_REQUEST); + } else if (0 == strcasecmp(*type, "response")) { + parser = new Parser(HTTP_RESPONSE); + } else { + return ThrowException(Exception::Error( + String::New("Constructor argument be 'request' or 'response'"))); + } + + parser->Wrap(args.This()); + assert(!parser->buffer_); + + return args.This(); + } + + // var bytesParsed = parser->execute(buffer, off, len); + static Handle Execute(const Arguments& args) { + HandleScope scope; + + Parser *parser = ObjectWrap::Unwrap(args.This()); + + assert(!parser->buffer_); + if (parser->buffer_) { + return ThrowException(Exception::TypeError( + String::New("Already parsing a buffer"))); + } + + if (!Buffer::HasInstance(args[0])) { + return ThrowException(Exception::TypeError( + String::New("Argument should be a buffer"))); + } + + Buffer * buffer = ObjectWrap::Unwrap(args[0]->ToObject()); + + size_t off = args[1]->Int32Value(); + if (off >= buffer->length()) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + size_t len = args[2]->Int32Value(); + if (off+len > buffer->length()) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + TryCatch try_catch; + + // Assign 'buffer_' while we parse. The callbacks will access that varible. + parser->buffer_ = buffer; + + size_t nparsed = + http_parser_execute(&parser->parser_, buffer->data()+off, len); + + // Unassign the 'buffer_' variable + assert(parser->buffer_); + parser->buffer_ = NULL; + + // If there was an exception in one of the callbacks + if (try_catch.HasCaught()) return try_catch.ReThrow(); + + Local nparsed_obj = Integer::New(nparsed); + // If there was a parse error in one of the callbacks + // TODO What if there is an error on EOF? + if (nparsed != len) { + Local e = Exception::Error(String::New("Parse Error")); + Local obj = e->ToObject(); + obj->Set(String::NewSymbol("bytesParsed"), nparsed_obj); + return ThrowException(e); + } + + assert(!parser->buffer_); + return scope.Close(nparsed_obj); + } + + static Handle Finish(const Arguments& args) { + HandleScope scope; + + Parser *parser = ObjectWrap::Unwrap(args.This()); + + assert(!parser->buffer_); + + http_parser_execute(&(parser->parser_), NULL, 0); + + return Undefined(); + } + + static Handle Reinitialize(const Arguments& args) { + HandleScope scope; + Parser *parser = ObjectWrap::Unwrap(args.This()); + + String::Utf8Value type(args[0]->ToString()); + + if (0 == strcasecmp(*type, "request")) { + parser->Init(HTTP_REQUEST); + } else if (0 == strcasecmp(*type, "response")) { + parser->Init(HTTP_RESPONSE); + } else { + return ThrowException(Exception::Error( + String::New("Argument be 'request' or 'response'"))); + } + return Undefined(); + } + + + private: + + void Init (enum http_parser_type type) { + assert(buffer_ == NULL); // don't call this during Execute() + http_parser_init(&parser_, type); + + parser_.on_message_begin = on_message_begin; + parser_.on_path = on_path; + parser_.on_query_string = on_query_string; + parser_.on_url = on_url; + parser_.on_fragment = on_fragment; + parser_.on_header_field = on_header_field; + parser_.on_header_value = on_header_value; + parser_.on_headers_complete = on_headers_complete; + parser_.on_body = on_body; + parser_.on_message_complete = on_message_complete; + + parser_.data = this; + } + + Buffer * buffer_; // The buffer currently being parsed. + http_parser parser_; +}; + + +void InitHttpParser(Handle target) { + HandleScope scope; + + Local t = FunctionTemplate::New(Parser::New); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("HTTPParser")); + + NODE_SET_PROTOTYPE_METHOD(t, "execute", Parser::Execute); + NODE_SET_PROTOTYPE_METHOD(t, "finish", Parser::Finish); + NODE_SET_PROTOTYPE_METHOD(t, "reinitialize", Parser::Reinitialize); + + target->Set(String::NewSymbol("HTTPParser"), t->GetFunction()); + + on_message_begin_sym = NODE_PSYMBOL("onMessageBegin"); + on_path_sym = NODE_PSYMBOL("onPath"); + on_query_string_sym = NODE_PSYMBOL("onQueryString"); + on_url_sym = NODE_PSYMBOL("onURL"); + on_fragment_sym = NODE_PSYMBOL("onFragment"); + on_header_field_sym = NODE_PSYMBOL("onHeaderField"); + on_header_value_sym = NODE_PSYMBOL("onHeaderValue"); + on_headers_complete_sym = NODE_PSYMBOL("onHeadersComplete"); + on_body_sym = NODE_PSYMBOL("onBody"); + on_message_complete_sym = NODE_PSYMBOL("onMessageComplete"); + + delete_sym = NODE_PSYMBOL("DELETE"); + get_sym = NODE_PSYMBOL("GET"); + head_sym = NODE_PSYMBOL("HEAD"); + post_sym = NODE_PSYMBOL("POST"); + put_sym = NODE_PSYMBOL("PUT"); + connect_sym = NODE_PSYMBOL("CONNECT"); + options_sym = NODE_PSYMBOL("OPTIONS"); + trace_sym = NODE_PSYMBOL("TRACE"); + copy_sym = NODE_PSYMBOL("COPY"); + lock_sym = NODE_PSYMBOL("LOCK"); + mkcol_sym = NODE_PSYMBOL("MKCOL"); + move_sym = NODE_PSYMBOL("MOVE"); + propfind_sym = NODE_PSYMBOL("PROPFIND"); + proppatch_sym = NODE_PSYMBOL("PROPPATCH"); + unlock_sym = NODE_PSYMBOL("UNLOCK"); + unknown_method_sym = NODE_PSYMBOL("UNKNOWN_METHOD"); + + method_sym = NODE_PSYMBOL("method"); + status_code_sym = NODE_PSYMBOL("statusCode"); + http_version_sym = NODE_PSYMBOL("httpVersion"); + version_major_sym = NODE_PSYMBOL("versionMajor"); + version_minor_sym = NODE_PSYMBOL("versionMinor"); + should_keep_alive_sym = NODE_PSYMBOL("shouldKeepAlive"); +} + +} // namespace node + diff --git a/src/node_http_parser.h b/src/node_http_parser.h new file mode 100644 index 00000000000..78ba175eed1 --- /dev/null +++ b/src/node_http_parser.h @@ -0,0 +1,12 @@ +#ifndef NODE_HTTP_PARSER +#define NODE_HTTP_PARSER + +#include + +namespace node { + +void InitHttpParser(v8::Handle target); + +} + +#endif // NODE_HTTP_PARSER diff --git a/src/node_idle_watcher.cc b/src/node_idle_watcher.cc index 031f2e0b149..0ba8d9de891 100644 --- a/src/node_idle_watcher.cc +++ b/src/node_idle_watcher.cc @@ -11,7 +11,7 @@ namespace node { using namespace v8; Persistent IdleWatcher::constructor_template; -Persistent callback_symbol; +static Persistent callback_symbol; void IdleWatcher::Initialize(Handle target) { HandleScope scope; diff --git a/src/node_io_watcher.cc b/src/node_io_watcher.cc new file mode 100644 index 00000000000..9b5c3b3ff6c --- /dev/null +++ b/src/node_io_watcher.cc @@ -0,0 +1,145 @@ +// Copyright 2009 Ryan Dahl +#include + +#include +#include + +#include + +namespace node { + +using namespace v8; + +Persistent IOWatcher::constructor_template; +Persistent callback_symbol; + + +void IOWatcher::Initialize(Handle target) { + HandleScope scope; + + Local t = FunctionTemplate::New(IOWatcher::New); + constructor_template = Persistent::New(t); + constructor_template->InstanceTemplate()->SetInternalFieldCount(1); + constructor_template->SetClassName(String::NewSymbol("IOWatcher")); + + NODE_SET_PROTOTYPE_METHOD(constructor_template, "start", IOWatcher::Start); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "stop", IOWatcher::Stop); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "set", IOWatcher::Set); + + target->Set(String::NewSymbol("IOWatcher"), constructor_template->GetFunction()); + + callback_symbol = NODE_PSYMBOL("callback"); +} + + +void IOWatcher::Callback(EV_P_ ev_io *w, int revents) { + IOWatcher *io = static_cast(w->data); + assert(w == &io->watcher_); + HandleScope scope; + + Local callback_v = io->handle_->Get(callback_symbol); + if (!callback_v->IsFunction()) { + io->Stop(); + return; + } + + Local callback = Local::Cast(callback_v); + + TryCatch try_catch; + + Local argv[2]; + argv[0] = Local::New(revents & EV_READ ? True() : False()); + argv[1] = Local::New(revents & EV_WRITE ? True() : False()); + + io->Ref(); + callback->Call(io->handle_, 2, argv); + io->Unref(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } +} + + +// +// var io = new process.IOWatcher(); +// process.callback = function (readable, writable) { ... }; +// io.set(fd, true, false); +// io.start(); +// +Handle IOWatcher::New(const Arguments& args) { + HandleScope scope; + IOWatcher *s = new IOWatcher(); + s->Wrap(args.This()); + return args.This(); +} + + +Handle IOWatcher::Start(const Arguments& args) { + HandleScope scope; + IOWatcher *io = ObjectWrap::Unwrap(args.Holder()); + io->Start(); + return Undefined(); +} + + +Handle IOWatcher::Stop(const Arguments& args) { + HandleScope scope; + IOWatcher *io = ObjectWrap::Unwrap(args.Holder()); + io->Stop(); + return Undefined(); +} + + +void IOWatcher::Start() { + if (!ev_is_active(&watcher_)) { + ev_io_start(EV_DEFAULT_UC_ &watcher_); + Ref(); + } +} + + +void IOWatcher::Stop() { + if (ev_is_active(&watcher_)) { + ev_io_stop(EV_DEFAULT_UC_ &watcher_); + Unref(); + } +} + + +Handle IOWatcher::Set(const Arguments& args) { + HandleScope scope; + + IOWatcher *io = ObjectWrap::Unwrap(args.Holder()); + + if (!args[0]->IsInt32()) { + return ThrowException(Exception::TypeError( + String::New("First arg should be a file descriptor."))); + } + + int fd = args[0]->Int32Value(); + + if (!args[1]->IsBoolean()) { + return ThrowException(Exception::TypeError( + String::New("Second arg should boolean (readable)."))); + } + + int events = 0; + + if (args[1]->IsTrue()) events |= EV_READ; + + if (!args[2]->IsBoolean()) { + return ThrowException(Exception::TypeError( + String::New("Third arg should boolean (writable)."))); + } + + if (args[2]->IsTrue()) events |= EV_WRITE; + + ev_io_set(&io->watcher_, fd, events); + + return Undefined(); +} + + + +} // namespace node diff --git a/src/node_io_watcher.h b/src/node_io_watcher.h new file mode 100644 index 00000000000..06d431ece94 --- /dev/null +++ b/src/node_io_watcher.h @@ -0,0 +1,44 @@ +// Copyright 2009 Ryan Dahl +#ifndef NODE_IO_H_ +#define NODE_IO_H_ + +#include +#include + +namespace node { + +class IOWatcher : ObjectWrap { + public: + static void Initialize(v8::Handle target); + + protected: + static v8::Persistent constructor_template; + + IOWatcher() : ObjectWrap() { + ev_init(&watcher_, IOWatcher::Callback); + watcher_.data = this; + } + + ~IOWatcher() { + ev_io_stop(EV_DEFAULT_UC_ &watcher_); + assert(!ev_is_active(&watcher_)); + assert(!ev_is_pending(&watcher_)); + } + + static v8::Handle New(const v8::Arguments& args); + static v8::Handle Start(const v8::Arguments& args); + static v8::Handle Stop(const v8::Arguments& args); + static v8::Handle Set(const v8::Arguments& args); + + private: + static void Callback(EV_P_ ev_io *watcher, int revents); + + void Start(); + void Stop(); + + ev_io watcher_; +}; + +} // namespace node +#endif // NODE_IO_H_ + diff --git a/src/node_net2.cc b/src/node_net2.cc new file mode 100644 index 00000000000..560db1db980 --- /dev/null +++ b/src/node_net2.cc @@ -0,0 +1,1309 @@ +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include /* inet_pton */ + +#include +#include + +#include + +#ifdef __linux__ +# include /* For the SIOCINQ / FIONREAD ioctl */ +#endif +/* Non-linux platforms like OS X define this ioctl elsewhere */ +#ifndef FIONREAD +#include +#endif + +#include + + +namespace node { + +using namespace v8; + +static Persistent errno_symbol; +static Persistent syscall_symbol; + +static Persistent fd_symbol; +static Persistent remote_address_symbol; +static Persistent remote_port_symbol; +static Persistent address_symbol; +static Persistent port_symbol; +static Persistent type_symbol; +static Persistent tcp_symbol; +static Persistent unix_symbol; + +static Persistent recv_msg_template; + + +#define FD_ARG(a) \ + if (!(a)->IsInt32()) { \ + return ThrowException(Exception::TypeError( \ + String::New("Bad file descriptor argument"))); \ + } \ + int fd = (a)->Int32Value(); + + +static inline const char *errno_string(int errorno) { +#define ERRNO_CASE(e) case e: return #e; + switch (errorno) { + +#ifdef EACCES + ERRNO_CASE(EACCES); +#endif + +#ifdef EADDRINUSE + ERRNO_CASE(EADDRINUSE); +#endif + +#ifdef EADDRNOTAVAIL + ERRNO_CASE(EADDRNOTAVAIL); +#endif + +#ifdef EAFNOSUPPORT + ERRNO_CASE(EAFNOSUPPORT); +#endif + +#ifdef EAGAIN + ERRNO_CASE(EAGAIN); +#else +# ifdef EWOULDBLOCK + ERRNO_CASE(EWOULDBLOCK); +# endif +#endif + +#ifdef EALREADY + ERRNO_CASE(EALREADY); +#endif + +#ifdef EBADF + ERRNO_CASE(EBADF); +#endif + +#ifdef EBADMSG + ERRNO_CASE(EBADMSG); +#endif + +#ifdef EBUSY + ERRNO_CASE(EBUSY); +#endif + +#ifdef ECANCELED + ERRNO_CASE(ECANCELED); +#endif + +#ifdef ECHILD + ERRNO_CASE(ECHILD); +#endif + +#ifdef ECONNABORTED + ERRNO_CASE(ECONNABORTED); +#endif + +#ifdef ECONNREFUSED + ERRNO_CASE(ECONNREFUSED); +#endif + +#ifdef ECONNRESET + ERRNO_CASE(ECONNRESET); +#endif + +#ifdef EDEADLK + ERRNO_CASE(EDEADLK); +#endif + +#ifdef EDESTADDRREQ + ERRNO_CASE(EDESTADDRREQ); +#endif + +#ifdef EDOM + ERRNO_CASE(EDOM); +#endif + +#ifdef EDQUOT + ERRNO_CASE(EDQUOT); +#endif + +#ifdef EEXIST + ERRNO_CASE(EEXIST); +#endif + +#ifdef EFAULT + ERRNO_CASE(EFAULT); +#endif + +#ifdef EFBIG + ERRNO_CASE(EFBIG); +#endif + +#ifdef EHOSTUNREACH + ERRNO_CASE(EHOSTUNREACH); +#endif + +#ifdef EIDRM + ERRNO_CASE(EIDRM); +#endif + +#ifdef EILSEQ + ERRNO_CASE(EILSEQ); +#endif + +#ifdef EINPROGRESS + ERRNO_CASE(EINPROGRESS); +#endif + +#ifdef EINTR + ERRNO_CASE(EINTR); +#endif + +#ifdef EINVAL + ERRNO_CASE(EINVAL); +#endif + +#ifdef EIO + ERRNO_CASE(EIO); +#endif + +#ifdef EISCONN + ERRNO_CASE(EISCONN); +#endif + +#ifdef EISDIR + ERRNO_CASE(EISDIR); +#endif + +#ifdef ELOOP + ERRNO_CASE(ELOOP); +#endif + +#ifdef EMFILE + ERRNO_CASE(EMFILE); +#endif + +#ifdef EMLINK + ERRNO_CASE(EMLINK); +#endif + +#ifdef EMSGSIZE + ERRNO_CASE(EMSGSIZE); +#endif + +#ifdef EMULTIHOP + ERRNO_CASE(EMULTIHOP); +#endif + +#ifdef ENAMETOOLONG + ERRNO_CASE(ENAMETOOLONG); +#endif + +#ifdef ENETDOWN + ERRNO_CASE(ENETDOWN); +#endif + +#ifdef ENETRESET + ERRNO_CASE(ENETRESET); +#endif + +#ifdef ENETUNREACH + ERRNO_CASE(ENETUNREACH); +#endif + +#ifdef ENFILE + ERRNO_CASE(ENFILE); +#endif + +#ifdef ENOBUFS + ERRNO_CASE(ENOBUFS); +#endif + +#ifdef ENODATA + ERRNO_CASE(ENODATA); +#endif + +#ifdef ENODEV + ERRNO_CASE(ENODEV); +#endif + +#ifdef ENOENT + ERRNO_CASE(ENOENT); +#endif + +#ifdef ENOEXEC + ERRNO_CASE(ENOEXEC); +#endif + +#ifdef ENOLCK + ERRNO_CASE(ENOLCK); +#endif + +#ifdef ENOLINK + ERRNO_CASE(ENOLINK); +#endif + +#ifdef ENOMEM + ERRNO_CASE(ENOMEM); +#endif + +#ifdef ENOMSG + ERRNO_CASE(ENOMSG); +#endif + +#ifdef ENOPROTOOPT + ERRNO_CASE(ENOPROTOOPT); +#endif + +#ifdef ENOSPC + ERRNO_CASE(ENOSPC); +#endif + +#ifdef ENOSR + ERRNO_CASE(ENOSR); +#endif + +#ifdef ENOSTR + ERRNO_CASE(ENOSTR); +#endif + +#ifdef ENOSYS + ERRNO_CASE(ENOSYS); +#endif + +#ifdef ENOTCONN + ERRNO_CASE(ENOTCONN); +#endif + +#ifdef ENOTDIR + ERRNO_CASE(ENOTDIR); +#endif + +#ifdef ENOTEMPTY + ERRNO_CASE(ENOTEMPTY); +#endif + +#ifdef ENOTSOCK + ERRNO_CASE(ENOTSOCK); +#endif + +#ifdef ENOTSUP + ERRNO_CASE(ENOTSUP); +#else +# ifdef EOPNOTSUPP + ERRNO_CASE(EOPNOTSUPP); +# endif +#endif + +#ifdef ENOTTY + ERRNO_CASE(ENOTTY); +#endif + +#ifdef ENXIO + ERRNO_CASE(ENXIO); +#endif + + +#ifdef EOVERFLOW + ERRNO_CASE(EOVERFLOW); +#endif + +#ifdef EPERM + ERRNO_CASE(EPERM); +#endif + +#ifdef EPIPE + ERRNO_CASE(EPIPE); +#endif + +#ifdef EPROTO + ERRNO_CASE(EPROTO); +#endif + +#ifdef EPROTONOSUPPORT + ERRNO_CASE(EPROTONOSUPPORT); +#endif + +#ifdef EPROTOTYPE + ERRNO_CASE(EPROTOTYPE); +#endif + +#ifdef ERANGE + ERRNO_CASE(ERANGE); +#endif + +#ifdef EROFS + ERRNO_CASE(EROFS); +#endif + +#ifdef ESPIPE + ERRNO_CASE(ESPIPE); +#endif + +#ifdef ESRCH + ERRNO_CASE(ESRCH); +#endif + +#ifdef ESTALE + ERRNO_CASE(ESTALE); +#endif + +#ifdef ETIME + ERRNO_CASE(ETIME); +#endif + +#ifdef ETIMEDOUT + ERRNO_CASE(ETIMEDOUT); +#endif + +#ifdef ETXTBSY + ERRNO_CASE(ETXTBSY); +#endif + +#ifdef EXDEV + ERRNO_CASE(EXDEV); +#endif + + default: return ""; + } +} + + +static inline Local ErrnoException(int errorno, + const char *syscall, + const char *msg = "") { + Local estring = String::NewSymbol(errno_string(errorno)); + if (!msg[0]) msg = strerror(errorno); + Local message = String::NewSymbol(msg); + + Local cons1 = String::Concat(estring, String::NewSymbol(", ")); + Local cons2 = String::Concat(cons1, message); + + Local e = Exception::Error(cons2); + + Local obj = e->ToObject(); + obj->Set(errno_symbol, Integer::New(errorno)); + obj->Set(syscall_symbol, String::NewSymbol(syscall)); + return e; +} + + +static inline bool SetCloseOnExec(int fd) { + return (fcntl(fd, F_SETFD, FD_CLOEXEC) != -1); +} + + +static inline bool SetNonBlock(int fd) { + return (fcntl(fd, F_SETFL, O_NONBLOCK) != -1); +} + + +static inline bool SetSockFlags(int fd) { + return SetNonBlock(fd) && SetCloseOnExec(fd); +} + + +// Creates nonblocking pipe +static Handle Pipe(const Arguments& args) { + HandleScope scope; + int fds[2]; + + if (pipe(fds) < 0) return ThrowException(ErrnoException(errno, "pipe")); + + if (!SetSockFlags(fds[0]) || !SetSockFlags(fds[1])) { + int fcntl_errno = errno; + close(fds[0]); + close(fds[1]); + return ThrowException(ErrnoException(fcntl_errno, "fcntl")); + } + + Local a = Array::New(2); + a->Set(Integer::New(0), Integer::New(fds[0])); + a->Set(Integer::New(1), Integer::New(fds[1])); + return scope.Close(a); +} + + +// Creates nonblocking socket pair +static Handle SocketPair(const Arguments& args) { + HandleScope scope; + + int fds[2]; + + // XXX support SOCK_DGRAM? + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) { + return ThrowException(ErrnoException(errno, "socketpair")); + } + + if (!SetSockFlags(fds[0]) || !SetSockFlags(fds[1])) { + int fcntl_errno = errno; + close(fds[0]); + close(fds[1]); + return ThrowException(ErrnoException(fcntl_errno, "fcntl")); + } + + Local a = Array::New(2); + a->Set(Integer::New(0), Integer::New(fds[0])); + a->Set(Integer::New(1), Integer::New(fds[1])); + return scope.Close(a); +} + + +// Creates a new non-blocking socket fd +// t.socket("TCP"); +// t.socket("UNIX"); +// t.socket("UDP"); +static Handle Socket(const Arguments& args) { + HandleScope scope; + + // default to TCP + int domain = PF_INET6; + int type = SOCK_STREAM; + + if (args[0]->IsString()) { + String::Utf8Value t(args[0]->ToString()); + if (0 == strcasecmp(*t, "TCP")) { + domain = PF_INET6; + type = SOCK_STREAM; + } else if (0 == strcasecmp(*t, "UDP")) { + domain = PF_INET6; + type = SOCK_DGRAM; + } else if (0 == strcasecmp(*t, "UNIX")) { + domain = PF_UNIX; + type = SOCK_STREAM; + } else { + return ThrowException(Exception::Error( + String::New("Unknown socket type."))); + } + } + + int fd = socket(domain, type, 0); + + if (fd < 0) return ThrowException(ErrnoException(errno, "socket")); + + if (!SetSockFlags(fd)) { + int fcntl_errno = errno; + close(fd); + return ThrowException(ErrnoException(fcntl_errno, "fcntl")); + } + + return scope.Close(Integer::New(fd)); +} + + +// NOT AT ALL THREAD SAFE - but that's okay for node.js +// (yes this is all to avoid one small heap alloc) +static struct sockaddr *addr; +static socklen_t addrlen; +static struct sockaddr_un un; +static struct sockaddr_in6 in6; +static inline Handle ParseAddressArgs(Handle first, + Handle second, + struct in6_addr default_addr + ) { + if (first->IsString() && second->IsUndefined()) { + // UNIX + String::Utf8Value path(first->ToString()); + + if (path.length() > sizeof un.sun_path) { + return Exception::Error(String::New("Socket path too long")); + } + + memset(&un, 0, sizeof un); + un.sun_family = AF_UNIX; + strcpy(un.sun_path, *path); + + addr = (struct sockaddr*)&un; + addrlen = path.length() + sizeof(un.sun_family) + 1; + + } else { + // TCP or UDP + int port = first->Int32Value(); + memset(&in6, 0, sizeof in6); + if (!second->IsString()) { + in6.sin6_addr = default_addr; + } else { + String::Utf8Value ip(second->ToString()); + + char ipv6[255] = "::FFFF:"; + + if (inet_pton(AF_INET, *ip, &(in6.sin6_addr)) > 0) { + // If this is an IPv4 address then we need to change it + // to the IPv4-mapped-on-IPv6 format which looks like + // ::FFFF: + // For more information see "Address Format" ipv6(7) and + // "BUGS" in inet_pton(3) + strcat(ipv6, *ip); + } else { + strcpy(ipv6, *ip); + } + + if (inet_pton(AF_INET6, ipv6, &(in6.sin6_addr)) <= 0) { + return ErrnoException(errno, "inet_pton", "Invalid IP Address"); + } + } + + in6.sin6_family = AF_INET6; + in6.sin6_port = htons(port); + + addr = (struct sockaddr*)&in6; + addrlen = sizeof in6; + } + return Handle(); +} + + +// Bind with UNIX +// t.bind(fd, "/tmp/socket") +// Bind with TCP +// t.bind(fd, 80, "192.168.11.2") +// t.bind(fd, 80) +static Handle Bind(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 2) { + return ThrowException(Exception::TypeError( + String::New("Must have at least two args"))); + } + + FD_ARG(args[0]) + + Handle error = ParseAddressArgs(args[1], args[2], in6addr_any); + if (!error.IsEmpty()) return ThrowException(error); + + int flags = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *)&flags, sizeof(flags)); + + int r = bind(fd, addr, addrlen); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "bind")); + } + + return Undefined(); +} + + +static Handle Close(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + if (0 > close(fd)) { + return ThrowException(ErrnoException(errno, "close")); + } + + return Undefined(); +} + + +// t.shutdown(fd, "read"); -- SHUT_RD +// t.shutdown(fd, "write"); -- SHUT_WR +// t.shutdown(fd, "readwrite"); -- SHUT_RDWR +// second arg defaults to "write". +static Handle Shutdown(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + int how = SHUT_WR; + + if (args[1]->IsString()) { + String::Utf8Value t(args[1]->ToString()); + if (0 == strcasecmp(*t, "write")) { + how = SHUT_WR; + } else if (0 == strcasecmp(*t, "read")) { + how = SHUT_RD; + } else if (0 == strcasecmp(*t, "readwrite")) { + how = SHUT_RDWR; + } else { + return ThrowException(Exception::Error(String::New( + "Unknown shutdown method. (Use 'read', 'write', or 'readwrite'.)"))); + } + } + + if (0 > shutdown(fd, how)) { + return ThrowException(ErrnoException(errno, "shutdown")); + } + + return Undefined(); +} + + +// Connect with unix +// t.connect(fd, "/tmp/socket") +// +// Connect with TCP or UDP +// t.connect(fd, 80, "192.168.11.2") +// t.connect(fd, 80, "::1") +// t.connect(fd, 80) +// the third argument defaults to "::1" +static Handle Connect(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 2) { + return ThrowException(Exception::TypeError( + String::New("Must have at least two args"))); + } + + FD_ARG(args[0]) + + Handle error = ParseAddressArgs(args[1], args[2], in6addr_loopback); + if (!error.IsEmpty()) return ThrowException(error); + + int r = connect(fd, addr, addrlen); + + if (r < 0 && errno != EINPROGRESS) { + return ThrowException(ErrnoException(errno, "connect")); + } + + return Undefined(); +} + + +static Handle GetSockName(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + struct sockaddr_storage address_storage; + socklen_t len = sizeof(struct sockaddr_storage); + + int r = getsockname(fd, (struct sockaddr *) &address_storage, &len); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "getsockname")); + } + + Local info = Object::New(); + + if (address_storage.ss_family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6*)&address_storage; + + char ip[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(a->sin6_addr), ip, INET6_ADDRSTRLEN); + + int port = ntohs(a->sin6_port); + + info->Set(address_symbol, String::New(ip)); + info->Set(port_symbol, Integer::New(port)); + } + + return scope.Close(info); +} + + +static Handle GetPeerName(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + struct sockaddr_storage address_storage; + socklen_t len = sizeof(struct sockaddr_storage); + + int r = getpeername(fd, (struct sockaddr *) &address_storage, &len); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "getsockname")); + } + + Local info = Object::New(); + + if (address_storage.ss_family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6*)&address_storage; + + char ip[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(a->sin6_addr), ip, INET6_ADDRSTRLEN); + + int port = ntohs(a->sin6_port); + + info->Set(remote_address_symbol, String::New(ip)); + info->Set(remote_port_symbol, Integer::New(port)); + } + + return scope.Close(info); +} + + +static Handle Listen(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + int backlog = args[1]->IsInt32() ? args[1]->Int32Value() : 128; + + if (0 > listen(fd, backlog)) { + return ThrowException(ErrnoException(errno, "listen")); + } + + + return Undefined(); +} + + +// var peerInfo = t.accept(server_fd); +// +// peerInfo.fd +// peerInfo.remoteAddress +// peerInfo.remotePort +// +// Returns a new nonblocking socket fd. If the listen queue is empty the +// function returns null (wait for server_fd to become readable and try +// again) +static Handle Accept(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + struct sockaddr_storage address_storage; + socklen_t len = sizeof(struct sockaddr_storage); + + int peer_fd = accept(fd, (struct sockaddr*) &address_storage, &len); + + if (peer_fd < 0) { + if (errno == EAGAIN) return scope.Close(Null()); + return ThrowException(ErrnoException(errno, "accept")); + } + + if (!SetSockFlags(peer_fd)) { + int fcntl_errno = errno; + close(peer_fd); + return ThrowException(ErrnoException(fcntl_errno, "fcntl")); + } + + Local peer_info = Object::New(); + + peer_info->Set(fd_symbol, Integer::New(peer_fd)); + + if (address_storage.ss_family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6*)&address_storage; + + char ip[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(a->sin6_addr), ip, INET6_ADDRSTRLEN); + + int port = ntohs(a->sin6_port); + + peer_info->Set(remote_address_symbol, String::New(ip)); + peer_info->Set(remote_port_symbol, Integer::New(port)); + } + + return scope.Close(peer_info); +} + + +static Handle SocketError(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + int error; + socklen_t len = sizeof(int); + int r = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "getsockopt")); + } + + return scope.Close(Integer::New(error)); +} + + +// var bytesRead = t.read(fd, buffer, offset, length); +// returns null on EAGAIN or EINTR, raises an exception on all other errors +// returns 0 on EOF. +static Handle Read(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 4) { + return ThrowException(Exception::TypeError( + String::New("Takes 4 parameters"))); + } + + FD_ARG(args[0]) + + if (!Buffer::HasInstance(args[1])) { + return ThrowException(Exception::TypeError( + String::New("Second argument should be a buffer"))); + } + + Buffer * buffer = ObjectWrap::Unwrap(args[1]->ToObject()); + + size_t off = args[2]->Int32Value(); + if (off >= buffer->length()) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + size_t len = args[3]->Int32Value(); + if (off + len > buffer->length()) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + ssize_t bytes_read = read(fd, (char*)buffer->data() + off, len); + + if (bytes_read < 0) { + if (errno == EAGAIN || errno == EINTR) return Null(); + return ThrowException(ErrnoException(errno, "read")); + } + + return scope.Close(Integer::New(bytes_read)); +} + + +// bytesRead = t.recvMsg(fd, buffer, offset, length) +// if (recvMsg.fd) { +// receivedFd = recvMsg.fd; +// } +static Handle RecvMsg(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 4) { + return ThrowException(Exception::TypeError( + String::New("Takes 4 parameters"))); + } + + FD_ARG(args[0]) + + if (!Buffer::HasInstance(args[1])) { + return ThrowException(Exception::TypeError( + String::New("Second argument should be a buffer"))); + } + + Buffer * buffer = ObjectWrap::Unwrap(args[1]->ToObject()); + + size_t off = args[2]->Int32Value(); + if (off >= buffer->length()) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + size_t len = args[3]->Int32Value(); + if (off + len > buffer->length()) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + int received_fd; + + struct iovec iov[1]; + iov[0].iov_base = (char*)buffer->data() + off; + iov[0].iov_len = len; + + struct msghdr msg; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_name = NULL; + msg.msg_namelen = 0; + /* Set up to receive a descriptor even if one isn't in the message */ + char cmsg_space[64]; // should be big enough + msg.msg_controllen = 64; + msg.msg_control = (void *) cmsg_space; + + ssize_t bytes_read = recvmsg(fd, &msg, 0); + + if (bytes_read < 0) { + if (errno == EAGAIN || errno == EINTR) return Null(); + return ThrowException(ErrnoException(errno, "recvMsg")); + } + + // Why not return a two element array here [bytesRead, fd]? Because + // creating an object for each recvmsg() action is heavy. Instead we just + // assign the recved fd to a globalally accessable variable (recvMsg.fd) + // that the wrapper can pick up. Since we're single threaded, this is not + // a problem - just make sure to copy out that variable before the next + // call to recvmsg(). + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg && cmsg->cmsg_type == SCM_RIGHTS) { + received_fd = *(int *) CMSG_DATA(cmsg); + recv_msg_template->GetFunction()->Set(fd_symbol, Integer::New(received_fd)); + } else { + recv_msg_template->GetFunction()->Set(fd_symbol, Null()); + } + + return scope.Close(Integer::New(bytes_read)); +} + + +// var bytesWritten = t.write(fd, buffer, offset, length); +// returns null on EAGAIN or EINTR, raises an exception on all other errors +static Handle Write(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 4) { + return ThrowException(Exception::TypeError( + String::New("Takes 4 parameters"))); + } + + FD_ARG(args[0]) + + if (!Buffer::HasInstance(args[1])) { + return ThrowException(Exception::TypeError( + String::New("Second argument should be a buffer"))); + } + + Buffer * buffer = ObjectWrap::Unwrap(args[1]->ToObject()); + + size_t off = args[2]->Int32Value(); + if (off >= buffer->length()) { + return ThrowException(Exception::Error( + String::New("Offset is out of bounds"))); + } + + size_t len = args[3]->Int32Value(); + if (off + len > buffer->length()) { + return ThrowException(Exception::Error( + String::New("Length is extends beyond buffer"))); + } + + ssize_t written = write(fd, (char*)buffer->data() + off, len); + + if (written < 0) { + if (errno == EAGAIN || errno == EINTR) return Null(); + return ThrowException(ErrnoException(errno, "write")); + } + + return scope.Close(Integer::New(written)); +} + + +// var bytesWritten = t.sendFD(self.fd) +// returns null on EAGAIN or EINTR, raises an exception on all other errors +static Handle SendFD(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 2) { + return ThrowException(Exception::TypeError( + String::New("Takes 2 parameters"))); + } + + FD_ARG(args[0]) + + // TODO: make sure fd is a unix domain socket? + + if (!args[1]->IsInt32()) { + return ThrowException(Exception::TypeError( + String::New("FD to send is not an integer"))); + } + + int fd_to_send = args[1]->Int32Value(); + + struct msghdr msg; + struct iovec iov[1]; + char control_msg[CMSG_SPACE(sizeof(fd_to_send))]; + struct cmsghdr *cmsg; + static char dummy = 'd'; // Need to send at least a byte of data in the message + + iov[0].iov_base = &dummy; + iov[0].iov_len = 1; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_flags = 0; + msg.msg_control = (void *) control_msg; + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(fd_to_send)); + *(int*) CMSG_DATA(cmsg) = fd_to_send; + msg.msg_controllen = cmsg->cmsg_len; + + ssize_t written = sendmsg(fd, &msg, 0); + + if (written < 0) { + if (errno == EAGAIN || errno == EINTR) return Null(); + return ThrowException(ErrnoException(errno, "sendmsg")); + } + + /* Note that the FD isn't explicitly closed here, this + * happens in the JS */ + + return scope.Close(Integer::New(written)); +} + + +// Probably only works for Linux TCP sockets? +// Returns the amount of data on the read queue. +static Handle ToRead(const Arguments& args) { + HandleScope scope; + + FD_ARG(args[0]) + + int value; + int r = ioctl(fd, FIONREAD, &value); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "ioctl")); + } + + return scope.Close(Integer::New(value)); +} + + +static Handle SetNoDelay(const Arguments& args) { + int flags, r; + HandleScope scope; + + FD_ARG(args[0]) + + flags = args[1]->IsFalse() ? 0 : 1; + r = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); + + if (r < 0) { + return ThrowException(ErrnoException(errno, "setsockopt")); + } + + return Undefined(); +} + + +// +// G E T A D D R I N F O +// + + +struct resolve_request { + Persistent cb; + struct addrinfo *address_list; + int ai_family; // AF_INET or AF_INET6 + char hostname[1]; +}; + + +static int AfterResolve(eio_req *req) { + ev_unref(EV_DEFAULT_UC); + + struct resolve_request * rreq = (struct resolve_request *)(req->data); + + HandleScope scope; + Local argv[1]; + + if (req->result != 0) { + if (req->result == EAI_NODATA) { + argv[0] = Array::New(); + } else { + argv[0] = ErrnoException(req->result, + "getaddrinfo", + gai_strerror(req->result)); + } + } else { + struct addrinfo *address; + int n = 0; + + for (address = rreq->address_list; address; address = address->ai_next) { n++; } + + Local results = Array::New(n); + + char ip[INET6_ADDRSTRLEN]; + const char *addr; + + n = 0; + address = rreq->address_list; + while (address) { + assert(address->ai_socktype == SOCK_STREAM); + assert(address->ai_family == AF_INET || address->ai_family == AF_INET6); + addr = ( address->ai_family == AF_INET + ? (char *) &((struct sockaddr_in *) address->ai_addr)->sin_addr + : (char *) &((struct sockaddr_in6 *) address->ai_addr)->sin6_addr + ); + const char *c = inet_ntop(address->ai_family, addr, ip, INET6_ADDRSTRLEN); + Local s = String::New(c); + results->Set(Integer::New(n), s); + + n++; + address = address->ai_next; + } + + argv[0] = results; + } + + TryCatch try_catch; + + rreq->cb->Call(Context::GetCurrent()->Global(), 1, argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + if (rreq->address_list) freeaddrinfo(rreq->address_list); + rreq->cb.Dispose(); // Dispose of the persistent handle + free(rreq); + + return 0; +} + + +static int Resolve(eio_req *req) { + // Note: this function is executed in the thread pool! CAREFUL + struct resolve_request * rreq = (struct resolve_request *) req->data; + + struct addrinfo hints; + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = rreq->ai_family; + hints.ai_socktype = SOCK_STREAM; + + req->result = getaddrinfo((char*)rreq->hostname, + NULL, + &hints, + &(rreq->address_list)); + return 0; +} + + +static Handle GetAddrInfo(const Arguments& args) { + HandleScope scope; + + String::Utf8Value hostname(args[0]->ToString()); + + int type = args[1]->Int32Value(); + int fam = AF_INET; + switch (type) { + case 4: + fam = AF_INET; + break; + case 6: + fam = AF_INET6; + break; + default: + return ThrowException(Exception::TypeError( + String::New("Second argument must be an integer 4 or 6"))); + } + + if (!args[2]->IsFunction()) { + return ThrowException(Exception::TypeError( + String::New("Thrid argument must be a callback"))); + } + + Local cb = Local::Cast(args[2]); + + struct resolve_request *rreq = (struct resolve_request *) + calloc(1, sizeof(struct resolve_request) + hostname.length()); + + if (!rreq) { + V8::LowMemoryNotification(); + return ThrowException(Exception::Error( + String::New("Could not allocate enough memory"))); + } + + strcpy(rreq->hostname, *hostname); + rreq->cb = Persistent::New(cb); + rreq->ai_family = fam; + + // For the moment I will do DNS lookups in the eio thread pool. This is + // sub-optimal and cannot handle massive numbers of requests. + // + // (One particularly annoying problem is that the pthread stack size needs + // to be increased dramatically to handle getaddrinfo() see X_STACKSIZE in + // wscript ). + // + // In the future I will move to a system using c-ares: + // http://lists.schmorp.de/pipermail/libev/2009q1/000632.html + eio_custom(Resolve, EIO_PRI_DEFAULT, AfterResolve, rreq); + + // There will not be any active watchers from this object on the event + // loop while getaddrinfo() runs. If the only thing happening in the + // script was this hostname resolution, then the event loop would drop + // out. Thus we need to add ev_ref() until AfterResolve(). + ev_ref(EV_DEFAULT_UC); + + return Undefined(); +} + + +static Handle NeedsLookup(const Arguments& args) { + HandleScope scope; + + if (args[0]->IsNull() || args[0]->IsUndefined()) return False(); + + String::Utf8Value s(args[0]->ToString()); + + // avoiding buffer overflows in the following strcat + // 2001:0db8:85a3:08d3:1319:8a2e:0370:7334 + // 39 = max ipv6 address. + if (s.length() > INET6_ADDRSTRLEN) return True(); + + struct sockaddr_in6 a; + + if (inet_pton(AF_INET, *s, &(a.sin6_addr)) > 0) return False(); + if (inet_pton(AF_INET6, *s, &(a.sin6_addr)) > 0) return False(); + + char ipv6[255] = "::FFFF:"; + strcat(ipv6, *s); + if (inet_pton(AF_INET6, ipv6, &(a.sin6_addr)) > 0) return False(); + + return True(); +} + + +static Handle CreateErrnoException(const Arguments& args) { + HandleScope scope; + + int errorno = args[0]->Int32Value(); + String::Utf8Value syscall(args[1]->ToString()); + + Local exception = ErrnoException(errorno, *syscall); + + return scope.Close(exception); +} + + +void InitNet2(Handle target) { + HandleScope scope; + + NODE_SET_METHOD(target, "write", Write); + NODE_SET_METHOD(target, "read", Read); + + NODE_SET_METHOD(target, "sendFD", SendFD); + + recv_msg_template = + Persistent::New(FunctionTemplate::New(RecvMsg)); + target->Set(String::NewSymbol("recvMsg"), recv_msg_template->GetFunction()); + + NODE_SET_METHOD(target, "socket", Socket); + NODE_SET_METHOD(target, "close", Close); + NODE_SET_METHOD(target, "shutdown", Shutdown); + NODE_SET_METHOD(target, "pipe", Pipe); + NODE_SET_METHOD(target, "socketpair", SocketPair); + + NODE_SET_METHOD(target, "connect", Connect); + NODE_SET_METHOD(target, "bind", Bind); + NODE_SET_METHOD(target, "listen", Listen); + NODE_SET_METHOD(target, "accept", Accept); + NODE_SET_METHOD(target, "socketError", SocketError); + NODE_SET_METHOD(target, "toRead", ToRead); + NODE_SET_METHOD(target, "setNoDelay", SetNoDelay); + NODE_SET_METHOD(target, "getsockname", GetSockName); + NODE_SET_METHOD(target, "getpeername", GetPeerName); + NODE_SET_METHOD(target, "getaddrinfo", GetAddrInfo); + NODE_SET_METHOD(target, "needsLookup", NeedsLookup); + NODE_SET_METHOD(target, "errnoException", CreateErrnoException); + + target->Set(String::NewSymbol("ENOENT"), Integer::New(ENOENT)); + target->Set(String::NewSymbol("EINPROGRESS"), Integer::New(EINPROGRESS)); + target->Set(String::NewSymbol("EINTR"), Integer::New(EINTR)); + target->Set(String::NewSymbol("EACCES"), Integer::New(EACCES)); + target->Set(String::NewSymbol("EPERM"), Integer::New(EPERM)); + target->Set(String::NewSymbol("EADDRINUSE"), Integer::New(EADDRINUSE)); + target->Set(String::NewSymbol("ECONNREFUSED"), Integer::New(ECONNREFUSED)); + + errno_symbol = NODE_PSYMBOL("errno"); + syscall_symbol = NODE_PSYMBOL("syscall"); + fd_symbol = NODE_PSYMBOL("fd"); + remote_address_symbol = NODE_PSYMBOL("remoteAddress"); + remote_port_symbol = NODE_PSYMBOL("remotePort"); + address_symbol = NODE_PSYMBOL("address"); + port_symbol = NODE_PSYMBOL("port"); +} + +} // namespace node diff --git a/src/node_net2.h b/src/node_net2.h new file mode 100644 index 00000000000..cd3e9f083ad --- /dev/null +++ b/src/node_net2.h @@ -0,0 +1,12 @@ +#ifndef NODE_NET2 +#define NODE_NET2 + +#include + +namespace node { + +void InitNet2(v8::Handle target); + +} + +#endif // NODE_NET2 diff --git a/src/node_stdio.cc b/src/node_stdio.cc index 2b5b30612c1..ee1278f4bb9 100644 --- a/src/node_stdio.cc +++ b/src/node_stdio.cc @@ -8,10 +8,8 @@ #include using namespace v8; -using namespace node; +namespace node { -static Persistent stdio; -static Persistent emit; static struct coupling *stdin_coupling = NULL; static struct coupling *stdout_coupling = NULL; @@ -19,33 +17,8 @@ static struct coupling *stdout_coupling = NULL; static int stdin_fd = -1; static int stdout_fd = -1; -static evcom_reader in; -static evcom_writer out; -static enum encoding stdin_encoding; - -static void -EmitInput (Local input) -{ - HandleScope scope; - - Local argv[2] = { String::NewSymbol("data"), input }; - - emit->Call(stdio, 2, argv); -} - -static void -EmitClose (void) -{ - HandleScope scope; - - Local argv[1] = { String::NewSymbol("close") }; - - emit->Call(stdio, 1, argv); -} - - -static inline Local errno_exception(int errorno) { +static Local errno_exception(int errorno) { Local e = Exception::Error(String::NewSymbol(strerror(errorno))); Local obj = e->ToObject(); obj->Set(String::NewSymbol("errno"), Integer::New(errorno)); @@ -53,7 +26,7 @@ static inline Local errno_exception(int errorno) { } -/* STDERR IS ALWAY SYNC */ +/* STDERR IS ALWAY SYNC ALWAYS UTF8 */ static Handle WriteError (const Arguments& args) { @@ -81,84 +54,8 @@ WriteError (const Arguments& args) return Undefined(); } -static Handle -Write (const Arguments& args) -{ - HandleScope scope; - if (args.Length() == 0) { - return ThrowException(Exception::Error(String::New("Bad argument"))); - } - - enum encoding enc = UTF8; - if (args.Length() > 1) enc = ParseEncoding(args[1], UTF8); - - ssize_t len = DecodeBytes(args[0], enc); - - if (len < 0) { - Local exception = Exception::TypeError(String::New("Bad argument")); - return ThrowException(exception); - } - - char buf[len]; - ssize_t written = DecodeWrite(buf, len, args[0], enc); - - assert(written == len); - - evcom_writer_write(&out, buf, len); - - return Undefined(); -} - -static void -detach_in (evcom_reader *r) -{ - assert(r == &in); - HandleScope scope; - - EmitClose(); - - evcom_reader_detach(&in); - - if (stdin_coupling) { - coupling_destroy(stdin_coupling); - stdin_coupling = NULL; - } - - stdin_fd = -1; -} - -static void -detach_out (evcom_writer* w) -{ - assert(w == &out); - - evcom_writer_detach(&out); - if (stdout_coupling) { - coupling_destroy(stdout_coupling); - stdout_coupling = NULL; - } - stdout_fd = -1; -} - -static void -on_read (evcom_reader *r, const void *buf, size_t len) -{ - assert(r == &in); - HandleScope scope; - - if (!len) { - return; - } - - Local data = Encode(buf, len, stdin_encoding); - - EmitInput(data); -} - -static inline int -set_nonblock (int fd) -{ +static inline int SetNonblock(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) return -1; @@ -168,20 +65,14 @@ set_nonblock (int fd) return 0; } -static Handle -Open (const Arguments& args) -{ + +static Handle OpenStdin(const Arguments& args) { HandleScope scope; if (stdin_fd >= 0) { return ThrowException(Exception::Error(String::New("stdin already open"))); } - stdin_encoding = UTF8; - if (args.Length() > 0) { - stdin_encoding = ParseEncoding(args[0]); - } - if (isatty(STDIN_FILENO)) { // XXX selecting on tty fds wont work in windows. // Must ALWAYS make a coupling on shitty platforms. @@ -190,34 +81,11 @@ Open (const Arguments& args) stdin_coupling = coupling_new_pull(STDIN_FILENO); stdin_fd = coupling_nonblocking_fd(stdin_coupling); } - set_nonblock(stdin_fd); + SetNonblock(stdin_fd); - evcom_reader_init(&in); - - in.on_read = on_read; - in.on_close = detach_in; - - evcom_reader_set(&in, stdin_fd); - evcom_reader_attach(EV_DEFAULT_ &in); - - return Undefined(); + return scope.Close(Integer::New(stdin_fd)); } -static Handle -Close (const Arguments& args) -{ - HandleScope scope; - - assert(stdio == args.Holder()); - - if (stdin_fd < 0) { - return ThrowException(Exception::Error(String::New("stdin not open"))); - } - - evcom_reader_close(&in); - - return Undefined(); -} void Stdio::Flush() { if (stdout_fd >= 0) { @@ -232,28 +100,10 @@ void Stdio::Flush() { } } -void -Stdio::Initialize (v8::Handle target) -{ + +void Stdio::Initialize(v8::Handle target) { HandleScope scope; - Local stdio_local = - EventEmitter::constructor_template->GetFunction()->NewInstance(0, NULL); - - stdio = Persistent::New(stdio_local); - - NODE_SET_METHOD(stdio, "open", Open); - NODE_SET_METHOD(stdio, "write", Write); - NODE_SET_METHOD(stdio, "writeError", WriteError); - NODE_SET_METHOD(stdio, "close", Close); - - target->Set(String::NewSymbol("stdio"), stdio); - - Local emit_v = stdio->Get(String::NewSymbol("emit")); - assert(emit_v->IsFunction()); - Local emit_f = Local::Cast(emit_v); - emit = Persistent::New(emit_f); - if (isatty(STDOUT_FILENO)) { // XXX selecting on tty fds wont work in windows. // Must ALWAYS make a coupling on shitty platforms. @@ -262,10 +112,13 @@ Stdio::Initialize (v8::Handle target) stdout_coupling = coupling_new_push(STDOUT_FILENO); stdout_fd = coupling_nonblocking_fd(stdout_coupling); } - set_nonblock(stdout_fd); + SetNonblock(stdout_fd); - evcom_writer_init(&out); - out.on_close = detach_out; - evcom_writer_set(&out, stdout_fd); - evcom_writer_attach(EV_DEFAULT_ &out); + target->Set(String::NewSymbol("stdoutFD"), Integer::New(stdout_fd)); + + NODE_SET_METHOD(target, "writeError", WriteError); + NODE_SET_METHOD(target, "openStdin", OpenStdin); } + + +} // namespace node diff --git a/src/node_stdio.h b/src/node_stdio.h index 60fa2912fd2..47639438735 100644 --- a/src/node_stdio.h +++ b/src/node_stdio.h @@ -2,9 +2,7 @@ #define node_stdio_h #include - #include -#include namespace node { @@ -14,5 +12,5 @@ public: static void Flush (); }; -} // namespace node -#endif +} // namespace node +#endif // node_stdio_h diff --git a/src/node_timer.cc b/src/node_timer.cc index 1fa470aad33..5ab409b048a 100644 --- a/src/node_timer.cc +++ b/src/node_timer.cc @@ -27,6 +27,7 @@ Timer::Initialize (Handle target) NODE_SET_PROTOTYPE_METHOD(constructor_template, "start", Timer::Start); NODE_SET_PROTOTYPE_METHOD(constructor_template, "stop", Timer::Stop); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "again", Timer::Again); constructor_template->InstanceTemplate()->SetAccessor(repeat_symbol, RepeatGetter, RepeatSetter); @@ -140,3 +141,30 @@ void Timer::Stop () { Unref(); } } + + +Handle Timer::Again(const Arguments& args) { + HandleScope scope; + Timer *timer = ObjectWrap::Unwrap(args.Holder()); + + int was_active = ev_is_active(&timer->watcher_); + + if (args.Length() > 0) { + ev_tstamp repeat = NODE_V8_UNIXTIME(args[0]); + if (repeat > 0) timer->watcher_.repeat = repeat; + } + + ev_timer_again(EV_DEFAULT_UC_ &timer->watcher_); + + // ev_timer_again can start or stop the watcher. + // So we need to check what happened and adjust the ref count + // appropriately. + + if (ev_is_active(&timer->watcher_)) { + if (!was_active) timer->Ref(); + } else { + if (was_active) timer->Unref(); + } + + return Undefined(); +} diff --git a/src/node_timer.h b/src/node_timer.h index 0472fdb0d60..56352912a66 100644 --- a/src/node_timer.h +++ b/src/node_timer.h @@ -15,12 +15,16 @@ class Timer : ObjectWrap { protected: static v8::Persistent constructor_template; - Timer () : ObjectWrap () { } + Timer () : ObjectWrap () { + // dummy timeout values + ev_timer_init(&watcher_, OnTimeout, 0., 1.); + } ~Timer(); static v8::Handle New (const v8::Arguments& args); static v8::Handle Start (const v8::Arguments& args); static v8::Handle Stop (const v8::Arguments& args); + static v8::Handle Again (const v8::Arguments& args); static v8::Handle RepeatGetter (v8::Local property, const v8::AccessorInfo& info); static void RepeatSetter (v8::Local property, v8::Local value, const v8::AccessorInfo& info); diff --git a/test-net-server.js b/test-net-server.js new file mode 100644 index 00000000000..9c1ddb0bf00 --- /dev/null +++ b/test-net-server.js @@ -0,0 +1,58 @@ +process.Buffer.prototype.toString = function () { + return this.utf8Slice(0, this.length); +}; + +var sys = require("sys"); +var net = require("./lib/net"); + +var server = new net.Server(function (socket) { + sys.puts("connection (" + socket.fd + "): " + + socket.remoteAddress + + " port " + + socket.remotePort + ); + sys.puts("server fd: " + server.fd); + + socket.addListener("data", function (b) { + socket.send("pong ascii\r\n", "ascii"); + socket.send(b); + socket.send("pong utf8\r\n", "utf8"); + if (/^quit/.test(b)) { + socket.close(); + server.close(); + } + }); + + socket.addListener("eof", function () { + sys.puts("server peer eof"); + socket.close(); + }); + + socket.addListener('drain', function () { + sys.puts("server-side socket drain"); + }); +}); + +server.addListener("listening", function () { + var c = net.createConnection("/tmp/node.sock"); + c.addListener('connect', function () { + sys.puts("!!!client connected"); + c.send("hello\n"); + }); + + c.addListener('drain', function () { + sys.puts("!!!client drain"); + }); + + c.addListener('data', function (d) { + sys.puts("!!!client got: " + JSON.stringify(d.toString())); + c.close(); + }); + + c.addListener('eof', function (d) { + sys.puts("!!!client eof"); + }); +}); + +server.listen("/tmp/node.sock"); +sys.puts("server fd: " + server.fd); diff --git a/test/disabled/test-net-fd-passing.js b/test/disabled/test-net-fd-passing.js new file mode 100644 index 00000000000..3d16ff62d37 --- /dev/null +++ b/test/disabled/test-net-fd-passing.js @@ -0,0 +1,67 @@ +process.mixin(require("../common")); +net = require("net"); + +var tests_run = 0; + +function fdPassingTest(path, port) { + var greeting = "howdy"; + var message = "beep toot"; + var expectedData = ["[greeting] " + greeting, "[echo] " + message]; + + var receiverArgs = [fixturesDir + "/net-fd-passing-receiver.js", path, greeting]; + var receiver = process.createChildProcess(process.ARGV[0], receiverArgs); + + var initializeSender = function() { + var fdHighway = new net.Socket(); + + fdHighway.addListener("connect", function() { + var sender = net.createServer(function(socket) { + fdHighway.sendFD(socket); + socket.flush(); + socket.forceClose(); // want to close() the fd, not shutdown() + }); + + sender.addListener("listening", function() { + var client = net.createConnection(port); + + client.addListener("connect", function() { + client.write(message); + }); + + client.addListener("data", function(data) { + assert.equal(expectedData[0], data); + if (expectedData.length > 1) { + expectedData.shift(); + } + else { + // time to shut down + fdHighway.close(); + sender.close(); + client.forceClose(); + } + }); + }); + + tests_run += 1; + sender.listen(port); + }); + + fdHighway.connect(path); + + + }; + + receiver.addListener("output", function(data) { + var initialized = false; + if ((! initialized) && (data == "ready")) { + initializeSender(); + initialized = true; + } + }); +} + +fdPassingTest("/tmp/passing-socket-test", 31075); + +process.addListener("exit", function () { + assert.equal(1, tests_run); +}); diff --git a/test/fixtures/echo.js b/test/fixtures/echo.js index 8cb96e2026f..60f3b614a03 100644 --- a/test/fixtures/echo.js +++ b/test/fixtures/echo.js @@ -1,12 +1,13 @@ require("../common"); -process.stdio.open(); print("hello world\r\n"); -process.stdio.addListener("data", function (data) { - print(data); +var stdin = process.openStdin(); + +stdin.addListener("data", function (data) { + process.stdout.write(data); }); -process.stdio.addListener("close", function () { - process.stdio.close(); +stdin.addListener("end", function () { + process.stdout.close(); }); diff --git a/test/fixtures/net-fd-passing-receiver.js b/test/fixtures/net-fd-passing-receiver.js new file mode 100644 index 00000000000..bcaf9e116ca --- /dev/null +++ b/test/fixtures/net-fd-passing-receiver.js @@ -0,0 +1,32 @@ +process.mixin(require("../common")); +net = require("net"); + +path = process.ARGV[2]; +greeting = process.ARGV[3]; + +receiver = net.createServer(function(socket) { + socket.addListener("fd", function(fd) { + var peerInfo = process.getpeername(fd); + peerInfo.fd = fd; + var passedSocket = new net.Socket(peerInfo); + + passedSocket.addListener("eof", function() { + passedSocket.close(); + }); + + passedSocket.addListener("data", function(data) { + passedSocket.send("[echo] " + data); + }); + passedSocket.addListener("close", function() { + receiver.close(); + }); + passedSocket.send("[greeting] " + greeting); + }); +}); + +/* To signal the test runne we're up and listening */ +receiver.addListener("listening", function() { + print("ready"); +}); + +receiver.listen(path); diff --git a/test/fixtures/print-chars.js b/test/fixtures/print-chars.js index 2f8b6cf769a..13a4c3d0b3f 100644 --- a/test/fixtures/print-chars.js +++ b/test/fixtures/print-chars.js @@ -3,8 +3,8 @@ require("../common"); var n = parseInt(process.argv[2]); var s = ""; -for (var i = 0; i < n-1; i++) { +for (var i = 0; i < n; i++) { s += 'c'; } -puts(s); // \n is the nth char. +process.stdout.write(s); diff --git a/test/pummel/test-process-spawn-loop.js b/test/pummel/test-process-spawn-loop.js deleted file mode 100644 index 6b8d9d8f4bf..00000000000 --- a/test/pummel/test-process-spawn-loop.js +++ /dev/null @@ -1,33 +0,0 @@ -require("../common"); - -var N = 40; -var finished = false; - -function spawn (i) { - var child = process.createChildProcess( 'python' - , ['-c', 'print 500 * 1024 * "C"'] - ); - var output = ""; - - child.addListener("output", function(chunk) { - if (chunk) output += chunk; - }); - - child.addListener("error", function(chunk) { - if (chunk) error(chunk) - }); - - child.addListener("exit", function () { - puts(output); - if (i < N) - spawn(i+1); - else - finished = true; - }); -} - -spawn(0); - -process.addListener("exit", function () { - assert.equal(true, finished); -}); diff --git a/test/pummel/test-tcp-many-clients.js b/test/pummel/test-tcp-many-clients.js index f6de67bc2d8..1c27169af4f 100644 --- a/test/pummel/test-tcp-many-clients.js +++ b/test/pummel/test-tcp-many-clients.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); // settings var bytes = 1024*40; var concurrency = 100; @@ -13,7 +13,7 @@ for (var i = 0; i < bytes; i++) { body += "C"; } -var server = tcp.createServer(function (c) { +var server = net.createServer(function (c) { c.addListener("connect", function () { total_connections++; print("#"); @@ -24,8 +24,10 @@ var server = tcp.createServer(function (c) { server.listen(PORT); function runClient (callback) { - var client = tcp.createConnection(PORT); + var client = net.createConnection(PORT); + client.connections = 0; + client.setEncoding("utf8"); client.addListener("connect", function () { @@ -38,14 +40,25 @@ function runClient (callback) { this.recved += chunk; }); - client.addListener("end", function (had_error) { + client.addListener("end", function () { client.close(); }); + client.addListener("error", function (e) { + puts("\n\nERROOOOOr"); + throw e; + }); + client.addListener("close", function (had_error) { print("."); assert.equal(false, had_error); assert.equal(bytes, client.recved.length); + + if (client.fd) { + puts(client.fd); + } + assert.ok(!client.fd); + if (this.connections < connections_per_client) { this.connect(PORT); } else { @@ -54,13 +67,14 @@ function runClient (callback) { }); } - -var finished_clients = 0; -for (var i = 0; i < concurrency; i++) { - runClient(function () { - if (++finished_clients == concurrency) server.close(); - }); -} +server.addListener('listening', function () { + var finished_clients = 0; + for (var i = 0; i < concurrency; i++) { + runClient(function () { + if (++finished_clients == concurrency) server.close(); + }); + } +}); process.addListener("exit", function () { assert.equal(connections_per_client * concurrency, total_connections); diff --git a/test/pummel/test-tcp-pause.js b/test/pummel/test-tcp-pause.js index 71b83df9e22..adb61544013 100644 --- a/test/pummel/test-tcp-pause.js +++ b/test/pummel/test-tcp-pause.js @@ -1,8 +1,8 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); N = 200; -server = tcp.createServer(function (connection) { +server = net.createServer(function (connection) { function write (j) { if (j >= N) { connection.close(); @@ -21,7 +21,7 @@ server.listen(PORT); recv = ""; chars_recved = 0; -client = tcp.createConnection(PORT); +client = net.createConnection(PORT); client.setEncoding("ascii"); client.addListener("data", function (d) { print(d); diff --git a/test/pummel/test-tcp-pingpong-delay.js b/test/pummel/test-tcp-pingpong-delay.js index b0a2a18aaf3..a2015c373e7 100644 --- a/test/pummel/test-tcp-pingpong-delay.js +++ b/test/pummel/test-tcp-pingpong-delay.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); var tests_run = 0; @@ -10,7 +10,7 @@ function pingPongTest (port, host, on_complete) { var count = 0; var client_closed = false; - var server = tcp.createServer(function (socket) { + var server = net.createServer(function (socket) { socket.setEncoding("utf8"); socket.addListener("data", function (data) { @@ -44,7 +44,7 @@ function pingPongTest (port, host, on_complete) { }); server.listen(port, host); - var client = tcp.createConnection(port, host); + var client = net.createConnection(port, host); client.setEncoding("utf8"); diff --git a/test/pummel/test-tcp-pingpong.js b/test/pummel/test-tcp-pingpong.js index 135b9a5958d..73d36c82d9d 100644 --- a/test/pummel/test-tcp-pingpong.js +++ b/test/pummel/test-tcp-pingpong.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); var tests_run = 0; @@ -8,13 +8,13 @@ function pingPongTest (port, host, on_complete) { var count = 0; var sent_final_ping = false; - var server = tcp.createServer(function (socket) { + var server = net.createServer(function (socket) { assert.equal(true, socket.remoteAddress !== null); assert.equal(true, socket.remoteAddress !== undefined); if (host === "127.0.0.1") assert.equal(socket.remoteAddress, "127.0.0.1"); else if (host == null) - assert.equal(socket.remoteAddress, "127.0.0.1"); + assert.equal(socket.remoteAddress, "::1"); socket.setEncoding("utf8"); socket.setNoDelay(); @@ -42,7 +42,7 @@ function pingPongTest (port, host, on_complete) { }); server.listen(port, host); - var client = tcp.createConnection(port, host); + var client = net.createConnection(port, host); client.setEncoding("utf8"); @@ -52,6 +52,8 @@ function pingPongTest (port, host, on_complete) { }); client.addListener("data", function (data) { + puts('client got: ' + data); + assert.equal("PONG", data); count += 1; diff --git a/test/pummel/test-tcp-throttle.js b/test/pummel/test-tcp-throttle.js index 2e6fdef3a15..029d3645726 100644 --- a/test/pummel/test-tcp-throttle.js +++ b/test/pummel/test-tcp-throttle.js @@ -1,6 +1,6 @@ require("../common"); -tcp = require("tcp"); -N = 60*1024; // 30kb +net = require("net"); +N = 160*1024; // 30kb puts("build big string"); var body = ""; @@ -10,7 +10,7 @@ for (var i = 0; i < N; i++) { puts("start server on port " + PORT); -server = tcp.createServer(function (connection) { +server = net.createServer(function (connection) { connection.addListener("connect", function () { assert.equal(false, connection.write(body)); connection.close(); @@ -24,7 +24,7 @@ npauses = 0; var paused = false; -client = tcp.createConnection(PORT); +client = net.createConnection(PORT); client.setEncoding("ascii"); client.addListener("data", function (d) { chars_recved += d.length; diff --git a/test/simple/test-buffer.js b/test/simple/test-buffer.js new file mode 100644 index 00000000000..d175c4dfd0d --- /dev/null +++ b/test/simple/test-buffer.js @@ -0,0 +1,76 @@ +require("../common"); +assert = require("assert"); + +var Buffer = require('buffer').Buffer; + +var b = new Buffer(1024); + +puts("b.length == " + b.length); +assert.equal(1024, b.length); + +for (var i = 0; i < 1024; i++) { + assert.ok(b[i] >= 0); + b[i] = i % 256; +} + +for (var i = 0; i < 1024; i++) { + assert.equal(i % 256, b[i]); +} + +var asciiString = "hello world"; +var offset = 100; +for (var j = 0; j < 500; j++) { + + for (var i = 0; i < asciiString.length; i++) { + b[i] = asciiString.charCodeAt(i); + } + var asciiSlice = b.asciiSlice(0, asciiString.length); + assert.equal(asciiString, asciiSlice); + + var written = b.asciiWrite(asciiString, offset); + assert.equal(asciiString.length, written); + var asciiSlice = b.asciiSlice(offset, offset+asciiString.length); + assert.equal(asciiString, asciiSlice); + + var sliceA = b.slice(offset, offset+asciiString.length); + var sliceB = b.slice(offset, offset+asciiString.length); + for (var i = 0; i < asciiString.length; i++) { + assert.equal(sliceA[i], sliceB[i]); + } + + // TODO utf8 slice tests +} + + +for (var j = 0; j < 100; j++) { + var slice = b.slice(100, 150); + assert.equal(50, slice.length); + for (var i = 0; i < 50; i++) { + assert.equal(b[100+i], slice[i]); + } +} + + +// unpack + +var b = new Buffer(10); +b[0] = 0x00; +b[1] = 0x01; +b[2] = 0x03; +b[3] = 0x00; + +assert.deepEqual([0x0001], b.unpack('n', 0)); +assert.deepEqual([0x0001, 0x0300], b.unpack('nn', 0)); +assert.deepEqual([0x0103], b.unpack('n', 1)); +assert.deepEqual([0x0300], b.unpack('n', 2)); +assert.deepEqual([0x00010300], b.unpack('N', 0)); +assert.throws(function () { + b.unpack('N', 8); +}); + +b[4] = 0xDE; +b[5] = 0xAD; +b[6] = 0xBE; +b[7] = 0xEF; + +assert.deepEqual([0xDEADBEEF], b.unpack('N', 4)); diff --git a/test/simple/test-process-buffering.js b/test/simple/test-child-process-buffering.js similarity index 73% rename from test/simple/test-process-buffering.js rename to test/simple/test-child-process-buffering.js index 78377403000..10ec8f847a2 100644 --- a/test/simple/test-process-buffering.js +++ b/test/simple/test-child-process-buffering.js @@ -1,14 +1,19 @@ require("../common"); +var spawn = require('child_process').spawn; + var pwd_called = false; function pwd (callback) { var output = ""; - var child = process.createChildProcess("pwd"); - child.addListener("output", function (s) { + var child = spawn("pwd"); + + child.stdout.setEncoding('utf8'); + child.stdout.addListener("data", function (s) { puts("stdout: " + JSON.stringify(s)); - if (s) output += s; + output += s; }); + child.addListener("exit", function (c) { puts("exit: " + c); assert.equal(0, c); diff --git a/test/simple/test-child-process-env.js b/test/simple/test-child-process-env.js index 4600fea9ef0..d6f9674d9be 100644 --- a/test/simple/test-child-process-env.js +++ b/test/simple/test-child-process-env.js @@ -1,10 +1,15 @@ require("../common"); -child = process.createChildProcess('/usr/bin/env', [], {'HELLO' : 'WORLD'}); + +var spawn = require('child_process').spawn; +child = spawn('/usr/bin/env', [], {'HELLO' : 'WORLD'}); + response = ""; -child.addListener("output", function (chunk) { - puts("stdout: " + JSON.stringify(chunk)); - if (chunk) response += chunk; +child.stdout.setEncoding('utf8'); + +child.stdout.addListener("data", function (chunk) { + puts("stdout: " + chunk); + response += chunk; }); process.addListener('exit', function () { diff --git a/test/simple/test-child-process-ipc.js b/test/simple/test-child-process-ipc.js new file mode 100644 index 00000000000..ca28462aa7b --- /dev/null +++ b/test/simple/test-child-process-ipc.js @@ -0,0 +1,41 @@ +require("../common"); + +var spawn = require('child_process').spawn; + +var path = require('path'); + +var sub = path.join(fixturesDir, 'echo.js'); + +var gotHelloWorld = false; +var gotEcho = false; + +var child = spawn(process.argv[0], [sub]); + +child.stderr.addListener("data", function (data){ + puts("parent stderr: " + data); +}); + +child.stdout.setEncoding('utf8'); + +child.stdout.addListener("data", function (data){ + puts('child said: ' + JSON.stringify(data)); + if (!gotHelloWorld) { + assert.equal("hello world\r\n", data); + gotHelloWorld = true; + child.stdin.write('echo me\r\n'); + } else { + assert.equal("echo me\r\n", data); + gotEcho = true; + child.stdin.close(); + } +}); + +child.stdout.addListener("end", function (data){ + puts('child end'); +}); + + +process.addListener('exit', function () { + assert.ok(gotHelloWorld); + assert.ok(gotEcho); +}); diff --git a/test/simple/test-child-process-kill.js b/test/simple/test-child-process-kill.js new file mode 100644 index 00000000000..d506bef8cb6 --- /dev/null +++ b/test/simple/test-child-process-kill.js @@ -0,0 +1,38 @@ +require("../common"); + +var spawn = require('child_process').spawn; + +var exitStatus = -1; +var gotStdoutEOF = false; +var gotStderrEOF = false; + +var cat = spawn("cat"); + + +cat.stdout.addListener("data", function (chunk) { + assert.ok(false); +}); + +cat.stdout.addListener("end", function () { + gotStdoutEOF = true; +}); + +cat.stderr.addListener("data", function (chunk) { + assert.ok(false); +}); + +cat.stderr.addListener("end", function () { + gotStderrEOF = true; +}); + +cat.addListener("exit", function (status) { + exitStatus = status; +}); + +cat.kill(); + +process.addListener("exit", function () { + assert.ok(exitStatus > 0); + assert.ok(gotStdoutEOF); + assert.ok(gotStderrEOF); +}); diff --git a/test/simple/test-child-process-spawn-loop.js b/test/simple/test-child-process-spawn-loop.js new file mode 100644 index 00000000000..76e4236c193 --- /dev/null +++ b/test/simple/test-child-process-spawn-loop.js @@ -0,0 +1,36 @@ +require("../common"); + +var spawn = require('child_process').spawn; + +var SIZE = 1000 * 1024; +var N = 40; +var finished = false; + +function doSpawn (i) { + var child = spawn( 'python', ['-c', 'print ' + SIZE + ' * "C"']); + var count = 0; + + child.stdout.setEncoding('ascii'); + child.stdout.addListener("data", function (chunk) { + count += chunk.length; + }); + + child.stderr.addListener("data", function (chunk) { + puts('stderr: ' + chunk); + }); + + child.addListener("exit", function () { + assert.equal(SIZE + 1, count); // + 1 for \n + if (i < N) { + doSpawn(i+1); + } else { + finished = true; + } + }); +} + +doSpawn(0); + +process.addListener("exit", function () { + assert.ok(finished); +}); diff --git a/test/simple/test-child-process-stdin.js b/test/simple/test-child-process-stdin.js new file mode 100644 index 00000000000..d60740cb715 --- /dev/null +++ b/test/simple/test-child-process-stdin.js @@ -0,0 +1,48 @@ +require("../common"); + +var spawn = require('child_process').spawn; + +var cat = spawn("cat"); +cat.stdin.write("hello"); +cat.stdin.write(" "); +cat.stdin.write("world"); +cat.stdin.close(); + +var response = ""; +var exitStatus = -1; + +var gotStdoutEOF = false; + +cat.stdout.setEncoding('utf8'); +cat.stdout.addListener("data", function (chunk) { + puts("stdout: " + chunk); + response += chunk; +}); + +cat.stdout.addListener('end', function () { + gotStdoutEOF = true; +}); + + +var gotStderrEOF = false; + +cat.stderr.addListener("data", function (chunk) { + // shouldn't get any stderr output + assert.ok(false); +}); + +cat.stderr.addListener("end", function (chunk) { + gotStderrEOF = true; +}); + + +cat.addListener("exit", function (status) { + puts("exit event"); + exitStatus = status; + assert.equal("hello world", response); +}); + +process.addListener("exit", function () { + assert.equal(0, exitStatus); + assert.equal("hello world", response); +}); diff --git a/test/simple/test-child-process-stdout-flush.js b/test/simple/test-child-process-stdout-flush.js new file mode 100644 index 00000000000..25c51186760 --- /dev/null +++ b/test/simple/test-child-process-stdout-flush.js @@ -0,0 +1,27 @@ +require("../common"); +var path = require('path'); +var spawn = require('child_process').spawn; +var sub = path.join(fixturesDir, 'print-chars.js'); + +n = 500000; + +var child = spawn(process.argv[0], [sub, n]); + +var count = 0; + +child.stderr.setEncoding('utf8'); +child.stderr.addListener("data", function (data) { + puts("parent stderr: " + data); + assert.ok(false); +}); + +child.stderr.setEncoding('utf8'); +child.stdout.addListener("data", function (data) { + count += data.length; + puts(count); +}); + +child.addListener("exit", function (data) { + assert.equal(n, count); + puts("okay"); +}); diff --git a/test/simple/test-exec.js b/test/simple/test-exec.js index d899073324c..10f537b8268 100644 --- a/test/simple/test-exec.js +++ b/test/simple/test-exec.js @@ -1,5 +1,5 @@ require("../common"); - +var exec = require('child_process').exec; success_count = 0; error_count = 0; diff --git a/test/simple/test-file-read-stream.js b/test/simple/test-file-read-stream.js index df57387947b..3ac308b9a26 100644 --- a/test/simple/test-file-read-stream.js +++ b/test/simple/test-file-read-stream.js @@ -60,4 +60,4 @@ process.addListener('exit', function() { for (var k in callbacks) { assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); } -}); \ No newline at end of file +}); diff --git a/test/simple/test-file-write-stream.js b/test/simple/test-file-write-stream.js index 579455c7d91..df045998cd7 100644 --- a/test/simple/test-file-write-stream.js +++ b/test/simple/test-file-write-stream.js @@ -59,4 +59,4 @@ process.addListener('exit', function() { for (var k in callbacks) { assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); } -}); \ No newline at end of file +}); diff --git a/test/simple/test-http-1.0.js b/test/simple/test-http-1.0.js index ceca527719f..105acdcd8ba 100644 --- a/test/simple/test-http-1.0.js +++ b/test/simple/test-http-1.0.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); http = require("http"); var body = "hello world\n"; @@ -13,7 +13,7 @@ var server = http.createServer(function (req, res) { }) server.listen(PORT); -var c = tcp.createConnection(PORT); +var c = net.createConnection(PORT); c.setEncoding("utf8"); diff --git a/test/simple/test-http-chunked.js b/test/simple/test-http-chunked.js index 28d4cc6424c..a838235472d 100644 --- a/test/simple/test-http-chunked.js +++ b/test/simple/test-http-chunked.js @@ -10,7 +10,7 @@ var server = http.createServer(function(req, res) { }); server.listen(PORT); -http.cat("http://localhost:"+PORT+"/", "utf8", function (err, data) { +http.cat("http://127.0.0.1:"+PORT+"/", "utf8", function (err, data) { if (err) throw err; assert.equal(UTF8_STRING, data); server.close(); diff --git a/test/simple/test-http-client-upload.js b/test/simple/test-http-client-upload.js index 4c11bd4078c..b4c0f61acba 100644 --- a/test/simple/test-http-client-upload.js +++ b/test/simple/test-http-client-upload.js @@ -33,7 +33,7 @@ req.write('3\n'); puts("client finished sending request"); req.addListener('response', function(res) { - res.setBodyEncoding("utf8"); + res.setEncoding("utf8"); res.addListener('data', function(chunk) { puts(chunk); }); diff --git a/test/simple/test-http-eof-on-connect.js b/test/simple/test-http-eof-on-connect.js index 67d4b28fe91..1f2f2e5121e 100644 --- a/test/simple/test-http-eof-on-connect.js +++ b/test/simple/test-http-eof-on-connect.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); http = require("http"); // This is a regression test for http://github.com/ry/node/issues/#issue/44 @@ -9,8 +9,8 @@ http = require("http"); server = http.createServer(function (req, res) {}); server.listen(PORT); -tcp.createConnection(PORT).addListener("connect", function () { - this.close(); +net.createConnection(PORT).addListener("connect", function () { + this.close(); }).addListener("close", function () { server.close(); }); diff --git a/test/simple/test-http-malformed-request.js b/test/simple/test-http-malformed-request.js index a1d4b8c91a8..47bcfca3249 100644 --- a/test/simple/test-http-malformed-request.js +++ b/test/simple/test-http-malformed-request.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); http = require("http"); url = require("url"); @@ -20,7 +20,7 @@ var s = http.createServer(function (req, res) { }); s.listen(PORT); -var c = tcp.createConnection(PORT); +var c = net.createConnection(PORT); c.addListener("connect", function () { c.write("GET /hello?foo=%99bar HTTP/1.1\r\n\r\n"); c.close(); diff --git a/test/simple/test-http-parser.js b/test/simple/test-http-parser.js new file mode 100644 index 00000000000..e524cd009dd --- /dev/null +++ b/test/simple/test-http-parser.js @@ -0,0 +1,62 @@ +require("../common"); + +// The purpose of this test is not to check HTTP compliance but to test the +// binding. Tests for pathological http messages should be submitted +// upstream to http://github.com/ry/http-parser for inclusion into +// deps/http-parser/test.c + +var HTTPParser = process.binding('http_parser').HTTPParser; + +var parser = new HTTPParser("request"); + +var Buffer = require('buffer').Buffer; +var buffer = new Buffer(1024); + +var request = "GET /hello HTTP/1.1\r\n\r\n"; + +buffer.asciiWrite(request, 0, request.length); + +var callbacks = 0; + +parser.onMessageBegin = function () { + puts("message begin"); + callbacks++; +}; + +parser.onHeadersComplete = function (info) { + puts("headers complete: " + JSON.stringify(info)); + assert.equal('GET', info.method); + assert.equal(1, info.versionMajor); + assert.equal(1, info.versionMinor); + callbacks++; +}; + +parser.onURL = function (b, off, len) { + //throw new Error("hello world"); + callbacks++; +}; + +parser.onPath = function (b, off, length) { + puts("path [" + off + ", " + length + "]"); + var path = b.asciiSlice(off, off+length); + puts("path = '" + path + "'"); + assert.equal('/hello', path); + callbacks++; +}; + +parser.execute(buffer, 0, request.length); +assert.equal(4, callbacks); + +// +// Check that if we throw an error in the callbacks that error will be +// thrown from parser.execute() +// + +parser.onURL = function (b, off, len) { + throw new Error("hello world"); +}; + +assert.throws(function () { + parser.execute(buffer, 0, request.length); +}, Error, "hello world"); + diff --git a/test/simple/test-http-proxy.js b/test/simple/test-http-proxy.js index 20992b9dc6d..348851d6bab 100644 --- a/test/simple/test-http-proxy.js +++ b/test/simple/test-http-proxy.js @@ -6,12 +6,12 @@ var PROXY_PORT = PORT; var BACKEND_PORT = PORT+1; var backend = http.createServer(function (req, res) { - // debug("backend"); + debug("backend request"); res.writeHead(200, {"content-type": "text/plain"}); res.write("hello world\n"); res.close(); }); -// debug("listen backend") +debug("listen backend") backend.listen(BACKEND_PORT); var proxy_client = http.createClient(BACKEND_PORT); @@ -25,31 +25,40 @@ var proxy = http.createServer(function (req, res) { }); proxy_res.addListener("end", function() { res.close(); - // debug("proxy res"); + debug("proxy res"); }); }); proxy_req.close(); }); -// debug("listen proxy") +debug("listen proxy") proxy.listen(PROXY_PORT); var body = ""; -var client = http.createClient(PROXY_PORT); -var req = client.request("/test"); -// debug("client req") -req.addListener('response', function (res) { - // debug("got res"); - assert.equal(200, res.statusCode); - res.setBodyEncoding("utf8"); - res.addListener('data', function (chunk) { body += chunk; }); - res.addListener('end', function () { - proxy.close(); - backend.close(); - // debug("closed both"); +nlistening = 0; +function startReq () { + nlistening++; + if (nlistening < 2) return; + + var client = http.createClient(PROXY_PORT); + var req = client.request("/test"); + debug("client req") + req.addListener('response', function (res) { + debug("got res"); + assert.equal(200, res.statusCode); + res.setBodyEncoding("utf8"); + res.addListener('data', function (chunk) { body += chunk; }); + res.addListener('end', function () { + proxy.close(); + backend.close(); + debug("closed both"); + }); }); -}); -req.close(); + req.close(); +} + +proxy.addListener('listening', startReq); +backend.addListener('listening', startReq); process.addListener("exit", function () { assert.equal(body, "hello world\n"); diff --git a/test/simple/test-http-server.js b/test/simple/test-http-server.js index fba33ba9fcb..c7deeb8baff 100644 --- a/test/simple/test-http-server.js +++ b/test/simple/test-http-server.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); http = require("http"); url = require("url"); qs = require("querystring"); @@ -43,7 +43,7 @@ http.createServer(function (req, res) { }).listen(PORT); -var c = tcp.createConnection(PORT); +var c = net.createConnection(PORT); c.setEncoding("utf8"); diff --git a/test/simple/test-http-wget.js b/test/simple/test-http-wget.js index 09bda325933..1d7f3412630 100644 --- a/test/simple/test-http-wget.js +++ b/test/simple/test-http-wget.js @@ -1,5 +1,5 @@ require("../common"); -tcp = require("tcp"); +net = require("net"); http = require("http"); // wget sends an HTTP/1.0 request with Connection: Keep-Alive @@ -29,7 +29,7 @@ var server = http.createServer(function (req, res) { }) server.listen(PORT); -var c = tcp.createConnection(PORT); +var c = net.createConnection(PORT); c.setEncoding("utf8"); diff --git a/test/simple/test-net-pingpong.js b/test/simple/test-net-pingpong.js new file mode 100644 index 00000000000..ed8cfebf877 --- /dev/null +++ b/test/simple/test-net-pingpong.js @@ -0,0 +1,108 @@ +require("../common"); + +net = require("net"); + +var tests_run = 0; + +function pingPongTest (port, host) { + var N = 1000; + var count = 0; + var sent_final_ping = false; + + var server = net.createServer(function (socket) { + puts("connection: " + socket.remoteAddress); + assert.equal(server, socket.server); + + socket.setNoDelay(); + socket.timeout = 0; + + socket.setEncoding('utf8'); + socket.addListener("data", function (data) { + puts("server got: " + data); + assert.equal(true, socket.writable); + assert.equal(true, socket.readable); + assert.equal(true, count <= N); + if (/PING/.exec(data)) { + socket.write("PONG"); + } + }); + + socket.addListener("end", function () { + assert.equal(true, socket.writable); + assert.equal(false, socket.readable); + socket.close(); + }); + + socket.addListener("error", function (e) { + throw e; + }); + + socket.addListener("close", function () { + puts('server socket closed'); + assert.equal(false, socket.writable); + assert.equal(false, socket.readable); + socket.server.close(); + }); + }); + + server.addListener("listening", function () { + puts("server listening on " + port + " " + host); + + var client = net.createConnection(port, host); + + client.setEncoding('ascii'); + client.addListener("connect", function () { + assert.equal(true, client.readable); + assert.equal(true, client.writable); + client.write("PING"); + }); + + client.addListener("data", function (data) { + puts("client got: " + data); + + assert.equal("PONG", data); + count += 1; + + if (sent_final_ping) { + assert.equal(false, client.writable); + assert.equal(true, client.readable); + return; + } else { + assert.equal(true, client.writable); + assert.equal(true, client.readable); + } + + if (count < N) { + client.write("PING"); + } else { + sent_final_ping = true; + client.write("PING"); + client.close(); + } + }); + + client.addListener("close", function () { + puts('client closed'); + assert.equal(N+1, count); + assert.equal(true, sent_final_ping); + tests_run += 1; + }); + + client.addListener("error", function (e) { + throw e; + }); + }); + + server.listen(port, host); +} + +/* All are run at once, so run on different ports */ +pingPongTest(20989, "localhost"); +pingPongTest(20988); +pingPongTest(20997, "::1"); +pingPongTest("/tmp/pingpong.sock"); + +process.addListener("exit", function () { + assert.equal(4, tests_run); + puts('done'); +}); diff --git a/test/simple/test-process-kill.js b/test/simple/test-process-kill.js deleted file mode 100644 index 6ee9e5fa3fc..00000000000 --- a/test/simple/test-process-kill.js +++ /dev/null @@ -1,15 +0,0 @@ -require("../common"); - -var exit_status = -1; - -var cat = process.createChildProcess("cat"); - -cat.addListener("output", function (chunk) { assert.equal(null, chunk); }); -cat.addListener("error", function (chunk) { assert.equal(null, chunk); }); -cat.addListener("exit", function (status) { exit_status = status; }); - -cat.kill(); - -process.addListener("exit", function () { - assert.equal(true, exit_status > 0); -}); diff --git a/test/simple/test-process-simple.js b/test/simple/test-process-simple.js deleted file mode 100644 index dad8f44442e..00000000000 --- a/test/simple/test-process-simple.js +++ /dev/null @@ -1,34 +0,0 @@ -require("../common"); - -var cat = process.createChildProcess("cat"); - -var response = ""; -var exit_status = -1; - -cat.addListener("output", function (chunk) { - puts("stdout: " + JSON.stringify(chunk)); - if (chunk) { - response += chunk; - if (response === "hello world") { - puts("closing cat"); - cat.close(); - } - } -}); -cat.addListener("error", function (chunk) { - puts("stderr: " + JSON.stringify(chunk)); - assert.equal(null, chunk); -}); -cat.addListener("exit", function (status) { - puts("exit event"); - exit_status = status; -}); - -cat.write("hello"); -cat.write(" "); -cat.write("world"); - -process.addListener("exit", function () { - assert.equal(0, exit_status); - assert.equal("hello world", response); -}); diff --git a/test/simple/test-stdio.js b/test/simple/test-stdio.js deleted file mode 100644 index 3c33fcfe66f..00000000000 --- a/test/simple/test-stdio.js +++ /dev/null @@ -1,36 +0,0 @@ -require("../common"); -var path = require('path'); - -var sub = path.join(fixturesDir, 'echo.js'); - -var gotHelloWorld = false; -var gotEcho = false; - -var child = process.createChildProcess(process.argv[0], [sub]); - -child.addListener("error", function (data){ - puts("parent stderr: " + data); -}); - -child.addListener("output", function (data){ - if (data) { - puts('child said: ' + JSON.stringify(data)); - if (!gotHelloWorld) { - assert.equal("hello world\r\n", data); - gotHelloWorld = true; - child.write('echo me\r\n'); - } else { - assert.equal("echo me\r\n", data); - gotEcho = true; - child.close(); - } - } else { - puts('child end'); - } -}); - - -process.addListener('exit', function () { - assert.ok(gotHelloWorld); - assert.ok(gotEcho); -}); diff --git a/test/simple/test-stdout-flush.js b/test/simple/test-stdout-flush.js deleted file mode 100644 index 06864a6b876..00000000000 --- a/test/simple/test-stdout-flush.js +++ /dev/null @@ -1,29 +0,0 @@ -require("../common"); -var path = require('path'); - -var sub = path.join(fixturesDir, 'print-chars.js'); - -n = 100000; - -var child = process.createChildProcess(process.argv[0], [sub, n]); - -var count = 0; - -child.addListener("error", function (data){ - if (data) { - puts("parent stderr: " + data); - assert.ok(false); - } -}); - -child.addListener("output", function (data){ - if (data) { - count += data.length; - puts(count); - } -}); - -child.addListener("exit", function (data) { - assert.equal(n, count); - puts("okay"); -}); diff --git a/test/simple/test-tcp-reconnect.js b/test/simple/test-tcp-reconnect.js index 2a1ce652a54..66d93e30d63 100644 --- a/test/simple/test-tcp-reconnect.js +++ b/test/simple/test-tcp-reconnect.js @@ -1,12 +1,12 @@ require("../common"); -tcp = require("tcp"); +net = require('net'); var N = 50; var c = 0; var client_recv_count = 0; var disconnect_count = 0; -var server = tcp.createServer(function (socket) { +var server = net.createServer(function (socket) { socket.addListener("connect", function () { socket.write("hello\r\n"); }); @@ -20,33 +20,38 @@ var server = tcp.createServer(function (socket) { assert.equal(false, had_error); }); }); + server.listen(PORT); -var client = tcp.createConnection(PORT); +server.addListener('listening', function () { + puts('listening'); + var client = net.createConnection(PORT); -client.setEncoding("UTF8"); + client.setEncoding("UTF8"); -client.addListener("connect", function () { - puts("client connected."); -}); + client.addListener("connect", function () { + puts("client connected."); + }); -client.addListener("data", function (chunk) { - client_recv_count += 1; - puts("client_recv_count " + client_recv_count); - assert.equal("hello\r\n", chunk); - client.close(); -}); + client.addListener("data", function (chunk) { + client_recv_count += 1; + puts("client_recv_count " + client_recv_count); + assert.equal("hello\r\n", chunk); + client.close(); + }); -client.addListener("close", function (had_error) { - puts("disconnect"); - assert.equal(false, had_error); - if (disconnect_count++ < N) - client.connect(PORT); // reconnect - else - server.close(); + client.addListener("close", function (had_error) { + puts("disconnect"); + assert.equal(false, had_error); + if (disconnect_count++ < N) + client.connect(PORT); // reconnect + else + server.close(); + }); }); process.addListener("exit", function () { assert.equal(N+1, disconnect_count); assert.equal(N+1, client_recv_count); }); + diff --git a/wscript b/wscript index 1dc666e9b90..cbbd16bcc43 100644 --- a/wscript +++ b/wscript @@ -125,8 +125,8 @@ def configure(conf): #if Options.options.debug: # conf.check(lib='profiler', uselib_store='PROFILER') - #if Options.options.efence: - # conf.check(lib='efence', libpath=['/usr/lib', '/usr/local/lib'], uselib_store='EFENCE') + if Options.options.efence: + conf.check(lib='efence', libpath=['/usr/lib', '/usr/local/lib'], uselib_store='EFENCE') if not conf.check(lib="execinfo", libpath=['/usr/lib', '/usr/local/lib'], uselib_store="EXECINFO"): # Note on Darwin/OS X: This will fail, but will still be used as the @@ -393,6 +393,10 @@ def build(bld): node.target = "node" node.source = """ src/node.cc + src/node_buffer.cc + src/node_http_parser.cc + src/node_net2.cc + src/node_io_watcher.cc src/node_child_process.cc src/node_constants.cc src/node_dns.cc