feat: 增强进程监控的自动重连功能,支持在意外断开后自动尝试重连
This commit is contained in:
1 parent
b8ec55590c
commit
01ebf2f942
3 files changed
+65
-6
No files matched your search
@@ -430,6 +430,7 @@ processManager = new ProcessManager({
|
||||
elements: collectProcessManagerElements(),
|
||||
getLanguage: () => currentLanguage,
|
||||
onError: (message) => event(message, 'process', true),
|
||||
onReconnect: (zh, en) => event(bilingual(zh, en), 'process'),
|
||||
});
|
||||
|
||||
function terminalTheme(): Record<string, string> {
|
||||
|
||||
@@ -52,9 +52,14 @@ interface ProcessManagerOptions {
|
||||
elements: ProcessManagerElements;
|
||||
getLanguage: () => 'zh-CN' | 'en';
|
||||
onError: (message: string) => void;
|
||||
onReconnect?: (zh: string, en: string) => void;
|
||||
}
|
||||
|
||||
const MAX_PROCESSES = 512;
|
||||
// Auto-reconnect backoff for the process monitor after an unexpected drop.
|
||||
const RECONNECT_BASE_MS = 1_000;
|
||||
const RECONNECT_MAX_MS = 15_000;
|
||||
const RECONNECT_MAX_ATTEMPTS = 10;
|
||||
|
||||
function finitePercent(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 10_000;
|
||||
@@ -135,17 +140,25 @@ export class ProcessManager {
|
||||
private readonly elements: ProcessManagerElements;
|
||||
private readonly getLanguage: () => 'zh-CN' | 'en';
|
||||
private readonly onError: (message: string) => void;
|
||||
private readonly onReconnect: ((zh: string, en: string) => void) | undefined;
|
||||
private socket: WebSocket | null = null;
|
||||
private generation = 0;
|
||||
private snapshot: ProcessSnapshot | null = null;
|
||||
private sortKey: ProcessSortKey = 'cpuPercent';
|
||||
private sortDirection: SortDirection = 'descending';
|
||||
private readonly progressAnimations = new Map<HTMLProgressElement, number>();
|
||||
// Whether the process monitor should stay connected for the current SSH session. Set false
|
||||
// when the user tears the session down (reset) so an unexpected drop is not mistaken for one.
|
||||
private wantConnection = false;
|
||||
private url: string | null = null;
|
||||
private reconnectTimer: number | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
|
||||
constructor(options: ProcessManagerOptions) {
|
||||
this.elements = options.elements;
|
||||
this.getLanguage = options.getLanguage;
|
||||
this.onError = options.onError;
|
||||
this.onReconnect = options.onReconnect;
|
||||
this.elements.panel.addEventListener('click', (event) => {
|
||||
const button = (event.target as HTMLElement).closest<HTMLButtonElement>('[data-process-sort]');
|
||||
if (!button) return;
|
||||
@@ -155,6 +168,8 @@ export class ProcessManager {
|
||||
}
|
||||
|
||||
attach(url: string): void {
|
||||
this.wantConnection = true;
|
||||
this.url = url;
|
||||
this.resetSocket();
|
||||
const target = new URL(url, window.location.href);
|
||||
if (target.origin !== window.location.origin || target.pathname !== '/api/processes') {
|
||||
@@ -167,6 +182,7 @@ export class ProcessManager {
|
||||
this.setStatus('正在启动进程监控…', 'Starting process monitor…');
|
||||
socket.addEventListener('open', () => {
|
||||
if (!this.isCurrent(socket, generation)) return;
|
||||
this.reconnectAttempts = 0;
|
||||
socket.send(JSON.stringify({ type: 'process_start' }));
|
||||
});
|
||||
socket.addEventListener('message', (event) => {
|
||||
@@ -181,12 +197,19 @@ export class ProcessManager {
|
||||
if (!this.isCurrent(socket, generation)) return;
|
||||
this.socket = null;
|
||||
if (event.code !== 1000 && event.code !== 1005) {
|
||||
this.showError('进程监控已意外停止。', 'Process monitor stopped unexpectedly.');
|
||||
// Unexpected drop. Auto-reconnect unless the user intentionally tore the session down.
|
||||
if (this.wantConnection) {
|
||||
this.scheduleReconnect();
|
||||
} else {
|
||||
this.showError('进程监控已意外停止。', 'Process monitor stopped unexpectedly.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.wantConnection = false;
|
||||
this.clearReconnectTimer();
|
||||
this.resetSocket();
|
||||
this.snapshot = null;
|
||||
this.render();
|
||||
@@ -409,6 +432,37 @@ export class ProcessManager {
|
||||
if (socket.readyState < WebSocket.CLOSING) socket.close(1000, 'Process monitor reset');
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (!this.wantConnection || !this.url || this.reconnectTimer !== null) return;
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts > RECONNECT_MAX_ATTEMPTS) {
|
||||
this.reconnectAttempts = 0;
|
||||
this.showError('进程监控已意外停止。', 'Process monitor stopped unexpectedly.');
|
||||
return;
|
||||
}
|
||||
const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * (2 ** (this.reconnectAttempts - 1)));
|
||||
if (this.reconnectAttempts === 1 && this.onReconnect) {
|
||||
this.onReconnect('进程监控已意外停止,正在尝试自动重连…', 'Process monitor stopped unexpectedly; attempting to reconnect automatically…');
|
||||
}
|
||||
this.setStatus('进程监控连接已断开,正在重连…', 'Process monitor disconnected; reconnecting…');
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
if (!this.wantConnection || !this.url) return;
|
||||
try {
|
||||
this.attach(this.url);
|
||||
} catch {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private clearReconnectTimer(): void {
|
||||
if (this.reconnectTimer !== null) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private isCurrent(socket: WebSocket, generation: number): boolean {
|
||||
return this.socket === socket && this.generation === generation;
|
||||
}
|
||||
|
||||
@@ -283,12 +283,13 @@ export class SSHSessionDO implements DurableObject {
|
||||
return Response.json({ error: 'Invalid process attachment token' }, { status: 401 });
|
||||
}
|
||||
const attach = this.processAttachTokens.get(token);
|
||||
if (!attach || attach.expiresAt < Date.now() || attach.mainWebSocket.readyState !== WebSocket.OPEN) {
|
||||
if (!attach || attach.mainWebSocket.readyState !== WebSocket.OPEN) {
|
||||
if (attach) this.deleteProcessAttachToken(token, attach);
|
||||
return Response.json({ error: 'Invalid or expired process attachment token' }, { status: 401 });
|
||||
}
|
||||
if (!attach.session) return Response.json({ error: 'SSH session is still initializing' }, { status: 409 });
|
||||
this.deleteProcessAttachToken(token, attach);
|
||||
// Keep the token valid after attach so the client can auto-reconnect the process monitor.
|
||||
// It is deleted in cleanup() when the main SSH session closes.
|
||||
const pair = new WebSocketPair();
|
||||
const client = pair[0];
|
||||
const server = pair[1];
|
||||
@@ -316,12 +317,15 @@ export class SSHSessionDO implements DurableObject {
|
||||
}
|
||||
|
||||
private registerProcessAttachToken(token: string, attachUrl: string, mainWebSocket: WebSocket): void {
|
||||
const expiresAt = Date.now() + SFTP_ATTACH_TOKEN_TTL_MS;
|
||||
// The process-monitor token intentionally lives for the lifetime of the main SSH
|
||||
// session (it is removed in cleanup() when that session closes). This lets the client
|
||||
// re-attach the process monitor after a transient drop instead of being locked out by a
|
||||
// one-time token. The SSH session setup still gates attach on attach.session being ready.
|
||||
const attach: AuxiliaryAttachToken = {
|
||||
mainWebSocket,
|
||||
attachUrl,
|
||||
expiresAt,
|
||||
timeout: setTimeout(() => this.deleteProcessAttachToken(token, attach), SFTP_ATTACH_TOKEN_TTL_MS),
|
||||
expiresAt: Number.MAX_SAFE_INTEGER,
|
||||
timeout: 0 as unknown as ReturnType<typeof setTimeout>,
|
||||
};
|
||||
this.processAttachTokens.set(token, attach);
|
||||
this.processTokenByMainWebSocket.set(mainWebSocket, token);
|
||||
|
||||
Reference in new issue
Block a user