"use strict"; var net = require("net"); var selenium_webdriver_1 = require("selenium-webdriver"); var util = require("util"); var logger_1 = require("./logger"); var helper = require("./util"); var logger = new logger_1.Logger('protractor'); var DebugHelper = (function () { function DebugHelper(browserUnderDebug_) { this.browserUnderDebug_ = browserUnderDebug_; } /** * 1) Set up helper functions for debugger clients to call on (e.g. * getControlFlowText, execute code, get autocompletion). * 2) Enter process into debugger mode. (i.e. process._debugProcess). * 3) Invoke the debugger client specified by debuggerClientPath. * * @param {string} debuggerClientPath Absolute path of debugger client to use. * @param {Function} onStartFn Function to call when the debugger starts. The * function takes a single parameter, which represents whether this is the * first time that the debugger is called. * @param {number=} opt_debugPort Optional port to use for the debugging * process. */ DebugHelper.prototype.init = function (debuggerClientPath, onStartFn, opt_debugPort) { var _this = this; selenium_webdriver_1.promise.ControlFlow.prototype.getControlFlowText = function () { var controlFlowText = this.getSchedule(/* opt_includeStackTraces */ true); // This filters the entire control flow text, not just the stack trace, so // unless we maintain a good (i.e. non-generic) set of keywords in // STACK_SUBSTRINGS_TO_FILTER, we run the risk of filtering out non stack // trace. The alternative though, which is to reimplement // webdriver.promise.ControlFlow.prototype.getSchedule() here is much // hackier, and involves messing with the control flow's internals / // private variables. return helper.filterStackTrace(controlFlowText); }; var vm_ = require('vm'); var flow = selenium_webdriver_1.promise.controlFlow(); var context = { require: require }; global.list = function (locator) { return global.protractor.browser.findElements(locator).then(function (arr) { var found = []; for (var i = 0; i < arr.length; ++i) { arr[i].getText().then(function (text) { found.push(text); }); } return found; }); }; for (var key in global) { context[key] = global[key]; } var sandbox = vm_.createContext(context); var debuggerReadyPromise = selenium_webdriver_1.promise.defer(); flow.execute(function () { process['debugPort'] = opt_debugPort || process['debugPort']; _this.validatePortAvailability_(process['debugPort']).then(function (firstTime) { onStartFn(firstTime); var args = [process.pid, process['debugPort']]; if (_this.browserUnderDebug_.debuggerServerPort) { args.push(_this.browserUnderDebug_.debuggerServerPort); } var nodedebug = require('child_process').fork(debuggerClientPath, args); process.on('exit', function () { nodedebug.kill('SIGTERM'); }); nodedebug .on('message', function (m) { if (m === 'ready') { debuggerReadyPromise.fulfill(); } }) .on('exit', function () { logger.info('Debugger exiting'); // Clear this so that we know it's ok to attach a debugger // again. _this.dbgCodeExecutor = null; }); }); }); var pausePromise = flow.execute(function () { return debuggerReadyPromise.then(function () { // Necessary for backward compatibility with node < 0.12.0 return _this.browserUnderDebug_.executeScriptWithDescription('', 'empty debugger hook'); }); }); // Helper used only by debuggers at './debugger/modes/*.js' to insert code // into the control flow. // In order to achieve this, we maintain a promise at the top of the control // flow, so that we can insert frames into it. // To be able to simulate callback/asynchronous code, we poll this object // for a result at every run of DeferredExecutor.execute. var browserUnderDebug = this.browserUnderDebug_; this.dbgCodeExecutor = { execPromise_: pausePromise, execPromiseResult_: undefined, execPromiseError_: undefined, // A dummy repl server to make use of its completion function. replServer_: require('repl').start({ input: { on: function () { }, resume: function () { } }, // dummy readable stream output: { write: function () { } }, useGlobal: true }), // Execute a function, which could yield a value or a promise, // and allow its result to be accessed synchronously execute_: function (execFn_) { var _this = this; this.execPromiseResult_ = this.execPromiseError_ = undefined; this.execPromise_ = this.execPromise_.then(execFn_).then(function (result) { _this.execPromiseResult_ = result; }, function (err) { _this.execPromiseError_ = err; }); // This dummy command is necessary so that the DeferredExecutor.execute // break point can find something to stop at instead of moving on to the // next real command. this.execPromise_.then(function () { return browserUnderDebug.executeScriptWithDescription('', 'empty debugger hook'); }); }, // Execute a piece of code. // Result is a string representation of the evaluation. execute: function (code) { var _this = this; var execFn_ = function () { // Run code through vm so that we can maintain a local scope which is // isolated from the rest of the execution. var res = vm_.runInContext(code, sandbox); if (!selenium_webdriver_1.promise.isPromise(res)) { res = selenium_webdriver_1.promise.fulfilled(res); } return res.then(function (res) { if (res === undefined) { return undefined; } else { // The '' forces res to be expanded into a string instead of just // '[Object]'. Then we remove the extra space caused by the '' // using substring. return util.format.apply(_this, ['', res]).substring(1); } }); }; this.execute_(execFn_); }, // Autocomplete for a line. // Result is a JSON representation of the autocomplete response. complete: function (line) { var _this = this; var execFn_ = function () { var deferred = selenium_webdriver_1.promise.defer(); _this.replServer_.complete(line, function (err, res) { if (err) { deferred.reject(err); } else { deferred.fulfill(JSON.stringify(res)); } }); return deferred; }; this.execute_(execFn_); }, // Code finished executing. resultReady: function () { return !this.execPromise_.isPending(); }, // Get asynchronous results synchronously. // This will throw if result is not ready. getResult: function () { if (!this.resultReady()) { throw new Error('Result not ready'); } if (this.execPromiseError_) { throw this.execPromiseError_; } return this.execPromiseResult_; } }; return pausePromise; }; /** * Validates that the port is free to use. This will only validate the first * time it is called. The reason is that on subsequent calls, the port will * already be bound to the debugger, so it will not be available, but that is * okay. * * @returns {Promise} A promise that becomes ready when the * validation * is done. The promise will resolve to a boolean which represents whether * this is the first time that the debugger is called. */ DebugHelper.prototype.validatePortAvailability_ = function (port) { var _this = this; if (this.debuggerValidated_) { return selenium_webdriver_1.promise.fulfilled(false); } var doneDeferred = selenium_webdriver_1.promise.defer(); // Resolve doneDeferred if port is available. var tester = net.connect({ port: port }, function () { doneDeferred.reject('Port ' + port + ' is already in use. Please specify ' + 'another port to debug.'); }); tester.once('error', function (err) { if (err.code === 'ECONNREFUSED') { tester .once('close', function () { doneDeferred.fulfill(true); }) .end(); } else { doneDeferred.reject('Unexpected failure testing for port ' + port + ': ' + JSON.stringify(err)); } }); return doneDeferred.then(function () { _this.debuggerValidated_ = true; }, function (err) { console.error(err); process.exit(1); }); }; DebugHelper.prototype.isAttached = function () { return !!this.dbgCodeExecutor; }; return DebugHelper; }()); exports.DebugHelper = DebugHelper;