debugger.js 10.4 KB
"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<boolean>} 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;