debugger.js
10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"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;