mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
Fix corner cases in connect/disconnect lifecycle and resource cleanup
1. Move TS6 handler patch to after client.connect() — the library's connect() internally replaces handler via #S(), discarding any patch applied beforehand. Patching after connect() is safe because clientinit is sent in async message callbacks after Init1 round-trips. 2. Preserve detectedProtocol across reconnect — disconnect() resets it to "unknown", causing the TS6 patch to be skipped on reconnect. 3. Prevent double "disconnected" event in BotInstance — disconnect() emitted it directly AND the async TS3Client disconnect triggered another through the event chain. 4. Guard playNext() against running after disconnect — check connected flag to avoid ghost queue processing. 5. Fix UDP error timer leak — clear previous timer before setting a new one to prevent accumulation. 6. Fix isPortFree() FD leak — close the test server on error path. https://claude.ai/code/session_01QzvMLUT3UkhsffShcY1qzD
This commit is contained in:
3 files changed
+51
-28
No files matched your search
+8
-3
@@ -105,6 +105,9 @@ export class BotInstance extends EventEmitter {
|
||||
});
|
||||
|
||||
this.tsClient.on("disconnected", () => {
|
||||
// Avoid duplicate event if disconnect() was called explicitly
|
||||
// (it already set connected = false and emitted "disconnected").
|
||||
if (!this.connected) return;
|
||||
this.connected = false;
|
||||
this.player.stop();
|
||||
this.emit("disconnected");
|
||||
@@ -119,8 +122,10 @@ export class BotInstance extends EventEmitter {
|
||||
|
||||
disconnect(): void {
|
||||
this.player.stop();
|
||||
this.tsClient.disconnect();
|
||||
// Set connected = false BEFORE tsClient.disconnect() so that the
|
||||
// "disconnected" event handler (setupTsEvents) won't emit a duplicate.
|
||||
this.connected = false;
|
||||
this.tsClient.disconnect();
|
||||
this.emit("disconnected");
|
||||
}
|
||||
|
||||
@@ -488,7 +493,7 @@ export class BotInstance extends EventEmitter {
|
||||
}
|
||||
|
||||
private async playNext(): Promise<void> {
|
||||
if (this.isAdvancing) return;
|
||||
if (this.isAdvancing || !this.connected) return;
|
||||
this.isAdvancing = true;
|
||||
try {
|
||||
this.voteSkipUsers.clear();
|
||||
@@ -497,7 +502,7 @@ export class BotInstance extends EventEmitter {
|
||||
let started = await this.resolveAndPlay(next);
|
||||
if (!started) {
|
||||
// Skip to next if URL resolve fails (up to 3 retries)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 3 && this.connected; i++) {
|
||||
const retry = this.queue.next();
|
||||
if (!retry) break;
|
||||
if (await this.resolveAndPlay(retry)) {
|
||||
|
||||
@@ -17,7 +17,9 @@ export interface ApiServerManager {
|
||||
function isPortFree(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", () => resolve(false));
|
||||
server.once("error", () => {
|
||||
server.close(() => resolve(false));
|
||||
});
|
||||
server.once("listening", () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
|
||||
+40
-24
@@ -119,10 +119,15 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Guard against calling connect() while already connected
|
||||
// Guard against calling connect() while already connected.
|
||||
// Save detectedProtocol first because disconnect() resets it.
|
||||
if (this.client) {
|
||||
this.logger.warn("connect() called while already connected, disconnecting first");
|
||||
const savedProtocol = this.detectedProtocol;
|
||||
const savedHttpQuery = this.httpQuery;
|
||||
this.disconnect();
|
||||
this.detectedProtocol = savedProtocol;
|
||||
this.httpQuery = savedHttpQuery;
|
||||
// Give the old client a moment to tear down
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
@@ -139,7 +144,9 @@ export class TS3Client extends EventEmitter {
|
||||
udpErrorCount++;
|
||||
if (udpErrorCount === 1) {
|
||||
this.logger.warn(msg);
|
||||
// After 2 seconds, log a summary and reset
|
||||
// After 2 seconds, log a summary and reset.
|
||||
// Clear any previous timer to avoid leaking it.
|
||||
if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer);
|
||||
this.udpErrorTimer = setTimeout(() => {
|
||||
if (udpErrorCount > 1) {
|
||||
this.logger.warn(`udp send error (repeated ${udpErrorCount} times, connection may be lost)`);
|
||||
@@ -162,28 +169,6 @@ export class TS3Client extends EventEmitter {
|
||||
},
|
||||
});
|
||||
|
||||
// Patch handler.sendPacket to intercept clientinit during handshake for TS6.
|
||||
// The library's commandMiddleware only applies to post-connection commands
|
||||
// (sendCommandNoWait/execCommand), but the handshake's clientinit is sent
|
||||
// directly via handler.sendPacket, bypassing middleware entirely.
|
||||
// This monkey-patch ensures the version is upgraded for TS6 servers.
|
||||
if (this.detectedProtocol === "ts6") {
|
||||
this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2");
|
||||
const origSendPacket = this.client.handler.sendPacket.bind(this.client.handler);
|
||||
this.client.handler.sendPacket = (pType: number, data: Uint8Array, flags: number) => {
|
||||
// PacketType.Command = 2
|
||||
if (pType === 2) {
|
||||
const str = Buffer.from(data).toString("utf-8");
|
||||
if (str.startsWith("clientinit ")) {
|
||||
const patched = patchClientInitVersion(str);
|
||||
origSendPacket(pType, Buffer.from(patched), flags);
|
||||
return;
|
||||
}
|
||||
}
|
||||
origSendPacket(pType, data, flags);
|
||||
};
|
||||
}
|
||||
|
||||
this.client.on("textMessage", (msg: TextMessage) => {
|
||||
const tsMsg: TS3TextMessage = {
|
||||
invokerName: msg.invokerName,
|
||||
@@ -209,6 +194,37 @@ export class TS3Client extends EventEmitter {
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
|
||||
// Patch handler.sendPacket to intercept clientinit during handshake for TS6.
|
||||
//
|
||||
// Why after connect(): The library's connect() internally calls #S() which
|
||||
// replaces this.handler with a fresh PacketHandler. Any patch applied before
|
||||
// connect() would be discarded. Patching here is safe because connect() only
|
||||
// establishes the UDP socket and sends the first Init1 packet — the actual
|
||||
// clientinit command is sent later in async message callbacks (after Init1
|
||||
// round-trips complete), which cannot fire before this sync continuation.
|
||||
//
|
||||
// Why not commandMiddleware: The library's middleware chain only applies to
|
||||
// post-connection commands (sendCommandNoWait/execCommand). The handshake's
|
||||
// clientinit is sent directly via handler.sendPacket(), bypassing middleware.
|
||||
if (this.detectedProtocol === "ts6") {
|
||||
this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2");
|
||||
const handler = this.client.handler;
|
||||
const origSendPacket = handler.sendPacket.bind(handler);
|
||||
handler.sendPacket = (pType: number, data: Uint8Array, flags: number) => {
|
||||
// PacketType.Command = 2
|
||||
if (pType === 2) {
|
||||
const str = Buffer.from(data).toString("utf-8");
|
||||
if (str.startsWith("clientinit ")) {
|
||||
const patched = patchClientInitVersion(str);
|
||||
origSendPacket(pType, Buffer.from(patched), flags);
|
||||
return;
|
||||
}
|
||||
}
|
||||
origSendPacket(pType, data, flags);
|
||||
};
|
||||
}
|
||||
|
||||
await this.client.waitConnected();
|
||||
this.clientId = this.client.clientID();
|
||||
this.voiceFramesSent = 0;
|
||||
|
||||
Reference in new issue
Block a user