1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const {helper, assert} = require('./helper');
|
17 | const debugProtocol = require('debug')('puppeteer:protocol');
|
18 | const debugSession = require('debug')('puppeteer:session');
|
19 |
|
20 | const EventEmitter = require('events');
|
21 | const WebSocket = require('ws');
|
22 | const Pipe = require('./Pipe');
|
23 |
|
24 | class Connection extends EventEmitter {
|
25 | |
26 |
|
27 |
|
28 |
|
29 |
|
30 | static 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 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | static createForPipe(pipeWrite, pipeRead, delay = 0) {
|
71 | return new Connection('', new Pipe(pipeWrite, pipeRead), delay);
|
72 | }
|
73 |
|
74 | |
75 |
|
76 |
|
77 |
|
78 |
|
79 | constructor(url, transport, delay = 0) {
|
80 | super();
|
81 | this._url = url;
|
82 | this._lastId = 0;
|
83 |
|
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 |
|
91 | this._sessions = new Map();
|
92 | }
|
93 |
|
94 | |
95 |
|
96 |
|
97 | url() {
|
98 | return this._url;
|
99 | }
|
100 |
|
101 | |
102 |
|
103 |
|
104 |
|
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 |
|
118 |
|
119 | setClosedCallback(callback) {
|
120 | this._closeCallback = callback;
|
121 | }
|
122 |
|
123 | |
124 |
|
125 |
|
126 | _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 |
|
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 |
|
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 |
|
206 |
|
207 |
|
208 | 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 |
|
242 | class CDPSession extends EventEmitter {
|
243 | |
244 |
|
245 |
|
246 |
|
247 |
|
248 | constructor(connection, targetType, sessionId) {
|
249 | super();
|
250 | this._lastId = 0;
|
251 |
|
252 | this._callbacks = new Map();
|
253 | this._connection = connection;
|
254 | this._targetType = targetType;
|
255 | this._sessionId = sessionId;
|
256 |
|
257 | this._sessions = new Map();
|
258 | }
|
259 |
|
260 | |
261 |
|
262 |
|
263 |
|
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 |
|
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 |
|
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 | 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 |
|
353 |
|
354 |
|
355 | _createSession(targetType, sessionId) {
|
356 | const session = new CDPSession(this, targetType, sessionId);
|
357 | this._sessions.set(sessionId, session);
|
358 | return session;
|
359 | }
|
360 | }
|
361 | helper.tracePublicAPI(CDPSession);
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 | function 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 |
|
379 |
|
380 |
|
381 |
|
382 | function rewriteError(error, message) {
|
383 | error.message = message;
|
384 | return error;
|
385 | }
|
386 |
|
387 | module.exports = {Connection, CDPSession};
|