-
Notifications
You must be signed in to change notification settings - Fork 78
fix(media): require an executable when resolving ffmpeg #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| import { spawn } from "node:child_process"; | ||||||
| import { createHash } from "node:crypto"; | ||||||
| import { existsSync } from "node:fs"; | ||||||
| import { accessSync, constants as fsConstants, statSync } from "node:fs"; | ||||||
| import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; | ||||||
| import path from "node:path"; | ||||||
| import { app } from "electron"; | ||||||
|
|
@@ -98,10 +98,46 @@ export function ffmpegCandidates(here: string = process.cwd()): string[] { | |||||
|
|
||||||
| let cachedFfmpeg: string | null | undefined; | ||||||
|
|
||||||
| /** First candidate that exists, or null when none does (callers fall back). */ | ||||||
| /** | ||||||
| * Whether a candidate is something that can actually be run. | ||||||
| * | ||||||
| * EXISTENCE IS NOT ENOUGH, and the difference is not academic. `existsSync` was | ||||||
| * the test here, and it answers true for a DIRECTORY: on a Linux dev machine | ||||||
| * `electron/native/bin/<tag>/ffmpeg` is a folder holding the shared libraries | ||||||
| * (`libavcodec.so.62` and friends) rather than the binary, so resolution picked | ||||||
| * the folder, every later candidate was skipped, and the failure surfaced much | ||||||
| * later as `spawn … EACCES` — a message that blames permissions rather than | ||||||
| * saying the wrong candidate was chosen. | ||||||
| * | ||||||
| * Every failure mode is swallowed on purpose. A candidate that is absent, not a | ||||||
| * regular file, or not executable is simply not this one; throwing out of | ||||||
| * resolution would let a single bad path deny a later, working one. | ||||||
| * | ||||||
| * `X_OK` and not `X_OK | R_OK`: executing a binary needs the execute bit, not | ||||||
| * the read bit, so an install shipped `--x` is legitimate and must not be | ||||||
| * refused. On Windows `X_OK` is not enforced at all — there the `isFile` check | ||||||
| * is the whole guard, which is enough for the failure this exists to stop, | ||||||
| * since `ffmpegCandidates` only ever proposes `.exe` names of its own. | ||||||
| */ | ||||||
| function isExecutableFile(candidate: string): boolean { | ||||||
| try { | ||||||
| if (!statSync(candidate).isFile()) { | ||||||
| return false; | ||||||
| } | ||||||
| accessSync(candidate, fsConstants.X_OK); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'audioPeaks\.ts$' . || true
echo "== relevant file outline/lines =="
file=$(fd 'audioPeaks\.ts$' . | head -n 1)
if [ -n "$file" ]; then
wc -l "$file"
sed -n '1,180p' "$file" | cat -n
fi
echo "== search access sync usage =="
rg -n "accessSync|candidate|X_OK|R_OK|READABLE|audios\|mediaFiles" -S . --glob '!node_modules' --glob '!dist' --glob '!build' || trueRepository: getopenscreen/openscreen Length of output: 50380 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== inspect related path checks =="
for f in \
electron/stt/whisperServer.ts \
electron/ipc/handlers.ts \
electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.ts \
electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts
do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n -C 4 "accessSync|fsConstants" "$f" || true
fi
done
echo "== behavioral probe for access flags on current sandbox tmp =="
python3 - <<'PY'
import os, stat, tempfile
d = tempfile.mkdtemp(prefix="audiosync_")
p = os.path.join(d, "x")
with open(p, "wb") as f:
f.write(b"ok")
for mode in [(stat.S_IRUSR | stat.S_IXUSR), (stat.S_IRUSR,), (stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)]:
os.chmod(p, mode)
print(f"{oct(mode):14} -> R_OK={bool(os.access(p, os.R_OK))} X_OK={bool(os.access(p, os.X_OK))}")
print("tmpdir", d)
PY
rm -rf /tmp /*audiosync_*Repository: getopenscreen/openscreen Length of output: 4595 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== behavioral probe for access flags =="
python3 - <<'PY'
import os, stat, tempfile
d = tempfile.mkdtemp(prefix="audiosync_")
p = os.path.join(d, "x")
with open(p, "wb") as f:
f.write(b"ok")
for mode in [(stat.S_IRUSR | stat.S_IXUSR), (stat.S_IRUSR,), (stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)]:
os.chmod(p, mode)
print(f"{oct(mode):14} -> R_OK={bool(os.access(p, os.R_OK))} X_OK={bool(os.access(p, os.X_OK))}")
try:
os.chmod(d, 0o500)
q = os.path.join(d, "y")
with open(q, "wb") as f:
f.write(b"ok")
except Exception as e:
print(type(e).__name__, str(e))
else:
os.chmod(q, 0o000)
print("directory_with_nothing_ ->", f"R_OK={bool(os.access(q, os.R_OK))} X_OK={bool(os.access(q, os.X_OK))}")
os.unlink(q)
PY
echo "== permissions probe =="
node - <<'JS'
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const d = fs.mkdtempSync(path.join(os.tmpdir(), "audiosync-"));
const p = path.join(d, "x");
fs.writeFileSync(p, "ok");
for (const mode of ["0555", "0444", "0755"]) {
fs.chmodSync(p, mode);
console.log(mode, {
read: fs.existsSync(p) && fs.accessSync(p, fs.constants.R_OK) === undefined,
exec: fs.existsSync(p) && fs.accessSync(p, fs.constants.X_OK) === undefined,
});
}
fs.rmSync(d, { recursive: true, force: true });
JSRepository: getopenscreen/openscreen Length of output: 364 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Node permissions probe =="
node - <<'JS'
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const d = fs.mkdtempSync(path.join(os.tmpdir(), "audiosync-"));
const p = path.join(d, "x");
fs.writeFileSync(p, "ok");
for (const mode of ["0555", "0444", "0755"]) {
try {
fs.chmodSync(p, mode);
const readOk = fs.accessSync(p, fs.constants.R_OK) === undefined;
const execOk = fs.accessSync(p, fs.constants.X_OK) === undefined;
console.log(mode, { read: readOk, exec: execOk });
} catch (error) {
console.log(mode, `error=${error.code}: ${error.message}`);
}
}
try {
fs.chmodSync(d, "0555");
const q = path.join(d, "y");
fs.writeFileSync(q, "ok");
fs.chmodSync(q, "0000");
console.log("dir_with_irregular_file", {
read: fs.accessSync(q, fs.constants.R_OK) === undefined,
exec: fs.accessSync(q, fs.constants.X_OK) === undefined,
});
} catch (error) {
console.log("dir_with_irregular_file", `error=${error.code}: ${error.message}`);
}
fs.rmSync(d, { recursive: true, force: true });
JSRepository: getopenscreen/openscreen Length of output: 1231 Require read access before selecting the ffmpeg candidate.
Proposed fix- accessSync(candidate, fsConstants.X_OK);
+ accessSync(candidate, fsConstants.R_OK | fsConstants.X_OK);📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.0)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||
| return true; | ||||||
| } catch { | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * First candidate that is an executable file, or null when none is (callers | ||||||
| * fall back). | ||||||
| */ | ||||||
| export function resolveFfmpeg(here?: string): string | null { | ||||||
| if (cachedFfmpeg !== undefined && here === undefined) return cachedFfmpeg; | ||||||
| const found = ffmpegCandidates(here).find((p) => existsSync(p)) ?? null; | ||||||
| const found = ffmpegCandidates(here).find(isExecutableFile) ?? null; | ||||||
| if (here === undefined) cachedFfmpeg = found; | ||||||
| return found; | ||||||
| } | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.