🐛 fix: instantly clear presence on disable

This commit is contained in:
2026-06-15 14:12:43 +07:00
parent c8814c463e
commit dd536d1e2d
2 changed files with 64 additions and 16 deletions
+9
View File
@@ -29,6 +29,15 @@ export class MirrorConnection {
} }
} }
/** Sends a final frame and closes the connection once it has been flushed. */
sendActivityAndClose(payload: Buffer): void {
if (this.sock.writable) {
this.sock.end(encodeFrame(OP_FRAME, payload))
} else {
this.sock.destroy()
}
}
destroy(): void { destroy(): void {
this.sock.destroy() this.sock.destroy()
} }
+55 -16
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'crypto'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import * as net from 'net' import * as net from 'net'
import { encodeFrame, Frame, FrameReader, OP_HANDSHAKE, parseFramePayload } from './ipc-protocol' import { encodeFrame, Frame, FrameReader, OP_HANDSHAKE, parseFramePayload } from './ipc-protocol'
@@ -123,6 +124,8 @@ export class RpcRelay extends EventEmitter {
private running = false private running = false
private lastError: string | null = null private lastError: string | null = null
private restarting = false private restarting = false
private mirrors = new Map<number, MirrorConnection>()
private lastActivityPid = new Map<number, number>()
getStatus(): RelayStatus { getStatus(): RelayStatus {
const instances: RelayInstance[] = this.claimed.map(({ index, path: socketPath }, i) => ({ const instances: RelayInstance[] = this.claimed.map(({ index, path: socketPath }, i) => ({
@@ -150,6 +153,7 @@ export class RpcRelay extends EventEmitter {
this.disabledMirrors.delete(index) this.disabledMirrors.delete(index)
} else { } else {
this.disabledMirrors.add(index) this.disabledMirrors.add(index)
this.clearMirrorActivity(index)
} }
this.emitStatus() this.emitStatus()
return this.getStatus() return this.getStatus()
@@ -219,6 +223,10 @@ export class RpcRelay extends EventEmitter {
platform.removeFakeSocket(platform.fakeSocketPath()) platform.removeFakeSocket(platform.fakeSocketPath())
for (const claimed of this.claimed) platform.restoreSocket(claimed) for (const claimed of this.claimed) platform.restoreSocket(claimed)
for (const mirror of this.mirrors.values()) mirror.destroy()
this.mirrors.clear()
this.lastActivityPid.clear()
this.claimed = [] this.claimed = []
this.connectedClients.clear() this.connectedClients.clear()
this.emitStatus() this.emitStatus()
@@ -280,13 +288,13 @@ export class RpcRelay extends EventEmitter {
const primaryReader = new FrameReader() const primaryReader = new FrameReader()
let handshakePayload: Buffer | null = null let handshakePayload: Buffer | null = null
const mirrors = new Map<number, MirrorConnection>()
const cleanup = (): void => { const cleanup = (): void => {
client.destroy() client.destroy()
primary.destroy() primary.destroy()
for (const mirror of mirrors.values()) mirror.destroy() for (const mirror of this.mirrors.values()) mirror.destroy()
mirrors.clear() this.mirrors.clear()
this.lastActivityPid.clear()
this.connectedClients.delete(clientId) this.connectedClients.delete(clientId)
this.emitStatus() this.emitStatus()
} }
@@ -309,7 +317,7 @@ export class RpcRelay extends EventEmitter {
pendingToPrimary.push(encoded) pendingToPrimary.push(encoded)
} }
if (handshakePayload) this.mirrorFrame(frame, handshakePayload, mirrors) if (handshakePayload) this.mirrorFrame(frame, handshakePayload)
void this.recordActivity(frame, clientAppId) void this.recordActivity(frame, clientAppId)
} }
}) })
@@ -355,28 +363,35 @@ export class RpcRelay extends EventEmitter {
} }
/** Forwards the handshake and SET_ACTIVITY frames to every enabled mirror instance. */ /** Forwards the handshake and SET_ACTIVITY frames to every enabled mirror instance. */
private mirrorFrame( private mirrorFrame(frame: Frame, handshakePayload: Buffer): void {
frame: Frame, const payload = frame.op !== OP_HANDSHAKE ? parseFramePayload(frame) : null
handshakePayload: Buffer, const isSetActivity = payload?.cmd === 'SET_ACTIVITY'
mirrors: Map<number, MirrorConnection>
): void {
const isSetActivity =
frame.op !== OP_HANDSHAKE && parseFramePayload(frame)?.cmd === 'SET_ACTIVITY'
if (frame.op !== OP_HANDSHAKE && !isSetActivity) return if (frame.op !== OP_HANDSHAKE && !isSetActivity) return
if (isSetActivity) {
const pid = (payload!.args as { pid?: number } | undefined)?.pid
if (typeof pid === 'number') {
for (let i = 1; i < this.claimed.length; i++) {
this.lastActivityPid.set(this.claimed[i].index, pid)
}
}
}
for (let i = 1; i < this.claimed.length; i++) { for (let i = 1; i < this.claimed.length; i++) {
const { index, path: mirrorPath } = this.claimed[i] const { index, path: mirrorPath } = this.claimed[i]
if (this.disabledMirrors.has(index)) { if (this.disabledMirrors.has(index)) {
mirrors.get(i)?.destroy() this.mirrors.get(index)?.destroy()
mirrors.delete(i) this.mirrors.delete(index)
continue continue
} }
let mirror = mirrors.get(i) let mirror = this.mirrors.get(index)
if (!mirror) { if (!mirror) {
mirror = new MirrorConnection(mirrorPath, handshakePayload, () => mirrors.delete(i)) mirror = new MirrorConnection(mirrorPath, handshakePayload, () =>
mirrors.set(i, mirror) this.mirrors.delete(index)
)
this.mirrors.set(index, mirror)
if (frame.op === OP_HANDSHAKE) continue // handshake already sent on connect if (frame.op === OP_HANDSHAKE) continue // handshake already sent on connect
} }
@@ -384,6 +399,30 @@ export class RpcRelay extends EventEmitter {
} }
} }
/** Sends a SET_ACTIVITY frame clearing the presence on a mirror, then disconnects it. */
private clearMirrorActivity(index: number): void {
const mirror = this.mirrors.get(index)
if (!mirror) return
const pid = this.lastActivityPid.get(index)
if (pid !== undefined) {
const clearPayload = Buffer.from(
JSON.stringify({
cmd: 'SET_ACTIVITY',
args: { pid, activity: null },
nonce: randomUUID()
}),
'utf8'
)
mirror.sendActivityAndClose(clearPayload)
} else {
mirror.destroy()
}
this.mirrors.delete(index)
this.lastActivityPid.delete(index)
}
private async recordActivity(frame: Frame, appId: string | null): Promise<void> { private async recordActivity(frame: Frame, appId: string | null): Promise<void> {
if (frame.op === OP_HANDSHAKE) return if (frame.op === OP_HANDSHAKE) return