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 | const EventEmitter = require('events');
|
20 |
|
21 | class Connection extends EventEmitter {
|
22 | |
23 |
|
24 |
|
25 |
|
26 |
|
27 | constructor(url, transport, delay = 0) {
|
28 | super();
|
29 | this._url = url;
|
30 | this._lastId = 0;
|
31 |
|
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 |
|
39 | this._sessions = new Map();
|
40 | this._closed = false;
|
41 | }
|
42 |
|
43 | |
44 |
|
45 |
|
46 |
|
47 | static fromSession(session) {
|
48 | let connection = session._connection;
|
49 |
|
50 | while (connection instanceof CDPSession)
|
51 | connection = connection._connection;
|
52 | return connection;
|
53 | }
|
54 |
|
55 | |
56 |
|
57 |
|
58 | url() {
|
59 | return this._url;
|
60 | }
|
61 |
|
62 | |
63 |
|
64 |
|
65 |
|
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 |
|
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 |
|
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 |
|
133 |
|
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 |
|
143 | Connection.Events = {
|
144 | Disconnected: Symbol('Connection.Events.Disconnected'),
|
145 | };
|
146 |
|
147 | class CDPSession extends EventEmitter {
|
148 | |
149 |
|
150 |
|
151 |
|
152 |
|
153 | constructor(connection, targetType, sessionId) {
|
154 | super();
|
155 | this._lastId = 0;
|
156 |
|
157 | this._callbacks = new Map();
|
158 |
|
159 | this._connection = connection;
|
160 | this._targetType = targetType;
|
161 | this._sessionId = sessionId;
|
162 |
|
163 | this._sessions = new Map();
|
164 | }
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
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 |
|
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 |
|
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 |
|
235 |
|
236 |
|
237 | _createSession(targetType, sessionId) {
|
238 | const session = new CDPSession(this, targetType, sessionId);
|
239 | this._sessions.set(sessionId, session);
|
240 | return session;
|
241 | }
|
242 | }
|
243 | helper.tracePublicAPI(CDPSession);
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | function 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 |
|
260 |
|
261 |
|
262 |
|
263 | function rewriteError(error, message) {
|
264 | error.message = message;
|
265 | return error;
|
266 | }
|
267 |
|
268 | module.exports = {Connection, CDPSession};
|