1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const {helper} = 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 |
|
23 | class Connection extends EventEmitter {
|
24 | |
25 |
|
26 |
|
27 |
|
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 |
|
39 |
|
40 |
|
41 |
|
42 | constructor(url, ws, delay = 0) {
|
43 | super();
|
44 | this._url = url;
|
45 | this._lastId = 0;
|
46 |
|
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 |
|
54 | this._sessions = new Map();
|
55 | }
|
56 |
|
57 | |
58 |
|
59 |
|
60 | url() {
|
61 | return this._url;
|
62 | }
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
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 |
|
81 |
|
82 | setClosedCallback(callback) {
|
83 | this._closeCallback = callback;
|
84 | }
|
85 |
|
86 | |
87 |
|
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 |
|
139 |
|
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 |
|
149 | class CDPSession extends EventEmitter {
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 | constructor(connection, targetId, sessionId) {
|
156 | super();
|
157 | this._lastId = 0;
|
158 |
|
159 | this._callbacks = new Map();
|
160 | this._connection = connection;
|
161 | this._targetId = targetId;
|
162 | this._sessionId = sessionId;
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 |
|
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 |
|
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 |
|
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 | }
|
219 | helper.tracePublicAPI(CDPSession);
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | function rewriteError(error, message) {
|
227 | error.message = message;
|
228 | return error;
|
229 | }
|
230 |
|
231 | module.exports = {Connection, CDPSession};
|