Skip to content

Commit 96003fc

Browse files
author
cadeli
committed
devtool http 兜底
1 parent e52f7d9 commit 96003fc

3 files changed

Lines changed: 112 additions & 24 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mprdev",
3-
"version": "0.3.0",
3+
"version": "0.4.0-beta.0",
44
"description": "A Web Remote Debug Toolkit",
55
"main": "dist/sdk/index.js",
66
"bin": {

src/server/app/websocket/channel.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class Channel {
5656
}
5757

5858
handleTargetMessage(buffer: Buffer) {
59+
console.log('???????', this.internalTargets, this.internalDevtools);
5960
this.sendMessageToOpponent(this.internalDevtools, buffer);
6061
}
6162

@@ -67,9 +68,13 @@ export class Channel {
6768
target.destroy();
6869
this.internalTargets = this.internalTargets.filter(item => item !== target);
6970
channelService.fireChannelChange(ChannelEventName.TARGET_REMOVE, this);
70-
// 如果已经没有 target 了,要把对应的所有 devtool 关掉
71+
// 如果已经没有 target 了,关掉所有 devtool;若也没有 devtool,直接触发 CHANNEL_EMPTY
7172
if (this.internalTargets.length === 0) {
72-
this.internalDevtools.forEach(devtool => devtool.ws.close());
73+
if (this.internalDevtools.length === 0) {
74+
channelService.fireChannelChange(ChannelEventName.CHANNEL_EMPTY, this);
75+
} else {
76+
this.internalDevtools.forEach(devtool => devtool.ws.close());
77+
}
7378
}
7479
}
7580

src/server/app/websocket/httpsocket-server.ts

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import * as Router from 'koa-router';
55
/**
66
* websocket不可用的环境,留一个http轮询的接入接口
77
*/
8-
const connections: Record<string, { socket: WebSocket, stream: PassThrough, expiry: number, messages: Data[] }> = {};
8+
type ConnectionEntry = { socket: WebSocket, stream: PassThrough, expiry: number, messages: Data[] };
9+
const connections: Record<string, ConnectionEntry> = {};
910
const cleanerInterval = 10000; // 清理间隔10s
1011
let cleanerIntervalId: NodeJS.Timer = null;
1112

@@ -28,7 +29,9 @@ const initCleaner = () => {
2829
}
2930
};
3031

31-
export function listenHttpSocket(router: Router) {
32+
// ─── SDK 端降级接口(SSE + POST 轮询)─────────────────────────────────────────
33+
34+
function listenTargetHttpSocket(router: Router) {
3235
router.get('/target/:id', async ctx => {
3336
// 使用SSE来推送服务端数据
3437
ctx.request.socket.setTimeout(0);
@@ -39,41 +42,42 @@ export function listenHttpSocket(router: Router) {
3942
'Cache-Control': 'no-cache',
4043
'Connection': 'keep-alive',
4144
});
42-
const id = ctx.params.id;
43-
if (connections[id]) {
44-
ctx.body = connections[id].stream = new PassThrough();
45-
const { stream, messages } = connections[id];
46-
connections[id].expiry = Date.now() + cleanerInterval;
47-
connections[id].messages = [];
45+
const key = ctx.params.id;
46+
if (connections[key]) {
47+
ctx.body = connections[key].stream = new PassThrough();
48+
const { stream, messages } = connections[key];
49+
connections[key].expiry = Date.now() + cleanerInterval;
50+
connections[key].messages = [];
4851
stream.write(`data: ${JSON.stringify(messages)}\n\n`);
4952
stream.addListener('close', () => {
50-
// 清空SSE流,以便定期清理
51-
if (connections[id]) {
52-
connections[id].expiry = Date.now() + cleanerInterval;
53-
connections[id].stream = null;
53+
if (connections[key]) {
54+
connections[key].stream = null;
55+
// SSE 断开说明页面已关闭,立即关闭内部 WS,触发 channel 清理
56+
connections[key].socket.close();
57+
delete connections[key];
5458
}
5559
});
5660
}
5761
});
62+
5863
router.post('/target/:id', async ctx => {
59-
const id = ctx.params.id;
64+
const { id } = ctx.params;
6065
const data = ctx.request.body as string[];
6166
// 初始化连接,记得干掉之前的连接
6267
if (data[0] === 'connect') {
6368
if (connections[id]) {
6469
connections[id].socket.terminate();
6570
await new Promise(resolve => {
66-
// 延迟一下,防止收到上个页面的调试消息
67-
connections[id].socket.onclose = () => setTimeout(resolve, 1000);
71+
connections[id].socket.once('close', () => setTimeout(resolve, 1000));
6872
});
6973
}
7074
connections[id] = {
71-
socket: new WebSocket(`ws://0.0.0.0:${ctx.socket.localPort}${ctx.url}`),
75+
socket: new WebSocket(`ws://0.0.0.0:${ctx.socket.localPort}/target/${id}`),
7276
stream: null,
7377
expiry: 0,
7478
messages: [],
7579
};
76-
connections[id].socket.onmessage = ({ data }) => {
80+
connections[id].socket.on('message', (data) => {
7781
if (connections[id]) {
7882
connections[id].messages.push(data);
7983
if (connections[id].stream) { // 如果支持SSE,直接推送
@@ -83,16 +87,95 @@ export function listenHttpSocket(router: Router) {
8387
stream.write(`data: ${JSON.stringify(messages)}\n\n`);
8488
}
8589
}
86-
};
90+
});
8791
initCleaner();
88-
await new Promise(resolve => connections[id].socket.onopen = resolve);
92+
await new Promise(resolve => connections[id].socket.once('open', resolve));
8993
}
90-
// 处理消息
94+
// 处理消息(connect 为内部指令,不转发)
9195
const { socket, messages } = connections[id];
9296
connections[id].expiry = Date.now() + cleanerInterval * 3;
9397
connections[id].messages = [];
94-
data.forEach((message) => socket.send(message));
98+
data.filter(msg => msg !== 'connect').forEach((message) => socket.send(message));
9599
// 返回缓存的消息
96100
ctx.body = JSON.stringify(messages);
97101
});
102+
}
103+
104+
// ─── 控制端降级接口(请求-响应模式)──────────────────────────────────────────
105+
106+
type PendingRequest = { resolve: (value: any) => void, timer: NodeJS.Timeout };
107+
type DevtoolEntry = { socket: WebSocket, cmdId: number, pending: Map<number, PendingRequest> };
108+
const devtoolConnections: Record<string, DevtoolEntry> = {};
109+
110+
function listenDevtoolHttpSocket(router: Router) {
111+
// POST /devtool/:id — 建立连接或发送 CDP 命令,等待响应后返回
112+
router.post('/devtool/:id', async ctx => {
113+
const { id } = ctx.params;
114+
const { method, params, timeout = 30000 } = ctx.request.body as {
115+
method: string, params?: Record<string, any>, timeout?: number,
116+
};
117+
118+
// 初始化到 devtool WS 的连接
119+
if (!devtoolConnections[id]) {
120+
const socket = new WebSocket(`ws://0.0.0.0:${ctx.socket.localPort}/devtool?targetId=${id}`);
121+
const entry: DevtoolEntry = { socket, cmdId: 0, pending: new Map() };
122+
devtoolConnections[id] = entry;
123+
124+
socket.on('message', (data) => {
125+
try {
126+
const msg = JSON.parse(data.toString());
127+
const req = entry.pending.get(msg.id);
128+
if (req) {
129+
clearTimeout(req.timer);
130+
entry.pending.delete(msg.id);
131+
req.resolve(msg);
132+
}
133+
} catch { /* 忽略非 JSON 消息 */ }
134+
});
135+
136+
socket.on('close', () => {
137+
// 连接断开时,清理所有 pending 请求
138+
entry.pending.forEach(({ resolve, timer }) => {
139+
clearTimeout(timer);
140+
resolve({ error: 'connection closed' });
141+
});
142+
entry.pending.clear();
143+
delete devtoolConnections[id];
144+
});
145+
146+
await new Promise<void>((resolve, reject) => {
147+
socket.once('open', () => resolve());
148+
socket.once('error', (e) => reject(e));
149+
});
150+
}
151+
152+
if (!method) {
153+
ctx.status = 400;
154+
ctx.body = { error: 'Missing method' };
155+
return;
156+
}
157+
158+
const entry = devtoolConnections[id];
159+
const cmdId = ++entry.cmdId;
160+
161+
try {
162+
const result = await new Promise<any>((resolve, reject) => {
163+
const timer = setTimeout(() => {
164+
entry.pending.delete(cmdId);
165+
reject(new Error('timeout'));
166+
}, timeout);
167+
entry.pending.set(cmdId, { resolve, timer });
168+
entry.socket.send(JSON.stringify({ id: cmdId, method, params: params || {} }));
169+
});
170+
ctx.body = { success: true, ...result };
171+
} catch (err) {
172+
ctx.status = 504;
173+
ctx.body = { success: false, error: err.message };
174+
}
175+
});
176+
}
177+
178+
export function listenHttpSocket(router: Router) {
179+
listenTargetHttpSocket(router);
180+
listenDevtoolHttpSocket(router);
98181
}

0 commit comments

Comments
 (0)