Skip to content
This repository was archived by the owner on Jul 29, 2024. It is now read-only.

Commit a40f682

Browse files
committed
feat(debugger): make element explorer work with node 0.12.0
Node has changed its debugger significantly in 0.12.0, and these changes are necessary to get it to work now. Specifically here are the key points to take note: * Before, the process continues running after `process._debugProcess(pid);` is called, but now, the process stops. > To over come this, the call to `process._debugProcess(pid)` is moved from protractor into the debugger client. Secondly, because the process stops after the call, we call `reqContinue` once the debugger connection is established, so that protractor continues into the first break point (for backwards compatibility, we added an extra empty webdriver command so that in earlier versions of node, protractor doesn't go past the first break point from the reqContinue). * Before repl provides '(foobar\n)' when an user inputs 'foobar'. Now it is just 'foobar\n'. > We will parse and strip away both the parenthesis and '\n' to support all versions of node. * Additionally (non-related to node 0.12.0), this change makes debugger processes fail fast if the port is taken.
1 parent 45341c9 commit a40f682

File tree

6 files changed

+147
-72
lines changed

6 files changed

+147
-72
lines changed

lib/debugger/clients/explorer.js

+6-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var repl = require('repl');
2-
var baseDebugger = require('_debugger');
2+
var debuggerCommons = require('../debuggerCommons');
33
var CommandRepl = require('../modes/commandRepl');
44

55
/**
@@ -9,28 +9,16 @@ var CommandRepl = require('../modes/commandRepl');
99
* @constructor
1010
*/
1111
var WdRepl = function() {
12-
this.client = new baseDebugger.Client();
12+
this.client;
1313
};
1414

1515
/**
1616
* Initiate debugger client.
1717
* @private
1818
*/
1919
WdRepl.prototype.initClient_ = function() {
20-
var client = this.client;
21-
22-
client.once('ready', function() {
23-
24-
client.setBreakpoint({
25-
type: 'scriptRegExp',
26-
target: '.*executors\.js', //jshint ignore:line
27-
line: 37
28-
}, function() {});
29-
});
30-
31-
var host = 'localhost';
32-
var port = process.argv[2] || 5858;
33-
client.connect(port, host); // TODO - might want to add retries here.
20+
this.client =
21+
debuggerCommons.attachDebugger(process.argv[2], process.argv[3]);
3422
};
3523

3624
/**
@@ -103,7 +91,7 @@ WdRepl.prototype.initRepl_ = function() {
10391
var stepEval = function(cmd, context, filename, callback) {
10492
// The command that eval feeds is of the form '(CMD\n)', so we trim the
10593
// double quotes and new line.
106-
cmd = cmd.slice(1, cmd.length - 2);
94+
cmd = debuggerCommons.trimReplCmd(cmd);
10795
cmdRepl.stepEval(cmd, function(err, res) {
10896
// Result is a string representation of the evaluation.
10997
if (res !== undefined) {
@@ -142,7 +130,7 @@ WdRepl.prototype.initReplOrServer_ = function() {
142130
// advantage is that the process becomes much more realistic because now we're
143131
// using the normal repl. However, it was not possible to test autocomplete
144132
// this way since we cannot immitate the TAB key over the wire.
145-
var debuggerServerPort = process.argv[3];
133+
var debuggerServerPort = process.argv[4];
146134
if (debuggerServerPort) {
147135
this.initServer_(debuggerServerPort);
148136
} else {

lib/debugger/clients/wddebugger.js

+26-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var repl = require('repl');
2-
var baseDebugger = require('_debugger');
2+
var debuggerCommons = require('../debuggerCommons');
33
var CommandRepl = require('../modes/commandRepl');
44
var DebuggerRepl = require('../modes/debuggerRepl');
55

@@ -11,7 +11,7 @@ var DebuggerRepl = require('../modes/debuggerRepl');
1111
* @constructor
1212
*/
1313
var WdDebugger = function() {
14-
this.client = new baseDebugger.Client();
14+
this.client;
1515
this.replServer;
1616

1717
// repl is broken into 'command repl' and 'debugger repl'.
@@ -26,28 +26,17 @@ var WdDebugger = function() {
2626
* @private
2727
*/
2828
WdDebugger.prototype.initClient_ = function() {
29-
var client = this.client;
30-
31-
client.once('ready', function() {
29+
this.client =
30+
debuggerCommons.attachDebugger(process.argv[2], process.argv[3]);
31+
this.client.once('ready', function() {
3232
console.log(' ready\n');
33-
34-
client.setBreakpoint({
35-
type: 'scriptRegExp',
36-
target: '.*executors\.js', //jshint ignore:line
37-
line: 37
38-
}, function() {
39-
console.log('press c to continue to the next webdriver command');
40-
console.log('press d to continue to the next debugger statement');
41-
console.log('type "repl" to enter interactive mode');
42-
console.log('type "exit" to break out of interactive mode');
43-
console.log('press ^C to exit');
44-
console.log();
45-
});
33+
console.log('press c to continue to the next webdriver command');
34+
console.log('press d to continue to the next debugger statement');
35+
console.log('type "repl" to enter interactive mode');
36+
console.log('type "exit" to break out of interactive mode');
37+
console.log('press ^C to exit');
38+
console.log();
4639
});
47-
48-
var host = 'localhost';
49-
var port = process.argv[2] || 5858;
50-
client.connect(port, host); // TODO - might want to add retries here.
5140
};
5241

5342
/**
@@ -60,19 +49,26 @@ WdDebugger.prototype.initClient_ = function() {
6049
*/
6150
WdDebugger.prototype.stepEval_ = function(cmd, context, filename, callback) {
6251
// The loop won't come back until 'callback' is called.
63-
// Strip out the () which the REPL adds and the new line.
6452
// Note - node's debugger gets around this by adding custom objects
6553
// named 'c', 's', etc to the REPL context. They have getters which
6654
// perform the desired function, and the callback is stored for later use.
6755
// Think about whether this is a better pattern.
68-
cmd = cmd.slice(1, cmd.length - 2);
56+
57+
cmd = debuggerCommons.trimReplCmd(cmd);
6958

7059
if (this.currentRepl === this.dbgRepl && cmd === 'repl' ||
7160
this.currentRepl === this.cmdRepl && cmd === 'exit') {
7261
// switch repl mode
7362
this.currentRepl =
7463
this.currentRepl === this.dbgRepl ? this.cmdRepl : this.dbgRepl;
75-
this.replServer.prompt = this.currentRepl.prompt;
64+
// For node backward compatibility. In older versions of node `setPrompt`
65+
// does not exist, and we set the prompt by overwriting `replServer.prompt`
66+
// directly.
67+
if (this.replServer.setPrompt) {
68+
this.replServer.setPrompt(this.currentRepl.prompt);
69+
} else {
70+
this.replServer.prompt = this.currentRepl.prompt;
71+
}
7672
this.replServer.complete = this.currentRepl.complete.bind(this.currentRepl);
7773
callback();
7874
} else if (this.currentRepl === this.cmdRepl) {
@@ -103,6 +99,11 @@ WdDebugger.prototype.initRepl_ = function() {
10399

104100
// We want the prompt to show up only after the controlflow text prints.
105101
this.dbgRepl.printControlFlow_(function() {
102+
// Backward compatibility: node version 0.8.14 has a number of built in
103+
// libraries for repl, and the keyword 'repl' clashes with our usage.
104+
if (repl._builtinLibs && repl._builtinLibs.indexOf('repl') > -1) {
105+
repl._builtinLibs.splice(repl._builtinLibs.indexOf('repl'), 1);
106+
}
106107
self.replServer = repl.start({
107108
prompt: self.currentRepl.prompt,
108109
input: process.stdin,

lib/debugger/debuggerCommons.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
var baseDebugger = require('_debugger');
2+
3+
/**
4+
* Create a debugger client and attach to a running protractor process.
5+
* Set a break point at webdriver executor.
6+
* @param {number} pid Pid of the process to attach the debugger to.
7+
* @param {number=} opt_port Port to set up the debugger connection over.
8+
* @return {!baseDebugger.Client} The connected debugger client.
9+
*/
10+
exports.attachDebugger = function(pid, opt_port) {
11+
var client = new baseDebugger.Client();
12+
var port = opt_port || process.debugPort;
13+
14+
// Call this private function instead of sending SIGUSR1 because Windows.
15+
process._debugProcess(pid);
16+
17+
client.once('ready', function() {
18+
client.setBreakpoint({
19+
type: 'scriptRegExp',
20+
target: '.*executors\.js', //jshint ignore:line
21+
line: 37
22+
}, function() {
23+
client.reqContinue(function() {
24+
// Intentionally blank.
25+
});
26+
});
27+
});
28+
29+
// Connect to debugger on port with retry 200ms apart.
30+
var connectWithRetry = function(attempts) {
31+
client.connect(port, 'localhost')
32+
.on('error', function(e) {
33+
if (attempts === 1) {
34+
throw e;
35+
} else {
36+
setTimeout(function() {
37+
connectWithRetry(attempts - 1);
38+
}, 200);
39+
}
40+
});
41+
};
42+
connectWithRetry(10);
43+
44+
return client;
45+
};
46+
47+
/**
48+
* Trim excess symbols from the repl command so that it is consistent with
49+
* the user input.
50+
* @param {string} cmd Cmd provided by the repl server.
51+
* @return {string} The trimmed cmd.
52+
*/
53+
exports.trimReplCmd = function(cmd) {
54+
// Given user input 'foobar', some versions of node provide '(foobar\n)',
55+
// while other versions of node provide 'foobar\n'.
56+
if (cmd.length >= 2 && cmd[0] === '(' && cmd[cmd.length - 1] === ')') {
57+
cmd = cmd.substring(1, cmd.length - 1);
58+
}
59+
return cmd.slice(0, cmd.length - 1);
60+
};

lib/protractor.js

+47-28
Original file line numberDiff line numberDiff line change
@@ -661,34 +661,30 @@ Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort)
661661
return asString;
662662
};
663663

664-
if (opt_debugPort) {
665-
process.debugPort = opt_debugPort;
666-
}
667-
668-
// Call this private function instead of sending SIGUSR1 because Windows.
669-
process._debugProcess(process.pid);
670-
671-
var flow = webdriver.promise.controlFlow();
672-
var self = this;
673-
var pausePromise = flow.execute(function() {
674-
log.puts('Starting WebDriver debugger in a child process. Pause is ' +
675-
'still beta, please report issues at github.com/angular/protractor\n');
676-
var args = [process.debugPort];
677-
if (self.debuggerServerPort_) {
678-
args.push(self.debuggerServerPort_);
679-
}
680-
var nodedebug = require('child_process').fork(debuggerClientPath, args);
681-
process.on('exit', function() {
682-
nodedebug.kill('SIGTERM');
664+
// Invoke fn if port is available.
665+
var onPortAvailable = function(port, fn) {
666+
var net = require('net');
667+
var tester = net.connect({port: port}, function() {
668+
console.error('Port ' + port + ' is already in use. Please specify ' +
669+
'another port to debug.');
670+
process.exit(1);
683671
});
684-
});
672+
tester.once('error', function (err) {
673+
if (err.code === 'ECONNREFUSED') {
674+
tester.once('close', fn).end();
675+
} else {
676+
console.error('Unexpected failure testing for port ' + port + ': ',
677+
err);
678+
process.exit(1);
679+
}
680+
});
681+
};
685682

686683
var vm_ = require('vm');
684+
var flow = webdriver.promise.controlFlow();
685+
687686
var context = { require: require };
688-
for (var key in global) {
689-
context[key] = global[key];
690-
}
691-
context.list = function(locator) {
687+
global.list = function(locator) {
692688
/* globals browser */
693689
return browser.findElements(locator).then(function(arr) {
694690
var found = [];
@@ -700,9 +696,33 @@ Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort)
700696
return found;
701697
});
702698
};
699+
for (var key in global) {
700+
context[key] = global[key];
701+
}
703702
var sandbox = vm_.createContext(context);
704-
703+
705704
var browserUnderDebug = this;
705+
flow.execute(function() {
706+
log.puts('Starting WebDriver debugger in a child process. Pause is ' +
707+
'still beta, please report issues at github.com/angular/protractor\n');
708+
process.debugPort = opt_debugPort || process.debugPort;
709+
onPortAvailable(process.debugPort, function() {
710+
var args = [process.pid, process.debugPort];
711+
if (browserUnderDebug.debuggerServerPort_) {
712+
args.push(browserUnderDebug.debuggerServerPort_);
713+
}
714+
var nodedebug = require('child_process').fork(debuggerClientPath, args);
715+
process.on('exit', function() {
716+
nodedebug.kill('SIGTERM');
717+
});
718+
});
719+
});
720+
721+
var pausePromise = flow.timeout(1000, 'waiting for debugger to attach')
722+
.then(function() {
723+
// Necessary for backward compatibility with node < 0.12.0
724+
browserUnderDebug.executeScript_('', 'empty debugger hook');
725+
});
706726

707727
// Helper used only by debuggers at './debugger/modes/*.js' to insert code
708728
// into the control flow.
@@ -718,7 +738,8 @@ Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort)
718738
// A dummy repl server to make use of its completion function.
719739
replServer_: require('repl').start({
720740
input: {on: function() {}, resume: function() {}}, // dummy readable stream
721-
output: {write: function() {}} // dummy writable stream
741+
output: {write: function() {}}, // dummy writable stream
742+
useGlobal: true
722743
}),
723744

724745
// Execute a function, which could yield a value or a promise,
@@ -802,8 +823,6 @@ Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort)
802823
return this.execPromiseResult_;
803824
}
804825
};
805-
806-
flow.timeout(1000, 'waiting for debugger to attach');
807826
};
808827

809828
/**

scripts/interactive_tests/interactive_test.js

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ test.addCommandExpectation('element(by.binding("nonexistent")).getText()',
3434
'ERROR: NoSuchElementError: No element found using locator: ' +
3535
'by.binding("nonexistent")');
3636

37+
// Check global `list` works.
38+
test.addCommandExpectation('list(by.binding("greeting"))', '[ \'Hiya\' ]');
39+
test.addCommandExpectation('list(by.binding("nonexistent"))', '[]');
40+
3741
// Check complete calls
3842
test.addCommandExpectation('\t',
3943
'[["element(by.id(\'\'))","element(by.css(\'\'))",' +
@@ -42,6 +46,8 @@ test.addCommandExpectation('\t',
4246
'"element(by.className(\'\'))"],""]');
4347
test.addCommandExpectation('ele\t', '[["element"],"ele"]');
4448
test.addCommandExpectation('br\t', '[["break","","browser"],"br"]');
49+
// Make sure the global 'list' we added shows up.
50+
test.addCommandExpectation('li\t', '[["list"],"li"]');
4551

4652
test.run();
4753

scripts/interactive_tests/interactive_test_util.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ exports.InteractiveTest = function(interactiveServerStartCmd, port) {
141141
};
142142
return verifyAll(0);
143143
}).fin(function() {
144-
client.sendCommand(0x1D); // '^]' This is the term signal.
144+
// '^]' This is the term signal.
145+
client.sendCommand(String.fromCharCode(0x1D));
145146
client.disconnect();
146147
});
147148
}).done();

0 commit comments

Comments
 (0)