mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Merge pull request #155 from ZHANGTIANYAO1/fix/152-setup-console-eio
fix(setup): stop a failed console write from aborting setup, require Node 22+ (#152)
This commit is contained in:
9 files changed
+349
-30
No files matched your search
@@ -65,22 +65,22 @@
|
||||
先装好 Node.js,其余依赖(含内置 FFmpeg)全部自动安装。
|
||||
|
||||
```
|
||||
1. 安装 Node.js 20 LTS 或 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/)
|
||||
1. 安装 Node.js 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/)
|
||||
2. 下载或 clone 本项目
|
||||
3. 双击 scripts\setup.bat (安装依赖并构建,不含 Node.js 本身)
|
||||
4. 双击 scripts\start.bat (启动机器人)
|
||||
5. 浏览器打开 http://localhost:3000
|
||||
```
|
||||
|
||||
> **先装 Node.js 20 LTS 或 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。
|
||||
> **先装 Node.js 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。
|
||||
>
|
||||
> 之后 `setup.bat` 会运行 `npm install` 安装所有依赖(包括内置 FFmpeg),按当前 Node 版本准备好原生模块,最后构建项目。之后每次只需双击 `start.bat` 启动。
|
||||
>
|
||||
> 更新的 Node 大版本(如 24)也能用,但通常没有现成的 opus / better-sqlite3 预编译包,安装脚本会改用源码编译,需要 C/C++ 构建工具且耗时更久——所以推荐 20 / 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。
|
||||
> **Node 20 已不再支持**:better-sqlite3 从 12.10.0 起不再发布它那个 ABI(115)的预编译包,装起来必须先备好 Python + C++ 构建工具([#152](https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/152))。Node 24 及更新的大版本能用,但 @discordjs/opus 0.10.0 同样没有 Node 24(ABI 137)的预编译包,安装脚本会改用源码编译,需要构建工具且耗时更久——所以推荐 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。
|
||||
|
||||
### 方式二:手动安装(所有系统)
|
||||
|
||||
**前置条件:** [Node.js 20 LTS 或 22 LTS](https://nodejs.org/)(推荐;更新的大版本可用但需要源码编译原生模块)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。
|
||||
**前置条件:** [Node.js 22 LTS](https://nodejs.org/)(Node 24 及更新版本也能用,但需要源码编译原生模块;Node 20 已不再支持)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。
|
||||
FFmpeg **已自动内置**,无需手动安装。
|
||||
|
||||
```bash
|
||||
@@ -510,7 +510,7 @@ teamspeak-music-bot/
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| **运行时** | Node.js 20 / 22 LTS, TypeScript 5 |
|
||||
| **运行时** | Node.js 22 LTS(推荐), TypeScript 5 |
|
||||
| **后端框架** | Express 4, WebSocket (ws) |
|
||||
| **数据库** | better-sqlite3 (SQLite) |
|
||||
| **音频处理** | FFmpeg (ffmpeg-static 内置), @discordjs/opus |
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "TeamSpeak music bot with NetEase Cloud Music and QQ Music support",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"node": "^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createLineWriter } from "./lib/console-log.mjs";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const NODE_MODULES = join(ROOT, "node_modules");
|
||||
@@ -106,7 +107,11 @@ for (const spec of REQUIRED) {
|
||||
function report() {
|
||||
const stamp = readStamp();
|
||||
const mismatch = broken.find((b) => b.abiMismatch);
|
||||
const out = (line) => process.stderr.write(`${line}\n`);
|
||||
// Never let a failed console write become an uncaught error and replace this
|
||||
// report with a stack trace - the console that cannot print the Chinese half
|
||||
// of these lines is exactly the one a user needs the English half from.
|
||||
// See scripts/lib/console-log.mjs and issue #152.
|
||||
const out = createLineWriter(process.stderr);
|
||||
|
||||
out("");
|
||||
out("============================================================");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# ==========================================
|
||||
|
||||
# --- Stage 1: Build backend + frontend ---
|
||||
FROM node:20-slim AS builder
|
||||
FROM node:22-slim AS builder
|
||||
|
||||
# Install build tools for native modules (opus, better-sqlite3)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -30,7 +30,7 @@ RUN npm run build
|
||||
RUN rm -rf node_modules && npm ci --production && npm cache clean --force
|
||||
|
||||
# --- Stage 2: Production image ---
|
||||
FROM node:20-slim
|
||||
FROM node:22-slim
|
||||
|
||||
# Install system FFmpeg — the ffmpeg-static npm package bundles a pre-compiled
|
||||
# binary that can SIGSEGV inside Docker (incompatible glibc / missing libs).
|
||||
@@ -51,6 +51,8 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
# goes through npm, but an interactive `docker exec ... npm start` would
|
||||
# otherwise die on a missing script rather than starting the bot.
|
||||
COPY --from=builder /app/scripts/check-native.mjs ./scripts/check-native.mjs
|
||||
# ...and the module it imports for crash-proof logging.
|
||||
COPY --from=builder /app/scripts/lib/console-log.mjs ./scripts/lib/console-log.mjs
|
||||
|
||||
# Data directory for database, cookies, logs
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
@@ -51,6 +51,7 @@ import { Readable } from "node:stream";
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createLineWriter } from "./lib/console-log.mjs";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const NODE_MODULES = join(ROOT, "node_modules");
|
||||
@@ -61,6 +62,11 @@ const CDN = process.argv[2] || "https://cdn.npmmirror.com/binaries";
|
||||
const PLATFORM = process.platform;
|
||||
const ARCH = process.arch;
|
||||
const NODE_ABI = process.versions.modules;
|
||||
const NODE_MAJOR = Number(process.versions.node.split(".")[0]);
|
||||
/** The newest Node major this project is regularly tested against, and the one
|
||||
* every required addon currently ships a prebuild for. Keep in sync with
|
||||
* TESTED_NODE_MAJOR in scripts/setup.bat. */
|
||||
const TESTED_NODE_MAJOR = 22;
|
||||
|
||||
/** Modules the bot cannot start without. ffmpeg-static is optional: a system
|
||||
* ffmpeg on PATH is a documented fallback, so it only ever produces a WARN. */
|
||||
@@ -79,10 +85,17 @@ const FFMPEG_MIN_BYTES = 20 * 1024 * 1024;
|
||||
// log keeps the full transcript too.
|
||||
const ECHO_STDOUT = process.env.TSMB_BINARY_LOG_STDOUT === "1";
|
||||
|
||||
// Both writers swallow a failed write instead of letting it become an uncaught
|
||||
// 'error' event: a console that cannot print the Chinese half of a line (issue
|
||||
// #152) must not be able to abort a whole setup run. The two streams degrade
|
||||
// independently, so setup.log keeps the full bilingual transcript either way.
|
||||
const writeErr = createLineWriter(process.stderr);
|
||||
const writeOut = createLineWriter(process.stdout);
|
||||
|
||||
function log(msg) {
|
||||
const line = msg === "" ? "" : ` [binary] ${msg}`;
|
||||
process.stderr.write(`${line}\n`);
|
||||
if (ECHO_STDOUT) process.stdout.write(`${line}\n`);
|
||||
writeErr(line);
|
||||
if (ECHO_STDOUT) writeOut(line);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -401,6 +414,25 @@ function buildFromSource(command) {
|
||||
execSync(command, { cwd: ROOT, stdio: ["ignore", "inherit", "inherit"] });
|
||||
}
|
||||
|
||||
/**
|
||||
* A 404 from the CDN is not a mirror outage: it means this exact package
|
||||
* version publishes no prebuilt binary for the running Node ABI at all.
|
||||
* @discordjs/opus 0.10.0 has no build for Node 24 (ABI 137), so a user on that
|
||||
* major lands in the source-build fallback below and is told to install Python
|
||||
* and a C++ toolchain. Switching Node major is the far cheaper fix, and nothing
|
||||
* else in this output points at it. (better-sqlite3 dropping its Node 20 / ABI
|
||||
* 115 builds in 12.10.0 is why Node 20 is no longer accepted at all.)
|
||||
* See issue #152.
|
||||
*/
|
||||
function explainMissingPrebuild(name, version, err) {
|
||||
if (!/HTTP 404/.test(err.message)) return;
|
||||
log(`${name}: ${name}@${version} ships no prebuilt binary for Node ${NODE_MAJOR} (ABI ${NODE_ABI})`);
|
||||
log(
|
||||
`${name}: Node ${TESTED_NODE_MAJOR} LTS has one — switching Node is usually much quicker than ` +
|
||||
`setting up a compiler (换用 Node ${TESTED_NODE_MAJOR} LTS 通常比装编译环境快得多)`,
|
||||
);
|
||||
}
|
||||
|
||||
function buildToolsHint() {
|
||||
log("Install build tools first:");
|
||||
log(" Windows: npm install --global windows-build-tools (或安装 Visual Studio Build Tools + Python)");
|
||||
@@ -521,6 +553,7 @@ async function ensureOpus() {
|
||||
log(`${name}: prebuilt binary installed`);
|
||||
} catch (cdnErr) {
|
||||
log(`${name}: CDN install failed (${cdnErr.message})`);
|
||||
explainMissingPrebuild(name, version, cdnErr);
|
||||
log(`${name}: falling back to a source build — 'npm rebuild ${name}' (可能需要几分钟)`);
|
||||
buildFromSource(`npm rebuild ${name}`);
|
||||
}
|
||||
@@ -591,6 +624,7 @@ async function ensureBetterSqlite3() {
|
||||
log(`${name}: prebuilt binary installed (${humanSize(dest)})`);
|
||||
} catch (cdnErr) {
|
||||
log(`${name}: CDN install failed (${cdnErr.message})`);
|
||||
explainMissingPrebuild(name, version, cdnErr);
|
||||
log(`${name}: falling back to a source build — 'npm rebuild ${name} --build-from-source' (可能需要几分钟)`);
|
||||
buildFromSource(`npm rebuild ${name} --build-from-source`);
|
||||
}
|
||||
@@ -661,10 +695,9 @@ try {
|
||||
// that same loop. Run these concurrently and the first module to fall back to
|
||||
// a source build kills every download still in flight — the connection is
|
||||
// healthy, the timer just never got a chance to be reset. That is not a rare
|
||||
// race: npmmirror has no opus prebuild for ABI 137 (Node 24) and no
|
||||
// better-sqlite3 prebuild for ABI 115 (Node 20), so on both of the Node
|
||||
// versions this project supports, one module 404s within ~100ms and starts
|
||||
// building while ffmpeg's ~80MB download is still going. ffmpeg is optional,
|
||||
// race: @discordjs/opus 0.10.0 has no prebuild for ABI 137, so on Node 24 that
|
||||
// module 404s within ~100ms and starts building while ffmpeg's ~80MB download
|
||||
// is still going. ffmpeg is optional,
|
||||
// so the spurious failure used to be swallowed as a WARN and setup still
|
||||
// reported success — leaving the user with no ffmpeg and no working playback.
|
||||
// Nothing here benefits from overlap anyway: every probe is execFileSync.
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Crash-proof line logging for the setup scripts.
|
||||
*
|
||||
* WHY THIS EXISTS (issue #152)
|
||||
* ----------------------------
|
||||
* setup.bat runs `chcp 65001` and shows progress on stderr. Some Windows
|
||||
* consoles - Windows Server 2012 R2 above all - cannot render non-ASCII text in
|
||||
* that code page and the OS fails the write with EIO. `process.stderr` is an
|
||||
* ordinary stream, so that EIO arrives as an 'error' event, and a stream with
|
||||
* no 'error' listener rethrows it as an uncaught exception:
|
||||
*
|
||||
* Error: write EIO
|
||||
* at afterWriteDispatched (node:internal/stream_base_commons:159:15)
|
||||
* ...
|
||||
* at log (scripts/download-binaries.mjs:84:18)
|
||||
* at ensureFfmpeg (scripts/download-binaries.mjs:451:5)
|
||||
*
|
||||
* That is setup killing itself inside its own progress logging, on the first
|
||||
* line of the run that happened to contain Chinese - nothing was wrong with the
|
||||
* download it was about to start.
|
||||
*
|
||||
* So: listen for the error and degrade instead of dying.
|
||||
* full -> ascii : drop the CJK the console choked on, keep the English half
|
||||
* ascii -> off : the stream is simply gone (closed pipe) - stay quiet
|
||||
* Each stream degrades on its own, so a console that gives up does not cost
|
||||
* setup.log its full bilingual transcript: that stdout is a redirected file.
|
||||
*/
|
||||
|
||||
const HAS_NON_ASCII = /[^\x00-\x7F]/;
|
||||
|
||||
/** Placeholders for a removed run: one that separated words, one that did not. */
|
||||
const SPACED = "\u0000";
|
||||
const TIGHT = "\u0001";
|
||||
|
||||
/** Punctuation the bilingual strings use that has an obvious ASCII twin. */
|
||||
const PUNCTUATION = new Map(
|
||||
Object.entries({
|
||||
"—": "-",
|
||||
"–": "-",
|
||||
"…": "...",
|
||||
"“": '"',
|
||||
"”": '"',
|
||||
"‘": "'",
|
||||
"’": "'",
|
||||
",": ",",
|
||||
"。": ".",
|
||||
"、": ",",
|
||||
":": ":",
|
||||
";": ";",
|
||||
"(": "(",
|
||||
")": ")",
|
||||
"!": "!",
|
||||
"?": "?",
|
||||
"←": "<-",
|
||||
"→": "->",
|
||||
"×": "x",
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Best-effort ASCII rendering of a log line, for a console that cannot print
|
||||
* anything else. Returns null when nothing worth printing survives - every
|
||||
* Chinese-only line in these scripts sits directly beside an English line
|
||||
* saying the same thing, so dropping it loses no information.
|
||||
*/
|
||||
export function toAsciiFallback(text) {
|
||||
if (!HAS_NON_ASCII.test(text)) return text;
|
||||
|
||||
let out = "";
|
||||
for (const ch of text) out += PUNCTUATION.get(ch) ?? ch;
|
||||
|
||||
out = out
|
||||
.replace(/[\u0000\u0001]/g, "")
|
||||
// Mark each removed run rather than just deleting it, so the tidy-up below
|
||||
// can tell "a separator that introduced text we dropped" from "a separator
|
||||
// that belongs to the English half". SPACED was holding two ASCII words
|
||||
// apart; TIGHT was hugging a bracket or a comma.
|
||||
.replace(/[ \t]*[^\x00-\x7F]+[ \t]*/g, (run) =>
|
||||
/^[ \t]/.test(run) && /[ \t]$/.test(run) ? SPACED : TIGHT,
|
||||
)
|
||||
// "(可能需要几分钟)" — the parentheses held nothing else.
|
||||
.replace(/[ \t]*\([ \t]*(?:[\u0000\u0001][ \t]*)+\)/g, "")
|
||||
// "FAILED — 编译失败", "(~80 MB, 请耐心等待)" — drop the trailing marks along
|
||||
// with the separators that were only ever there to introduce them.
|
||||
.replace(/[ \t]*[-,;:]*[ \t]*(?:[\u0000\u0001][ \t,;:-]*)+(?=[)\]]|$)/gm, "")
|
||||
.replace(/\u0000/g, " ")
|
||||
.replace(/\u0001/g, "")
|
||||
.replace(/[ \t]+$/gm, "");
|
||||
|
||||
return /[A-Za-z0-9]/.test(out) ? out : null;
|
||||
}
|
||||
|
||||
/** One degradation state per stream, shared by every writer built on it. */
|
||||
const guards = new WeakMap();
|
||||
|
||||
function guardFor(stream) {
|
||||
const existing = guards.get(stream);
|
||||
if (existing) return existing;
|
||||
|
||||
const guard = { mode: "full" };
|
||||
guards.set(stream, guard);
|
||||
try {
|
||||
// The whole point: without this listener the next EIO/EPIPE is fatal.
|
||||
stream.on("error", () => degrade(guard));
|
||||
} catch {
|
||||
/* not an EventEmitter - the try/catch around write() still guards us */
|
||||
}
|
||||
return guard;
|
||||
}
|
||||
|
||||
function degrade(guard) {
|
||||
guard.mode = guard.mode === "full" ? "ascii" : "off";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `writeLine(text)` that appends a newline, never throws, and never
|
||||
* lets a failed console write take the process down with it.
|
||||
* Returns true when the line reached the stream.
|
||||
*/
|
||||
export function createLineWriter(stream) {
|
||||
const guard = guardFor(stream);
|
||||
|
||||
return function writeLine(text) {
|
||||
if (guard.mode === "off") return false;
|
||||
|
||||
let line = text;
|
||||
if (guard.mode === "ascii") {
|
||||
line = toAsciiFallback(text);
|
||||
if (line === null) return false;
|
||||
}
|
||||
|
||||
try {
|
||||
stream.write(`${line}\n`);
|
||||
return true;
|
||||
} catch {
|
||||
// A synchronous throw (EBADF on a closed handle) never reaches the
|
||||
// 'error' listener, so degrade here too.
|
||||
degrade(guard);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createLineWriter, toAsciiFallback } from "./console-log.mjs";
|
||||
|
||||
/** Stand-in for process.stderr: an EventEmitter with a write() we can steer. */
|
||||
function fakeStream() {
|
||||
const stream = new EventEmitter();
|
||||
stream.written = [];
|
||||
stream.throwOnWrite = false;
|
||||
stream.write = (chunk) => {
|
||||
if (stream.throwOnWrite) throw new Error("EBADF");
|
||||
stream.written.push(chunk);
|
||||
return true;
|
||||
};
|
||||
return stream;
|
||||
}
|
||||
|
||||
describe("toAsciiFallback", () => {
|
||||
it("leaves ASCII lines exactly as they are", () => {
|
||||
const line = " [binary] better-sqlite3: OK (loads under v22.23.2, ABI 127)";
|
||||
expect(toAsciiFallback(line)).toBe(line);
|
||||
expect(toAsciiFallback("")).toBe("");
|
||||
});
|
||||
|
||||
it("keeps the English half of the line that crashed setup in #152", () => {
|
||||
expect(
|
||||
toAsciiFallback(
|
||||
" [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)",
|
||||
),
|
||||
).toBe(" [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB)");
|
||||
});
|
||||
|
||||
it("drops parentheses and separators left stranded by the removed text", () => {
|
||||
expect(
|
||||
toAsciiFallback(" [binary] better-sqlite3: falling back — 'npm rebuild' (可能需要几分钟)"),
|
||||
).toBe(" [binary] better-sqlite3: falling back - 'npm rebuild'");
|
||||
expect(
|
||||
toAsciiFallback(" Windows: npm install --global windows-build-tools (或安装 VS Build Tools)"),
|
||||
).toBe(" Windows: npm install --global windows-build-tools (VS Build Tools)");
|
||||
});
|
||||
|
||||
it("drops a Chinese-only line, which always has an English twin beside it", () => {
|
||||
expect(toAsciiFallback("必需的原生模块不可用,机器人无法启动 —— 请查看上面的错误信息。")).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves the indentation the summary is aligned on, and drops the dangling dash", () => {
|
||||
expect(toAsciiFallback(" - better-sqlite3 FAILED — 编译失败")).toBe(
|
||||
" - better-sqlite3 FAILED",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a separator that belongs to the English half", () => {
|
||||
expect(toAsciiFallback("Summary — Node v22.0.0 / ABI 127 / win32-x64:")).toBe(
|
||||
"Summary - Node v22.0.0 / ABI 127 / win32-x64:",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLineWriter", () => {
|
||||
it("appends a newline and reports the write", () => {
|
||||
const stream = fakeStream();
|
||||
expect(createLineWriter(stream)("hello")).toBe(true);
|
||||
expect(stream.written).toEqual(["hello\n"]);
|
||||
});
|
||||
|
||||
it("survives the EIO that killed setup: an 'error' event must not throw", () => {
|
||||
const stream = fakeStream();
|
||||
createLineWriter(stream);
|
||||
expect(stream.listenerCount("error")).toBe(1);
|
||||
expect(() => stream.emit("error", Object.assign(new Error("write EIO"), { code: "EIO" }))).not.toThrow();
|
||||
});
|
||||
|
||||
it("falls back to ASCII once the console has refused a line", () => {
|
||||
const stream = fakeStream();
|
||||
const write = createLineWriter(stream);
|
||||
write(" [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)");
|
||||
stream.emit("error", new Error("write EIO"));
|
||||
write(" [binary] GET https://cdn/opus.tar.gz (~1 MB, 这一步比较慢,请耐心等待)");
|
||||
expect(stream.written).toEqual([
|
||||
" [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)\n",
|
||||
" [binary] GET https://cdn/opus.tar.gz (~1 MB)\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("goes quiet after a second failure rather than retrying a dead stream", () => {
|
||||
const stream = fakeStream();
|
||||
const write = createLineWriter(stream);
|
||||
stream.emit("error", new Error("write EIO"));
|
||||
stream.emit("error", new Error("write EPIPE"));
|
||||
expect(write("anything at all")).toBe(false);
|
||||
expect(stream.written).toEqual([]);
|
||||
});
|
||||
|
||||
it("degrades on a synchronous throw too, which never reaches the listener", () => {
|
||||
const stream = fakeStream();
|
||||
const write = createLineWriter(stream);
|
||||
stream.throwOnWrite = true;
|
||||
expect(write(" [binary] 下载中 downloading")).toBe(false);
|
||||
stream.throwOnWrite = false;
|
||||
write(" [binary] 下载中 downloading");
|
||||
expect(stream.written).toEqual([" [binary] downloading\n"]);
|
||||
});
|
||||
|
||||
it("degrades each stream on its own, so setup.log keeps the full transcript", () => {
|
||||
const console_ = fakeStream();
|
||||
const logFile = fakeStream();
|
||||
const writeConsole = createLineWriter(console_);
|
||||
const writeLog = createLineWriter(logFile);
|
||||
console_.emit("error", new Error("write EIO"));
|
||||
|
||||
const line = " [binary] ffmpeg-static: 下载完成 done";
|
||||
writeConsole(line);
|
||||
writeLog(line);
|
||||
|
||||
expect(console_.written).toEqual([" [binary] ffmpeg-static: done\n"]);
|
||||
expect(logFile.written).toEqual([`${line}\n`]);
|
||||
});
|
||||
|
||||
it("never installs a second listener for a stream that already has a writer", () => {
|
||||
const stream = fakeStream();
|
||||
createLineWriter(stream);
|
||||
createLineWriter(stream);
|
||||
expect(stream.listenerCount("error")).toBe(1);
|
||||
});
|
||||
|
||||
it("still guards a stream that is not an EventEmitter", () => {
|
||||
const stream = { write: vi.fn(() => { throw new Error("EBADF"); }) };
|
||||
const write = createLineWriter(stream);
|
||||
expect(() => write("line")).not.toThrow();
|
||||
expect(write("line")).toBe(false);
|
||||
});
|
||||
});
|
||||
+11
-9
@@ -11,7 +11,7 @@ title TSMusicBot Setup
|
||||
:: ============================================================
|
||||
|
||||
set "SCRIPT_VERSION=2.2"
|
||||
set "MIN_NODE_MAJOR=20"
|
||||
set "MIN_NODE_MAJOR=22"
|
||||
:: Newest Node major this project is regularly tested against. Anything above
|
||||
:: still works, it just may have no prebuilt addons and fall back to a source build.
|
||||
set "TESTED_NODE_MAJOR=22"
|
||||
@@ -67,13 +67,15 @@ for /f "tokens=1 delims=v." %%a in ("%NODE_VER%") do set "NODE_MAJOR=%%a"
|
||||
call :log "Node.js version: %NODE_VER%"
|
||||
echo [OK] Node.js found: %NODE_VER%
|
||||
|
||||
:: The supported floor is not just a major version, so let node decide:
|
||||
:: @honeybbq/teamspeak-client needs >=20.19, @sansenjian/qq-music-api needs
|
||||
:: >=20.17 / >=22.9, and the odd majors (21 / 23) are excluded by
|
||||
:: better-sqlite3 and vitest. Keep this in sync with "engines" in package.json.
|
||||
node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)"
|
||||
:: The supported floor is not just a major version, so let node decide.
|
||||
:: Node 20 was dropped: better-sqlite3 ships no prebuilt binary for its ABI
|
||||
:: (115) since 12.10.0, so every Node 20 install needed Python and a C++
|
||||
:: toolchain just to get off the ground (issue #152). The odd majors (21 /
|
||||
:: 23) are excluded by better-sqlite3 and vitest.
|
||||
:: Keep this in sync with "engines" in package.json.
|
||||
node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)"
|
||||
if errorlevel 1 (
|
||||
call :error "Node.js %NODE_VER% is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS."
|
||||
call :error "Node.js %NODE_VER% is not supported. Use Node 22.12+ LTS or newer."
|
||||
echo Download: https://nodejs.org/ or https://nodejs.cn/
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -86,10 +88,10 @@ if errorlevel 1 (
|
||||
:: characters and starts eating the "echo " prefix of following lines.
|
||||
:: Bilingual guidance lives in the Node scripts, which print UTF-8 reliably.
|
||||
if %NODE_MAJOR% GTR %TESTED_NODE_MAJOR% (
|
||||
echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 20 / Node 22.
|
||||
echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 22.
|
||||
echo Newer Node majors may have no prebuilt opus / better-sqlite3,
|
||||
echo so setup falls back to a source build - slower, needs C++ build tools.
|
||||
echo Recommended: Node 20 LTS or Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/
|
||||
echo Recommended: Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/
|
||||
echo This is only a warning; setup still builds the binaries for %NODE_VER%.
|
||||
call :log "[WARN] Node major %NODE_MAJOR% is newer than tested LTS %TESTED_NODE_MAJOR%"
|
||||
)
|
||||
|
||||
+8
-6
@@ -32,16 +32,18 @@ echo "[OK] Node.js $(node -v)"
|
||||
TESTED_NODE_MAJOR=22
|
||||
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
||||
|
||||
# The floor is not just a major version, so let node decide: @honeybbq/teamspeak-client
|
||||
# needs >=20.19, @sansenjian/qq-music-api needs >=20.17 / >=22.9, and the odd majors
|
||||
# (21 / 23) are excluded by better-sqlite3 and vitest. Keep in sync with package.json "engines".
|
||||
if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then
|
||||
echo "[ERROR] Node.js $(node -v) is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS."
|
||||
# The floor is not just a major version, so let node decide. Node 20 was dropped:
|
||||
# better-sqlite3 ships no prebuilt binary for its ABI (115) since 12.10.0, so every
|
||||
# Node 20 install needed Python and a C++ toolchain just to get off the ground
|
||||
# (issue #152). The odd majors (21 / 23) are excluded by better-sqlite3 and vitest.
|
||||
# Keep in sync with package.json "engines".
|
||||
if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then
|
||||
echo "[ERROR] Node.js $(node -v) is not supported. Use Node 22.12+ LTS or newer."
|
||||
echo " https://nodejs.org/ | https://nodejs.cn/"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$NODE_MAJOR" -gt "$TESTED_NODE_MAJOR" ]; then
|
||||
echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 20 / Node 22)."
|
||||
echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 22)."
|
||||
echo " 新版 Node 可能没有现成的 opus / better-sqlite3 预编译包,"
|
||||
echo " 安装时会自动改用源码编译,需要 C/C++ 构建工具,速度较慢。"
|
||||
echo " This is only a warning - setup builds the binaries for $(node -v) either way."
|
||||
|
||||
Reference in new issue
Block a user