mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Fix corner cases in protocol detection and connection cleanup
- Fix double-resolve race in probeTS3Query (data event vs connect timer)
by guarding with a resolved flag
- Fix double-resolve in probeTS6HttpQuery similarly
- Support custom query ports in detectServerProtocol via DetectOptions
- Clean up httpQuery and detectedProtocol on disconnect()
- Add res.on("error") handler in HTTP Query client to avoid unhandled errors
- Improve logging: warn with actionable message when protocol is unknown
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
This commit is contained in:
3 files changed
+61
-33
No files matched your search
@@ -87,12 +87,25 @@ export class TS3Client extends EventEmitter {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.info({ addr }, "Detecting server protocol (TS3/TS6)...");
|
this.logger.info({ addr }, "Detecting server protocol (TS3/TS6)...");
|
||||||
const detection = await detectServerProtocol(this.options.host, this.options.port);
|
const detection = await detectServerProtocol(
|
||||||
this.detectedProtocol = detection.protocol;
|
this.options.host,
|
||||||
this.logger.info(
|
this.options.port,
|
||||||
{ addr, protocol: this.detectedProtocol, queryPort: detection.queryPort },
|
3000,
|
||||||
`Server protocol detected: ${this.detectedProtocol.toUpperCase()}`,
|
{ ts3QueryPort: 10011, ts6HttpPort: 10080 },
|
||||||
);
|
);
|
||||||
|
this.detectedProtocol = detection.protocol;
|
||||||
|
if (this.detectedProtocol === "unknown") {
|
||||||
|
this.logger.warn(
|
||||||
|
{ addr },
|
||||||
|
"Could not detect server protocol (query ports 10011/10080 unreachable). " +
|
||||||
|
"Will attempt voice connection anyway. Use serverProtocol option to force TS3 or TS6.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.info(
|
||||||
|
{ addr, protocol: this.detectedProtocol, queryPort: detection.queryPort },
|
||||||
|
`Server protocol detected: ${this.detectedProtocol.toUpperCase()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up TS6 HTTP Query if applicable
|
// Set up TS6 HTTP Query if applicable
|
||||||
@@ -270,6 +283,8 @@ export class TS3Client extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clientId = 0;
|
this.clientId = 0;
|
||||||
|
this.httpQuery = null;
|
||||||
|
this.detectedProtocol = "unknown";
|
||||||
this.logger.info("Disconnected from TeamSpeak server");
|
this.logger.info("Disconnected from TeamSpeak server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +82,7 @@ export class TS6HttpQuery extends EventEmitter {
|
|||||||
let data = "";
|
let data = "";
|
||||||
res.setEncoding("utf-8");
|
res.setEncoding("utf-8");
|
||||||
res.on("data", (chunk: string) => (data += chunk));
|
res.on("data", (chunk: string) => (data += chunk));
|
||||||
|
res.on("error", reject);
|
||||||
res.on("end", () => {
|
res.on("end", () => {
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ export interface ProtocolDetectResult {
|
|||||||
voicePort: number;
|
voicePort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DetectOptions {
|
||||||
|
/** TS3 ServerQuery probe port (default: 10011) */
|
||||||
|
ts3QueryPort?: number;
|
||||||
|
/** TS6 HTTP Query probe port (default: 10080) */
|
||||||
|
ts6HttpPort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probe a TeamSpeak server to determine if it's running TS3 or TS6.
|
* Probe a TeamSpeak server to determine if it's running TS3 or TS6.
|
||||||
*
|
*
|
||||||
@@ -23,17 +30,22 @@ export async function detectServerProtocol(
|
|||||||
host: string,
|
host: string,
|
||||||
voicePort = 9987,
|
voicePort = 9987,
|
||||||
timeoutMs = 3000,
|
timeoutMs = 3000,
|
||||||
|
options?: DetectOptions,
|
||||||
): Promise<ProtocolDetectResult> {
|
): Promise<ProtocolDetectResult> {
|
||||||
|
const ts3Port = options?.ts3QueryPort ?? 10011;
|
||||||
|
const ts6Port = options?.ts6HttpPort ?? 10080;
|
||||||
|
|
||||||
const [ts3, ts6] = await Promise.allSettled([
|
const [ts3, ts6] = await Promise.allSettled([
|
||||||
probeTS3Query(host, 10011, timeoutMs),
|
probeTS3Query(host, ts3Port, timeoutMs),
|
||||||
probeTS6HttpQuery(host, 10080, timeoutMs),
|
probeTS6HttpQuery(host, ts6Port, timeoutMs),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Prefer TS3 if both somehow respond (shouldn't happen in practice)
|
||||||
if (ts3.status === "fulfilled" && ts3.value) {
|
if (ts3.status === "fulfilled" && ts3.value) {
|
||||||
return { protocol: "ts3", queryPort: 10011, voicePort };
|
return { protocol: "ts3", queryPort: ts3Port, voicePort };
|
||||||
}
|
}
|
||||||
if (ts6.status === "fulfilled" && ts6.value) {
|
if (ts6.status === "fulfilled" && ts6.value) {
|
||||||
return { protocol: "ts6", queryPort: 10080, voicePort };
|
return { protocol: "ts6", queryPort: ts6Port, voicePort };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { protocol: "unknown", queryPort: null, voicePort };
|
return { protocol: "unknown", queryPort: null, voicePort };
|
||||||
@@ -44,49 +56,50 @@ export async function detectServerProtocol(
|
|||||||
*/
|
*/
|
||||||
function probeTS3Query(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
function probeTS3Query(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const socket = net.createConnection({ host, port, timeout: timeoutMs });
|
let resolved = false;
|
||||||
let banner = "";
|
const done = (value: boolean) => {
|
||||||
|
if (resolved) return;
|
||||||
const cleanup = () => {
|
resolved = true;
|
||||||
socket.removeAllListeners();
|
socket.removeAllListeners();
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
resolve(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const socket = net.createConnection({ host, port, timeout: timeoutMs });
|
||||||
|
let banner = "";
|
||||||
|
|
||||||
socket.setTimeout(timeoutMs);
|
socket.setTimeout(timeoutMs);
|
||||||
|
|
||||||
socket.on("data", (data: Buffer) => {
|
socket.on("data", (data: Buffer) => {
|
||||||
banner += data.toString("utf-8");
|
banner += data.toString("utf-8");
|
||||||
if (banner.includes("TS3")) {
|
if (banner.includes("TS3")) {
|
||||||
cleanup();
|
done(true);
|
||||||
resolve(true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
// Wait briefly for banner
|
// Wait briefly for banner after TCP connect
|
||||||
setTimeout(() => {
|
setTimeout(() => done(banner.includes("TS3")), 500);
|
||||||
cleanup();
|
|
||||||
resolve(banner.includes("TS3"));
|
|
||||||
}, 500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("error", () => {
|
socket.on("error", () => done(false));
|
||||||
cleanup();
|
socket.on("timeout", () => done(false));
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("timeout", () => {
|
|
||||||
cleanup();
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probe TS6 HTTP Query by sending GET / and checking for a valid response.
|
* Probe TS6 HTTP Query by sending GET / and checking for a valid response.
|
||||||
|
* Any HTTP status (including 401/403) confirms the TS6 HTTP Query exists.
|
||||||
*/
|
*/
|
||||||
function probeTS6HttpQuery(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
function probeTS6HttpQuery(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
const done = (value: boolean) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
const req = http.request(
|
const req = http.request(
|
||||||
{
|
{
|
||||||
hostname: host,
|
hostname: host,
|
||||||
@@ -97,16 +110,15 @@ function probeTS6HttpQuery(host: string, port: number, timeoutMs: number): Promi
|
|||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
// TS6 HTTP Query returns some response (even 401/403 is valid — it means the service exists)
|
|
||||||
res.resume();
|
res.resume();
|
||||||
resolve(res.statusCode !== undefined);
|
done(res.statusCode !== undefined);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
req.on("error", () => resolve(false));
|
req.on("error", () => done(false));
|
||||||
req.on("timeout", () => {
|
req.on("timeout", () => {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
resolve(false);
|
done(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
|
|||||||
Reference in new issue
Block a user