var net = require('net'); var readline = require('readline'); var inherits = require('util').inherits; var spawn = require('child_process').spawn; _AN_Write_port('port', exports, false , 5858); exports.start = function (){ var interface = new Interface(); } ; var args = process.argv.slice(2); args.unshift('--debug-brk'); function Protocol(){ this._newRes(); } _AN_Write_protocol('Protocol', exports, false , Protocol); Protocol.prototype._newRes = function (raw){ this.res = { raw: raw || '', headers: { } } ; this.state = 'headers'; this.reqSeq = 1; this.execute(''); } ; Protocol.prototype.execute = function (d){ var res = this.res; res.raw += d; switch (this.state){ case 'headers': var endHeaderIndex = res.raw.indexOf('\r\n\r\n'); if (endHeaderIndex < 0) break ; var lines = res.raw.slice(0, endHeaderIndex).split('\r\n'); for (var i = 0; i < _AN_Read_length('length', lines); i++ ){ var kv = lines[i].split(/: +/); res.headers[kv[0]] = kv[1]; } this.contentLength = + res.headers["Content-Length"] ; this.bodyStartIndex = endHeaderIndex + 4; this.state = 'body'; if (_AN_Read_length('length', res.raw) - this.bodyStartIndex < this.contentLength) break ; case 'body': if (_AN_Read_length('length', res.raw) - this.bodyStartIndex >= this.contentLength) { res.body = res.raw.slice(this.bodyStartIndex, this.bodyStartIndex + this.contentLength); res.body = _AN_Read_length('length', res.body)? JSON.parse(res.body): { } ; this.onResponse(res); this._newRes(res.raw.slice(this.bodyStartIndex + this.contentLength)); } break ; default : { throw new Error("Unknown state") break ; } } } ; Protocol.prototype.serialize = function (req){ req.type = 'request'; req.seq = this.reqSeq++ ; var json = JSON.stringify(req); return 'Content-Length: ' + _AN_Read_length('length', json) + '\r\n\r\n' + json; } ; var NO_FRAME = -1; function Client(){ net.Stream.call(this); var protocol = _AN_Write_protocol('protocol', this, false , new Protocol(this)); this._reqCallbacks = [] ; var socket = this; this.currentFrame = NO_FRAME; this.currentSourceLine = -1; this.currentSource = null ; this.handles = { } ; this.scripts = { } ; socket.setEncoding('utf8'); socket.on('data', function (d){ protocol.execute(d); } ); protocol.onResponse = this._onResponse.bind(this); } ; inherits(Client, net.Stream); exports.Client = Client; Client.prototype._addHandle = function (desc){ if (typeof desc != 'object' || !desc.handle) throw new Error("bad type") this.handles[desc.id] = desc; if (desc.type == 'script') { this._addScript(desc); } } ; var natives = process.binding('natives'); Client.prototype._addScript = function (desc){ this.scripts[desc.id] = desc; if (desc.name) { desc.isNative = (_AN_Call_replace('replace', desc.name, '.js', '') in natives) || desc.name == 'node.js'; } } ; Client.prototype._removeScript = function (desc){ this.scripts[desc.id] = undefined; } ; Client.prototype._onResponse = function (res){ for (var i = 0; i < _AN_Read_length('length', this._reqCallbacks); i++ ){ var cb = this._reqCallbacks[i]; if (this._reqCallbacks[i].request_seq == res.body.request_seq) break ; } var self = this; var handled = false ; if (res.headers.Type == 'connect') { self.reqScripts(); self.emit('ready'); handled = true ; } else if (res.body && res.body.event == 'break') { this.emit('break', res.body); handled = true ; } else if (res.body && res.body.event == 'afterCompile') { this._addHandle(res.body.body.script); handled = true ; } else if (res.body && res.body.event == 'scriptCollected') { this._removeScript(res.body.body.script); handled = true ; } if (cb) { this._reqCallbacks.splice(i, 1); handled = true ; cb(res.body); } if (!handled) this.emit('unhandledResponse', res.body); } ; Client.prototype.req = function (req, cb){ _AN_Call_write('write', this, _AN_Read_protocol('protocol', this).serialize(req)); cb.request_seq = req.seq; this._reqCallbacks.push(cb); } ; Client.prototype.reqVersion = function (cb){ this.req({ command: 'version'} , function (res){ if (cb) cb(res.body.V8Version, res.running); } ); } ; Client.prototype.reqEval = function (expression, cb){ var req = { command: 'evaluate', arguments: { expression: expression} } ; if (this.currentFrame == NO_FRAME) { req.arguments.global = true ; } else { req.arguments.frame = this.currentFrame; } this.req(req, function (res){ if (cb) cb(res.body); } ); } ; Client.prototype.reqBacktrace = function (cb){ this.req({ command: 'backtrace'} , function (res){ if (cb) cb(res.body); } ); } ; Client.prototype.reqScripts = function (cb){ var self = this; this.req({ command: 'scripts'} , function (res){ for (var i = 0; i < _AN_Read_length('length', res.body); i++ ){ self._addHandle(res.body[i]); } if (cb) cb(); } ); } ; Client.prototype.reqContinue = function (cb){ this.req({ command: 'continue'} , function (res){ if (cb) cb(res); } ); } ; Client.prototype.listbreakpoints = function (cb){ this.req({ command: 'listbreakpoints'} , function (res){ if (cb) cb(res); } ); } ; Client.prototype.step = function (action, count, cb){ var req = { command: 'continue', arguments: { stepaction: action, stepcount: count} } ; this.req(req, function (res){ if (cb) cb(res); } ); } ; var helpMessage = "Commands: run, kill, print, step, next, " + "continue, scripts, backtrace, version, quit"; function SourceUnderline(sourceText, position){ if (!sourceText) return ; var underline = ''; for (var i = 0; i < position; i++ ){ if (sourceText[i] == '\t') { underline += '\t'; } else { underline += ' '; } } underline += '^'; return sourceText + '\n' + underline; } function SourceInfo(body){ var result = ''; if (body.script) { if (body.script.name) { result += body.script.name; } else { result += '[unnamed]'; } } result += ':'; result += body.sourceLine + 1; return result; } function Interface(){ var self = this; var term = this.term = readline.createInterface(process.stdout); var child; var client; var term; process.on('exit', function (){ self.killChild(); } ); this.stdin = process.openStdin(); this.stdin.addListener('data', function (chunk){ _AN_Call_write('write', term, chunk); } ); term.setPrompt('debug> '); term.prompt(); this.quitting = false ; process.on('SIGINT', function (){ self.handleSIGINT(); } ); term.on('SIGINT', function (){ self.handleSIGINT(); } ); term.on('close', function (){ self.tryQuit(); } ); term.on('line', function (cmd){ cmd = _AN_Call_replace('replace', _AN_Call_replace('replace', cmd, /^\s*/, ''), /\s*$/, ''); if (cmd.length) { self._lastCommand = cmd; self.handleCommand(cmd); } else { self.handleCommand(self._lastCommand); } } ); } Interface.prototype.handleSIGINT = function (){ if (this.paused) { this.child.kill('SIGINT'); } else { this.tryQuit(); } } ; Interface.prototype.quit = function (){ if (this.quitting) return ; this.quitting = true ; this.killChild(); this.term.close(); process.exit(0); } ; Interface.prototype.tryQuit = function (){ var self = this; if (self.child) { self.quitQuestion(function (yes){ if (yes) { self.quit(); } else { self.term.prompt(); } } ); } else { self.quit(); } } ; Interface.prototype.pause = function (){ this.paused = true ; this.stdin.pause(); this.term.pause(); } ; Interface.prototype.resume = function (){ if (!this.paused) return false ; this.paused = false ; this.stdin.resume(); this.term.resume(); this.term.prompt(); return true ; } ; Interface.prototype.handleBreak = function (r){ var result = ''; if (r.breakpoints) { result += 'breakpoint'; if (_AN_Read_length('length', r.breakpoints) > 1) { result += 's'; } result += ' #'; for (var i = 0; i < _AN_Read_length('length', r.breakpoints); i++ ){ if (i > 0) { result += ', #'; } result += r.breakpoints[i]; } } else { result += 'break'; } result += ' in '; result += r.invocationText; result += ', '; result += SourceInfo(r); result += '\n'; result += SourceUnderline(r.sourceLineText, r.sourceColumn); this.client.currentSourceLine = r.sourceLine; this.client.currentFrame = 0; this.client.currentScript = r.script.name; console.log(result); if (!this.resume()) this.term.prompt(); } ; Interface.prototype.handleCommand = function (cmd){ var self = this; var client = this.client; var term = this.term; if (cmd == 'quit' || cmd == 'q' || cmd == 'exit') { self._lastCommand = null ; self.tryQuit(); } else if (/^r(un)?/.test(cmd)) { self._lastCommand = null ; if (self.child) { self.restartQuestion(function (yes){ if (!yes) { self._lastCommand = null ; term.prompt(); } else { console.log("restarting..."); self.killChild(); _AN_Call_settimeout("setTimeout", window, function (){ self.trySpawn(); } , 1000); } } ); } else { self.trySpawn(); } } else if (/^help/.test(cmd)) { console.log(helpMessage); term.prompt(); } else if ('version' == cmd) { if (!client) { self.printNotConnected(); return ; } client.reqVersion(function (v){ console.log(v); term.prompt(); } ); } else if (/info +breakpoints/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } client.listbreakpoints(function (res){ console.log(res); term.prompt(); } ); } else if (/^backtrace/.test(cmd) || /^bt/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } client.reqBacktrace(function (bt){ if (/full/.test(cmd)) { console.log(bt); } else if (bt.totalFrames == 0) { console.log('(empty stack)'); } else { var result = ''; for (j = 0; j < _AN_Read_length('length', bt.frames); j++ ){ if (j != 0) result += '\n'; result += bt.frames[j].text; } console.log(result); } term.prompt(); } ); } else if (cmd == 'scripts' || cmd == 'scripts full') { if (!client) { self.printNotConnected(); return ; } self.printScripts(cmd.indexOf('full') > 0); term.prompt(); } else if (/^c(ontinue)?/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } self.pause(); client.reqContinue(function (){ self.resume(); } ); } else if (/^k(ill)?/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } if (self.child) { self.killQuestion(function (yes){ if (yes) { self.killChild(); } else { self._lastCommand = null ; } } ); } else { self.term.prompt(); } } else if (/^next/.test(cmd) || /^n/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } client.step('next', 1, function (res){ } ); } else if (/^step/.test(cmd) || /^s/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } client.step('in', 1, function (res){ } ); } else if (/^print/.test(cmd) || /^p/.test(cmd)) { if (!client) { self.printNotConnected(); return ; } var i = cmd.indexOf(' '); if (i < 0) { console.log("print [expression]"); term.prompt(); } else { cmd = cmd.slice(i); client.reqEval(cmd, function (res){ if (res) { console.log(res.text); } else { console.log(res); } term.prompt(); } ); } } else { if (!/^\s*$/.test(cmd)) { console.log('Unknown command "%s". Try "help"', cmd); } term.prompt(); } } ; Interface.prototype.yesNoQuestion = function (prompt, cb){ var self = this; self.resume(); this.term.question(prompt, function (answer){ if (/^y(es)?$/i.test(answer)) { cb(true ); } else if (/^n(o)?$/i.test(answer)) { cb(false ); } else { console.log("Please answer y or n."); self.restartQuestion(cb); } } ); } ; Interface.prototype.restartQuestion = function (cb){ this.yesNoQuestion("The program being debugged has been started already.\n" + "Start it from the beginning? (y or n) ", cb); } ; Interface.prototype.killQuestion = function (cb){ this.yesNoQuestion("Kill the program being debugged? (y or n) ", cb); } ; Interface.prototype.quitQuestion = function (cb){ this.yesNoQuestion("A debugging session is active. Quit anyway? (y or n) ", cb); } ; Interface.prototype.killChild = function (){ if (this.child) { this.child.kill(); this.child = null ; } if (this.client) { this.client.destroy(); this.client = null ; } this.resume(); } ; Interface.prototype.trySpawn = function (cb){ var self = this; this.killChild(); this.child = spawn(process.execPath, args, { customFds: [0, 1, 2] } ); this.pause(); _AN_Call_settimeout("setTimeout", window, function (){ _AN_Call_write("write", process.stdout, "connecting..."); var client = self.client = new Client(); client.connect(_AN_Read_port("port", exports)); client.once('ready', function (){ _AN_Call_write('write', process.stdout, "ok\r\n"); client.reqContinue(function (){ if (cb) cb(); } ); } ); client.on('close', function (){ console.log("\nprogram terminated"); self.client = null ; self.killChild(); if (!self.quitting) self.term.prompt(); } ); client.on('unhandledResponse', function (res){ console.log("\r\nunhandled res:"); console.log(res); self.term.prompt(); } ); client.on('break', function (res){ self.handleBreak(res.body); } ); } , 100); } ; Interface.prototype.printNotConnected = function (){ console.log("Program not running. Try 'run'."); this.term.prompt(); } ; Interface.prototype.printScripts = function (displayNatives){ var client = this.client; var text = ''; for (var id in client.scripts){ var script = client.scripts[id]; if (typeof script == 'object' && script.name) { if (displayNatives || script.name == client.currentScript || !script.isNative) { text += script.name == client.currentScript? '* ': ' '; text += script.name + '\n'; } } } _AN_Call_write('write', process.stdout, text); } ;