UNPKG

11.3 kBJavaScriptView Raw
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 */
16const {helper, assert} = require('./helper');
17const debugProtocol = require('debug')('puppeteer:protocol');
18const debugSession = require('debug')('puppeteer:session');
19
20const EventEmitter = require('events');
21const WebSocket = require('ws');
22const Pipe = require('./Pipe');
23
24class Connection extends EventEmitter {
25 /**
26 * @param {string} url
27 * @param {number=} delay
28 * @return {!Promise<!Connection>}
29 */
30 static /* async */ createForWebSocket(url, delay = 0) {return (fn => {
31 const gen = fn.call(this);
32 return new Promise((resolve, reject) => {
33 function step(key, arg) {
34 let info, value;
35 try {
36 info = gen[key](arg);
37 value = info.value;
38 } catch (error) {
39 reject(error);
40 return;
41 }
42 if (info.done) {
43 resolve(value);
44 } else {
45 return Promise.resolve(value).then(
46 value => {
47 step('next', value);
48 },
49 err => {
50 step('throw', err);
51 });
52 }
53 }
54 return step('next');
55 });
56})(function*(){
57 return new Promise((resolve, reject) => {
58 const ws = new WebSocket(url, { perMessageDeflate: false });
59 ws.on('open', () => resolve(new Connection(url, ws, delay)));
60 ws.on('error', reject);
61 });
62 });}
63
64 /**
65 * @param {!NodeJS.WritableStream} pipeWrite
66 * @param {!NodeJS.ReadableStream} pipeRead
67 * @param {number=} delay
68 * @return {!Connection}
69 */
70 static createForPipe(pipeWrite, pipeRead, delay = 0) {
71 return new Connection('', new Pipe(pipeWrite, pipeRead), delay);
72 }
73
74 /**
75 * @param {string} url
76 * @param {!Puppeteer.ConnectionTransport} transport
77 * @param {number=} delay
78 */
79 constructor(url, transport, delay = 0) {
80 super();
81 this._url = url;
82 this._lastId = 0;
83 /** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
84 this._callbacks = new Map();
85 this._delay = delay;
86
87 this._transport = transport;
88 this._transport.on('message', this._onMessage.bind(this));
89 this._transport.on('close', this._onClose.bind(this));
90 /** @type {!Map<string, !CDPSession>}*/
91 this._sessions = new Map();
92 }
93
94 /**
95 * @return {string}
96 */
97 url() {
98 return this._url;
99 }
100
101 /**
102 * @param {string} method
103 * @param {!Object=} params
104 * @return {!Promise<?Object>}
105 */
106 send(method, params = {}) {
107 const id = ++this._lastId;
108 const message = JSON.stringify({id, method, params});
109 debugProtocol('SEND ► ' + message);
110 this._transport.send(message);
111 return new Promise((resolve, reject) => {
112 this._callbacks.set(id, {resolve, reject, error: new Error(), method});
113 });
114 }
115
116 /**
117 * @param {function()} callback
118 */
119 setClosedCallback(callback) {
120 this._closeCallback = callback;
121 }
122
123 /**
124 * @param {string} message
125 */
126 /* async */ _onMessage(message) {return (fn => {
127 const gen = fn.call(this);
128 return new Promise((resolve, reject) => {
129 function step(key, arg) {
130 let info, value;
131 try {
132 info = gen[key](arg);
133 value = info.value;
134 } catch (error) {
135 reject(error);
136 return;
137 }
138 if (info.done) {
139 resolve(value);
140 } else {
141 return Promise.resolve(value).then(
142 value => {
143 step('next', value);
144 },
145 err => {
146 step('throw', err);
147 });
148 }
149 }
150 return step('next');
151 });
152})(function*(){
153 if (this._delay)
154 (yield new Promise(f => setTimeout(f, this._delay)));
155 debugProtocol('◀ RECV ' + message);
156 const object = JSON.parse(message);
157 if (object.id) {
158 const callback = this._callbacks.get(object.id);
159 // Callbacks could be all rejected if someone has called `.dispose()`.
160 if (callback) {
161 this._callbacks.delete(object.id);
162 if (object.error)
163 callback.reject(createProtocolError(callback.error, callback.method, object));
164 else
165 callback.resolve(object.result);
166 }
167 } else {
168 if (object.method === 'Target.receivedMessageFromTarget') {
169 const session = this._sessions.get(object.params.sessionId);
170 if (session)
171 session._onMessage(object.params.message);
172 } else if (object.method === 'Target.detachedFromTarget') {
173 const session = this._sessions.get(object.params.sessionId);
174 if (session)
175 session._onClosed();
176 this._sessions.delete(object.params.sessionId);
177 } else {
178 this.emit(object.method, object.params);
179 }
180 }
181 });}
182
183 _onClose() {
184 if (this._closeCallback) {
185 this._closeCallback();
186 this._closeCallback = null;
187 }
188 this._transport.removeAllListeners();
189 // If transport throws any error at this point of time, we don't care and should swallow it.
190 this._transport.on('error', () => {});
191 for (const callback of this._callbacks.values())
192 callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
193 this._callbacks.clear();
194 for (const session of this._sessions.values())
195 session._onClosed();
196 this._sessions.clear();
197 }
198
199 dispose() {
200 this._onClose();
201 this._transport.close();
202 }
203
204 /**
205 * @param {Protocol.Target.TargetInfo} targetInfo
206 * @return {!Promise<!CDPSession>}
207 */
208 /* async */ createSession(targetInfo) {return (fn => {
209 const gen = fn.call(this);
210 return new Promise((resolve, reject) => {
211 function step(key, arg) {
212 let info, value;
213 try {
214 info = gen[key](arg);
215 value = info.value;
216 } catch (error) {
217 reject(error);
218 return;
219 }
220 if (info.done) {
221 resolve(value);
222 } else {
223 return Promise.resolve(value).then(
224 value => {
225 step('next', value);
226 },
227 err => {
228 step('throw', err);
229 });
230 }
231 }
232 return step('next');
233 });
234})(function*(){
235 const {sessionId} = (yield this.send('Target.attachToTarget', {targetId: targetInfo.targetId}));
236 const session = new CDPSession(this, targetInfo.type, sessionId);
237 this._sessions.set(sessionId, session);
238 return session;
239 });}
240}
241
242class CDPSession extends EventEmitter {
243 /**
244 * @param {!Connection|!CDPSession} connection
245 * @param {string} targetType
246 * @param {string} sessionId
247 */
248 constructor(connection, targetType, sessionId) {
249 super();
250 this._lastId = 0;
251 /** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
252 this._callbacks = new Map();
253 this._connection = connection;
254 this._targetType = targetType;
255 this._sessionId = sessionId;
256 /** @type {!Map<string, !CDPSession>}*/
257 this._sessions = new Map();
258 }
259
260 /**
261 * @param {string} method
262 * @param {!Object=} params
263 * @return {!Promise<?Object>}
264 */
265 send(method, params = {}) {
266 if (!this._connection)
267 return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
268 const id = ++this._lastId;
269 const message = JSON.stringify({id, method, params});
270 debugSession('SEND ► ' + message);
271 this._connection.send('Target.sendMessageToTarget', {sessionId: this._sessionId, message}).catch(e => {
272 // The response from target might have been already dispatched.
273 if (!this._callbacks.has(id))
274 return;
275 const callback = this._callbacks.get(id);
276 this._callbacks.delete(id);
277 callback.reject(rewriteError(callback.error, e && e.message));
278 });
279 return new Promise((resolve, reject) => {
280 this._callbacks.set(id, {resolve, reject, error: new Error(), method});
281 });
282 }
283
284 /**
285 * @param {string} message
286 */
287 _onMessage(message) {
288 debugSession('◀ RECV ' + message);
289 const object = JSON.parse(message);
290 if (object.id && this._callbacks.has(object.id)) {
291 const callback = this._callbacks.get(object.id);
292 this._callbacks.delete(object.id);
293 if (object.error)
294 callback.reject(createProtocolError(callback.error, callback.method, object));
295 else
296 callback.resolve(object.result);
297 } else {
298 if (object.method === 'Target.receivedMessageFromTarget') {
299 const session = this._sessions.get(object.params.sessionId);
300 if (session)
301 session._onMessage(object.params.message);
302 } else if (object.method === 'Target.detachedFromTarget') {
303 const session = this._sessions.get(object.params.sessionId);
304 if (session) {
305 session._onClosed();
306 this._sessions.delete(object.params.sessionId);
307 }
308 }
309 assert(!object.id);
310 this.emit(object.method, object.params);
311 }
312 }
313
314 /* async */ detach() {return (fn => {
315 const gen = fn.call(this);
316 return new Promise((resolve, reject) => {
317 function step(key, arg) {
318 let info, value;
319 try {
320 info = gen[key](arg);
321 value = info.value;
322 } catch (error) {
323 reject(error);
324 return;
325 }
326 if (info.done) {
327 resolve(value);
328 } else {
329 return Promise.resolve(value).then(
330 value => {
331 step('next', value);
332 },
333 err => {
334 step('throw', err);
335 });
336 }
337 }
338 return step('next');
339 });
340})(function*(){
341 (yield this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId}));
342 });}
343
344 _onClosed() {
345 for (const callback of this._callbacks.values())
346 callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
347 this._callbacks.clear();
348 this._connection = null;
349 }
350
351 /**
352 * @param {string} targetType
353 * @param {string} sessionId
354 */
355 _createSession(targetType, sessionId) {
356 const session = new CDPSession(this, targetType, sessionId);
357 this._sessions.set(sessionId, session);
358 return session;
359 }
360}
361helper.tracePublicAPI(CDPSession);
362
363/**
364 * @param {!Error} error
365 * @param {string} method
366 * @param {{error: {message: string, data: any}}} object
367 * @return {!Error}
368 */
369function createProtocolError(error, method, object) {
370 let message = `Protocol error (${method}): ${object.error.message}`;
371 if ('data' in object.error)
372 message += ` ${object.error.data}`;
373 if (object.error.message)
374 return rewriteError(error, message);
375}
376
377/**
378 * @param {!Error} error
379 * @param {string} message
380 * @return {!Error}
381 */
382function rewriteError(error, message) {
383 error.message = message;
384 return error;
385}
386
387module.exports = {Connection, CDPSession};