helper.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /**
  2. * Copyright 2017 Google Inc. All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. const {TimeoutError} = require('./Errors');
  17. const debugError = require('debug')(`puppeteer:error`);
  18. const fs = require('fs');
  19. class Helper {
  20. /**
  21. * @param {Function|string} fun
  22. * @param {!Array<*>} args
  23. * @return {string}
  24. */
  25. static evaluationString(fun, ...args) {
  26. if (Helper.isString(fun)) {
  27. assert(args.length === 0, 'Cannot evaluate a string with arguments');
  28. return /** @type {string} */ (fun);
  29. }
  30. return `(${fun})(${args.map(serializeArgument).join(',')})`;
  31. /**
  32. * @param {*} arg
  33. * @return {string}
  34. */
  35. function serializeArgument(arg) {
  36. if (Object.is(arg, undefined))
  37. return 'undefined';
  38. return JSON.stringify(arg);
  39. }
  40. }
  41. /**
  42. * @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
  43. * @return {string}
  44. */
  45. static getExceptionMessage(exceptionDetails) {
  46. if (exceptionDetails.exception)
  47. return exceptionDetails.exception.description || exceptionDetails.exception.value;
  48. let message = exceptionDetails.text;
  49. if (exceptionDetails.stackTrace) {
  50. for (const callframe of exceptionDetails.stackTrace.callFrames) {
  51. const location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
  52. const functionName = callframe.functionName || '<anonymous>';
  53. message += `\n at ${functionName} (${location})`;
  54. }
  55. }
  56. return message;
  57. }
  58. /**
  59. * @param {!Protocol.Runtime.RemoteObject} remoteObject
  60. * @return {*}
  61. */
  62. static valueFromRemoteObject(remoteObject) {
  63. assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
  64. if (remoteObject.unserializableValue) {
  65. if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
  66. return BigInt(remoteObject.unserializableValue.replace('n', ''));
  67. switch (remoteObject.unserializableValue) {
  68. case '-0':
  69. return -0;
  70. case 'NaN':
  71. return NaN;
  72. case 'Infinity':
  73. return Infinity;
  74. case '-Infinity':
  75. return -Infinity;
  76. default:
  77. throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
  78. }
  79. }
  80. return remoteObject.value;
  81. }
  82. /**
  83. * @param {!Puppeteer.CDPSession} client
  84. * @param {!Protocol.Runtime.RemoteObject} remoteObject
  85. */
  86. static async releaseObject(client, remoteObject) {
  87. if (!remoteObject.objectId)
  88. return;
  89. await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
  90. // Exceptions might happen in case of a page been navigated or closed.
  91. // Swallow these since they are harmless and we don't leak anything in this case.
  92. debugError(error);
  93. });
  94. }
  95. /**
  96. * @param {!Object} classType
  97. */
  98. static installAsyncStackHooks(classType) {
  99. for (const methodName of Reflect.ownKeys(classType.prototype)) {
  100. const method = Reflect.get(classType.prototype, methodName);
  101. if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction')
  102. continue;
  103. Reflect.set(classType.prototype, methodName, function(...args) {
  104. const syncStack = {};
  105. Error.captureStackTrace(syncStack);
  106. return method.call(this, ...args).catch(e => {
  107. const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
  108. const clientStack = stack.substring(stack.indexOf('\n'));
  109. if (e instanceof Error && e.stack && !e.stack.includes(clientStack))
  110. e.stack += '\n -- ASYNC --\n' + stack;
  111. throw e;
  112. });
  113. });
  114. }
  115. }
  116. /**
  117. * @param {!NodeJS.EventEmitter} emitter
  118. * @param {(string|symbol)} eventName
  119. * @param {function(?):void} handler
  120. * @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}}
  121. */
  122. static addEventListener(emitter, eventName, handler) {
  123. emitter.on(eventName, handler);
  124. return { emitter, eventName, handler };
  125. }
  126. /**
  127. * @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?):void}>} listeners
  128. */
  129. static removeEventListeners(listeners) {
  130. for (const listener of listeners)
  131. listener.emitter.removeListener(listener.eventName, listener.handler);
  132. listeners.length = 0;
  133. }
  134. /**
  135. * @param {!Object} obj
  136. * @return {boolean}
  137. */
  138. static isString(obj) {
  139. return typeof obj === 'string' || obj instanceof String;
  140. }
  141. /**
  142. * @param {!Object} obj
  143. * @return {boolean}
  144. */
  145. static isNumber(obj) {
  146. return typeof obj === 'number' || obj instanceof Number;
  147. }
  148. /**
  149. * @param {function} nodeFunction
  150. * @return {function}
  151. */
  152. static promisify(nodeFunction) {
  153. function promisified(...args) {
  154. return new Promise((resolve, reject) => {
  155. function callback(err, ...result) {
  156. if (err)
  157. return reject(err);
  158. if (result.length === 1)
  159. return resolve(result[0]);
  160. return resolve(result);
  161. }
  162. nodeFunction.call(null, ...args, callback);
  163. });
  164. }
  165. return promisified;
  166. }
  167. /**
  168. * @param {!NodeJS.EventEmitter} emitter
  169. * @param {(string|symbol)} eventName
  170. * @param {function} predicate
  171. * @param {number} timeout
  172. * @param {!Promise<!Error>} abortPromise
  173. * @return {!Promise}
  174. */
  175. static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
  176. let eventTimeout, resolveCallback, rejectCallback;
  177. const promise = new Promise((resolve, reject) => {
  178. resolveCallback = resolve;
  179. rejectCallback = reject;
  180. });
  181. const listener = Helper.addEventListener(emitter, eventName, event => {
  182. if (!predicate(event))
  183. return;
  184. resolveCallback(event);
  185. });
  186. if (timeout) {
  187. eventTimeout = setTimeout(() => {
  188. rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
  189. }, timeout);
  190. }
  191. function cleanup() {
  192. Helper.removeEventListeners([listener]);
  193. clearTimeout(eventTimeout);
  194. }
  195. const result = await Promise.race([promise, abortPromise]).then(r => {
  196. cleanup();
  197. return r;
  198. }, e => {
  199. cleanup();
  200. throw e;
  201. });
  202. if (result instanceof Error)
  203. throw result;
  204. return result;
  205. }
  206. /**
  207. * @template T
  208. * @param {!Promise<T>} promise
  209. * @param {string} taskName
  210. * @param {number} timeout
  211. * @return {!Promise<T>}
  212. */
  213. static async waitWithTimeout(promise, taskName, timeout) {
  214. let reject;
  215. const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
  216. const timeoutPromise = new Promise((resolve, x) => reject = x);
  217. let timeoutTimer = null;
  218. if (timeout)
  219. timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
  220. try {
  221. return await Promise.race([promise, timeoutPromise]);
  222. } finally {
  223. if (timeoutTimer)
  224. clearTimeout(timeoutTimer);
  225. }
  226. }
  227. /**
  228. * @param {!Puppeteer.CDPSession} client
  229. * @param {string} handle
  230. * @param {?string} path
  231. * @return {!Promise<!Buffer>}
  232. */
  233. static async readProtocolStream(client, handle, path) {
  234. let eof = false;
  235. let file;
  236. if (path)
  237. file = await openAsync(path, 'w');
  238. const bufs = [];
  239. while (!eof) {
  240. const response = await client.send('IO.read', {handle});
  241. eof = response.eof;
  242. const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
  243. bufs.push(buf);
  244. if (path)
  245. await writeAsync(file, buf);
  246. }
  247. if (path)
  248. await closeAsync(file);
  249. await client.send('IO.close', {handle});
  250. let resultBuffer = null;
  251. try {
  252. resultBuffer = Buffer.concat(bufs);
  253. } finally {
  254. return resultBuffer;
  255. }
  256. }
  257. }
  258. const openAsync = Helper.promisify(fs.open);
  259. const writeAsync = Helper.promisify(fs.write);
  260. const closeAsync = Helper.promisify(fs.close);
  261. /**
  262. * @param {*} value
  263. * @param {string=} message
  264. */
  265. function assert(value, message) {
  266. if (!value)
  267. throw new Error(message);
  268. }
  269. module.exports = {
  270. helper: Helper,
  271. assert,
  272. debugError
  273. };