debugger.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const net = require("net");
  4. const selenium_webdriver_1 = require("selenium-webdriver");
  5. const util = require("util");
  6. const logger_1 = require("./logger");
  7. let breakpointHook = require('./breakpointhook.js');
  8. let logger = new logger_1.Logger('protractor');
  9. class DebugHelper {
  10. constructor(browserUnderDebug_) {
  11. this.browserUnderDebug_ = browserUnderDebug_;
  12. }
  13. initBlocking(debuggerClientPath, onStartFn, opt_debugPort) {
  14. this.init_(debuggerClientPath, true, onStartFn, opt_debugPort);
  15. }
  16. init(debuggerClientPath, onStartFn, opt_debugPort) {
  17. this.init_(debuggerClientPath, false, onStartFn, opt_debugPort);
  18. }
  19. /**
  20. * 1) Set up helper functions for debugger clients to call on (e.g.
  21. * execute code, get autocompletion).
  22. * 2) Enter process into debugger mode. (i.e. process._debugProcess).
  23. * 3) Invoke the debugger client specified by debuggerClientPath.
  24. *
  25. * @param {string} debuggerClientPath Absolute path of debugger client to use.
  26. * @param {boolean} blockUntilExit Whether to block the flow until process exit or resume
  27. * immediately.
  28. * @param {Function} onStartFn Function to call when the debugger starts. The
  29. * function takes a single parameter, which represents whether this is the
  30. * first time that the debugger is called.
  31. * @param {number=} opt_debugPort Optional port to use for the debugging
  32. * process.
  33. *
  34. * @return {Promise} If blockUntilExit, a promise resolved when the debugger process
  35. * exits. Otherwise, resolved when the debugger process is ready to begin.
  36. */
  37. init_(debuggerClientPath, blockUntilExit, onStartFn, opt_debugPort) {
  38. const vm_ = require('vm');
  39. let flow = selenium_webdriver_1.promise.controlFlow();
  40. let context = { require: require };
  41. global.list = (locator) => {
  42. return global.protractor.browser.findElements(locator).then((arr) => {
  43. let found = [];
  44. for (let i = 0; i < arr.length; ++i) {
  45. arr[i].getText().then((text) => {
  46. found.push(text);
  47. });
  48. }
  49. return found;
  50. });
  51. };
  52. for (let key in global) {
  53. context[key] = global[key];
  54. }
  55. let sandbox = vm_.createContext(context);
  56. let debuggingDone = selenium_webdriver_1.promise.defer();
  57. // We run one flow.execute block for the debugging session. All
  58. // subcommands should be scheduled under this task.
  59. let executePromise = flow.execute(() => {
  60. process['debugPort'] = opt_debugPort || process['debugPort'];
  61. this.validatePortAvailability_(process['debugPort']).then((firstTime) => {
  62. onStartFn(firstTime);
  63. let args = [process.pid, process['debugPort']];
  64. if (this.browserUnderDebug_.debuggerServerPort) {
  65. args.push(this.browserUnderDebug_.debuggerServerPort);
  66. }
  67. let nodedebug = require('child_process').fork(debuggerClientPath, args);
  68. process.on('exit', function () {
  69. nodedebug.kill('SIGTERM');
  70. });
  71. nodedebug
  72. .on('message', (m) => {
  73. if (m === 'ready') {
  74. breakpointHook();
  75. if (!blockUntilExit) {
  76. debuggingDone.fulfill();
  77. }
  78. }
  79. })
  80. .on('exit', () => {
  81. // Clear this so that we know it's ok to attach a debugger
  82. // again.
  83. this.dbgCodeExecutor = null;
  84. debuggingDone.fulfill();
  85. });
  86. });
  87. return debuggingDone.promise;
  88. }, 'debugging tasks');
  89. // Helper used only by debuggers at './debugger/modes/*.js' to insert code
  90. // into the control flow, via debugger 'evaluate' protocol.
  91. // In order to achieve this, we maintain a task at the top of the control
  92. // flow, so that we can insert frames into it.
  93. // To be able to simulate callback/asynchronous code, we poll this object
  94. // whenever `breakpointHook` is called.
  95. this.dbgCodeExecutor = {
  96. execPromise_: undefined,
  97. execPromiseResult_: undefined,
  98. execPromiseError_: undefined,
  99. // A dummy repl server to make use of its completion function.
  100. replServer_: require('repl').start({
  101. input: { on: function () { }, resume: function () { } },
  102. // dummy readable stream
  103. output: { write: function () { } },
  104. useGlobal: true
  105. }),
  106. // Execute a function, which could yield a value or a promise,
  107. // and allow its result to be accessed synchronously
  108. execute_: function (execFn_) {
  109. this.execPromiseResult_ = this.execPromiseError_ = undefined;
  110. this.execPromise_ = execFn_();
  111. // Note: This needs to be added after setting execPromise to execFn,
  112. // or else we cause this.execPromise_ to get stuck in pending mode
  113. // at our next breakpoint.
  114. this.execPromise_.then((result) => {
  115. this.execPromiseResult_ = result;
  116. breakpointHook();
  117. }, (err) => {
  118. this.execPromiseError_ = err;
  119. breakpointHook();
  120. });
  121. },
  122. // Execute a piece of code.
  123. // Result is a string representation of the evaluation.
  124. execute: function (code) {
  125. let execFn_ = () => {
  126. // Run code through vm so that we can maintain a local scope which is
  127. // isolated from the rest of the execution.
  128. let res;
  129. try {
  130. res = vm_.runInContext(code, sandbox);
  131. }
  132. catch (e) {
  133. res = selenium_webdriver_1.promise.when('Error while evaluating command: ' + e);
  134. }
  135. if (!selenium_webdriver_1.promise.isPromise(res)) {
  136. res = selenium_webdriver_1.promise.when(res);
  137. }
  138. return res.then((res) => {
  139. if (res === undefined) {
  140. return undefined;
  141. }
  142. else {
  143. // The '' forces res to be expanded into a string instead of just
  144. // '[Object]'. Then we remove the extra space caused by the ''
  145. // using substring.
  146. return util.format.apply(this, ['', res]).substring(1);
  147. }
  148. });
  149. };
  150. this.execute_(execFn_);
  151. },
  152. // Autocomplete for a line.
  153. // Result is a JSON representation of the autocomplete response.
  154. complete: function (line) {
  155. let execFn_ = () => {
  156. let deferred = selenium_webdriver_1.promise.defer();
  157. this.replServer_.complete(line, (err, res) => {
  158. if (err) {
  159. deferred.reject(err);
  160. }
  161. else {
  162. deferred.fulfill(JSON.stringify(res));
  163. }
  164. });
  165. return deferred.promise;
  166. };
  167. this.execute_(execFn_);
  168. },
  169. // Code finished executing.
  170. resultReady: function () {
  171. return !(this.execPromise_.state_ === 'pending');
  172. },
  173. // Get asynchronous results synchronously.
  174. // This will throw if result is not ready.
  175. getResult: function () {
  176. if (!this.resultReady()) {
  177. throw new Error('Result not ready');
  178. }
  179. if (this.execPromiseError_) {
  180. throw this.execPromiseError_;
  181. }
  182. return this.execPromiseResult_;
  183. }
  184. };
  185. return executePromise;
  186. }
  187. /**
  188. * Validates that the port is free to use. This will only validate the first
  189. * time it is called. The reason is that on subsequent calls, the port will
  190. * already be bound to the debugger, so it will not be available, but that is
  191. * okay.
  192. *
  193. * @returns {Promise<boolean>} A promise that becomes ready when the
  194. * validation
  195. * is done. The promise will resolve to a boolean which represents whether
  196. * this is the first time that the debugger is called.
  197. */
  198. validatePortAvailability_(port) {
  199. if (this.debuggerValidated_) {
  200. return selenium_webdriver_1.promise.when(false);
  201. }
  202. let doneDeferred = selenium_webdriver_1.promise.defer();
  203. // Resolve doneDeferred if port is available.
  204. let tester = net.connect({ port: port }, () => {
  205. doneDeferred.reject('Port ' + port + ' is already in use. Please specify ' +
  206. 'another port to debug.');
  207. });
  208. tester.once('error', (err) => {
  209. if (err.code === 'ECONNREFUSED') {
  210. tester
  211. .once('close', () => {
  212. doneDeferred.fulfill(true);
  213. })
  214. .end();
  215. }
  216. else {
  217. doneDeferred.reject('Unexpected failure testing for port ' + port + ': ' + JSON.stringify(err));
  218. }
  219. });
  220. return doneDeferred.promise.then((firstTime) => {
  221. this.debuggerValidated_ = true;
  222. return firstTime;
  223. }, (err) => {
  224. console.error(err);
  225. return process.exit(1);
  226. });
  227. }
  228. isAttached() {
  229. return !!this.dbgCodeExecutor;
  230. }
  231. }
  232. exports.DebugHelper = DebugHelper;
  233. //# sourceMappingURL=debugger.js.map