UNPKG

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