UNPKG

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