fix(spotify): PATH-aware binary presence detection (bin/ or PATH) [whole-branch I3,m1]

rustPresent/goPresent and getBackendInfo used existsSync(findX()), which for a
bare PATH command name resolves against cwd, not $PATH — so a scoop/choco/cargo/
apt install was invisible and Spotify was gated off. Add a sync PATH-aware
resolveExecutable() + isLibrespotPresent()/isGoLibrespotPresent() in binary.ts
and route controller.ts and web/server.ts through them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-07-03 01:45:48 +08:00
1 parent b4c3cc0539
commit 399cf0cf41
5 files changed
+211 -28

No files matched your search

+53 -1
View File
@@ -2,12 +2,44 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { dirname, join, delimiter } from "node:path";
const execFileAsync = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Resolve a command to an existing absolute path, SYNCHRONOUSLY. A candidate
* that already contains a path separator (a project bin/ path) is checked
* directly; a BARE command name (e.g. "librespot" / "go-librespot") is searched
* across the $PATH directories — with PATHEXT extensions appended on win32 — so
* a scoop/choco/cargo/apt PATH install resolves. Returns the resolved path, or
* null when nothing exists.
*
* This is the sync counterpart to checkLibrespotAvailable()/…GoLibrespot… and
* is what the hot-path presence gates (chooseBackend/isAvailable) rely on: a
* bare name must NOT be handed to existsSync() directly, since existsSync
* resolves it against process.cwd() rather than PATH (Bug I3/m1).
*/
export function resolveExecutable(cmd: string): string | null {
if (cmd.includes("/") || cmd.includes("\\")) {
return existsSync(cmd) ? cmd : null;
}
const dirs = (process.env.PATH || "").split(delimiter).filter(Boolean);
const exts =
process.platform === "win32"
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
: [""];
for (const dir of dirs) {
for (const ext of exts) {
const hasExt = ext && cmd.toLowerCase().endsWith(ext.toLowerCase());
const full = join(dir, hasExt ? cmd : cmd + ext);
if (existsSync(full)) return full;
}
}
return null;
}
/**
* True only on Linux. go-librespot ships Linux-only release binaries and the
* sidecar relies on a POSIX FIFO (mkfifo), so the Spotify audio backend is
@@ -44,6 +76,16 @@ export function findGoLibrespot(): string {
return pickGoLibrespotPath([binPath, "go-librespot"], existsSync);
}
/**
* SYNCHRONOUS presence gate for go-librespot: supported platform (linux) AND
* the resolved binary (project bin/ OR a PATH install) actually exists. This is
* what chooseBackend()/isAvailable() must use — NOT existsSync(findGoLibrespot())
* which cannot see a bare PATH name (Bug I3/m1).
*/
export function isGoLibrespotPresent(): boolean {
return isGoLibrespotSupported() && resolveExecutable(findGoLibrespot()) !== null;
}
// Injectable `--version` probe. Defaults to the real execFile call; tests
// override it so checkGoLibrespotAvailable() needs no real binary. Keeps the
// public checkGoLibrespotAvailable() signature param-free per the contract.
@@ -143,6 +185,16 @@ export function findLibrespot(): string {
return pickLibrespotPath([binExe, binBare, exe], existsSync);
}
/**
* SYNCHRONOUS presence gate for Rust librespot: supported everywhere AND the
* resolved binary (project bin/ OR a scoop/choco/cargo PATH install) actually
* exists. This is what chooseBackend()/isAvailable() must use — NOT
* existsSync(findLibrespot()) which cannot see a bare PATH name (Bug I3/m1).
*/
export function isLibrespotPresent(): boolean {
return isRustLibrespotSupported() && resolveExecutable(findLibrespot()) !== null;
}
// Injectable `--version` probe. Defaults to the real execFile call; tests
// override it so checkLibrespotAvailable() needs no real binary. Keeps the
// public checkLibrespotAvailable() signature param-free per the contract.