Connection.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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 {assert} = require('./helper');
  17. const {Events} = require('./Events');
  18. const debugProtocol = require('debug')('puppeteer:protocol');
  19. const EventEmitter = require('events');
  20. class Connection extends EventEmitter {
  21. /**
  22. * @param {string} url
  23. * @param {!Puppeteer.ConnectionTransport} transport
  24. * @param {number=} delay
  25. */
  26. constructor(url, transport, delay = 0) {
  27. super();
  28. this._url = url;
  29. this._lastId = 0;
  30. /** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
  31. this._callbacks = new Map();
  32. this._delay = delay;
  33. this._transport = transport;
  34. this._transport.onmessage = this._onMessage.bind(this);
  35. this._transport.onclose = this._onClose.bind(this);
  36. /** @type {!Map<string, !CDPSession>}*/
  37. this._sessions = new Map();
  38. this._closed = false;
  39. }
  40. /**
  41. * @param {!CDPSession} session
  42. * @return {!Connection}
  43. */
  44. static fromSession(session) {
  45. return session._connection;
  46. }
  47. /**
  48. * @param {string} sessionId
  49. * @return {?CDPSession}
  50. */
  51. session(sessionId) {
  52. return this._sessions.get(sessionId) || null;
  53. }
  54. /**
  55. * @return {string}
  56. */
  57. url() {
  58. return this._url;
  59. }
  60. /**
  61. * @param {string} method
  62. * @param {!Object=} params
  63. * @return {!Promise<?Object>}
  64. */
  65. send(method, params = {}) {
  66. const id = this._rawSend({method, params});
  67. return new Promise((resolve, reject) => {
  68. this._callbacks.set(id, {resolve, reject, error: new Error(), method});
  69. });
  70. }
  71. /**
  72. * @param {*} message
  73. * @return {number}
  74. */
  75. _rawSend(message) {
  76. const id = ++this._lastId;
  77. message = JSON.stringify(Object.assign({}, message, {id}));
  78. debugProtocol('SEND ► ' + message);
  79. this._transport.send(message);
  80. return id;
  81. }
  82. /**
  83. * @param {string} message
  84. */
  85. async _onMessage(message) {
  86. if (this._delay)
  87. await new Promise(f => setTimeout(f, this._delay));
  88. debugProtocol('◀ RECV ' + message);
  89. const object = JSON.parse(message);
  90. if (object.method === 'Target.attachedToTarget') {
  91. const sessionId = object.params.sessionId;
  92. const session = new CDPSession(this, object.params.targetInfo.type, sessionId);
  93. this._sessions.set(sessionId, session);
  94. } else if (object.method === 'Target.detachedFromTarget') {
  95. const session = this._sessions.get(object.params.sessionId);
  96. if (session) {
  97. session._onClosed();
  98. this._sessions.delete(object.params.sessionId);
  99. }
  100. }
  101. if (object.sessionId) {
  102. const session = this._sessions.get(object.sessionId);
  103. if (session)
  104. session._onMessage(object);
  105. } else if (object.id) {
  106. const callback = this._callbacks.get(object.id);
  107. // Callbacks could be all rejected if someone has called `.dispose()`.
  108. if (callback) {
  109. this._callbacks.delete(object.id);
  110. if (object.error)
  111. callback.reject(createProtocolError(callback.error, callback.method, object));
  112. else
  113. callback.resolve(object.result);
  114. }
  115. } else {
  116. this.emit(object.method, object.params);
  117. }
  118. }
  119. _onClose() {
  120. if (this._closed)
  121. return;
  122. this._closed = true;
  123. this._transport.onmessage = null;
  124. this._transport.onclose = null;
  125. for (const callback of this._callbacks.values())
  126. callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
  127. this._callbacks.clear();
  128. for (const session of this._sessions.values())
  129. session._onClosed();
  130. this._sessions.clear();
  131. this.emit(Events.Connection.Disconnected);
  132. }
  133. dispose() {
  134. this._onClose();
  135. this._transport.close();
  136. }
  137. /**
  138. * @param {Protocol.Target.TargetInfo} targetInfo
  139. * @return {!Promise<!CDPSession>}
  140. */
  141. async createSession(targetInfo) {
  142. const {sessionId} = await this.send('Target.attachToTarget', {targetId: targetInfo.targetId, flatten: true});
  143. return this._sessions.get(sessionId);
  144. }
  145. }
  146. class CDPSession extends EventEmitter {
  147. /**
  148. * @param {!Connection} connection
  149. * @param {string} targetType
  150. * @param {string} sessionId
  151. */
  152. constructor(connection, targetType, sessionId) {
  153. super();
  154. /** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
  155. this._callbacks = new Map();
  156. this._connection = connection;
  157. this._targetType = targetType;
  158. this._sessionId = sessionId;
  159. }
  160. /**
  161. * @param {string} method
  162. * @param {!Object=} params
  163. * @return {!Promise<?Object>}
  164. */
  165. send(method, params = {}) {
  166. if (!this._connection)
  167. return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
  168. const id = this._connection._rawSend({sessionId: this._sessionId, method, params});
  169. return new Promise((resolve, reject) => {
  170. this._callbacks.set(id, {resolve, reject, error: new Error(), method});
  171. });
  172. }
  173. /**
  174. * @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
  175. */
  176. _onMessage(object) {
  177. if (object.id && this._callbacks.has(object.id)) {
  178. const callback = this._callbacks.get(object.id);
  179. this._callbacks.delete(object.id);
  180. if (object.error)
  181. callback.reject(createProtocolError(callback.error, callback.method, object));
  182. else
  183. callback.resolve(object.result);
  184. } else {
  185. assert(!object.id);
  186. this.emit(object.method, object.params);
  187. }
  188. }
  189. async detach() {
  190. if (!this._connection)
  191. throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`);
  192. await this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId});
  193. }
  194. _onClosed() {
  195. for (const callback of this._callbacks.values())
  196. callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
  197. this._callbacks.clear();
  198. this._connection = null;
  199. this.emit(Events.CDPSession.Disconnected);
  200. }
  201. }
  202. /**
  203. * @param {!Error} error
  204. * @param {string} method
  205. * @param {{error: {message: string, data: any}}} object
  206. * @return {!Error}
  207. */
  208. function createProtocolError(error, method, object) {
  209. let message = `Protocol error (${method}): ${object.error.message}`;
  210. if ('data' in object.error)
  211. message += ` ${object.error.data}`;
  212. return rewriteError(error, message);
  213. }
  214. /**
  215. * @param {!Error} error
  216. * @param {string} message
  217. * @return {!Error}
  218. */
  219. function rewriteError(error, message) {
  220. error.message = message;
  221. return error;
  222. }
  223. module.exports = {Connection, CDPSession};