fix(mounts): use bun shell instead of execFile

This commit is contained in:
Nicolas Meienberger
2025-11-10 06:52:14 +01:00
parent e9eeda304b
commit 2b0fea9645
7 changed files with 49 additions and 75 deletions

View File

@@ -6,9 +6,8 @@ await Bun.build({
sourcemap: true,
minify: {
whitespace: true,
identifiers: true,
identifiers: false,
syntax: true,
keepNames: true,
},
external: ["ssh2"],
});

View File

@@ -4,7 +4,6 @@ import { logger } from "../utils/logger";
export type SystemCapabilities = {
docker: boolean;
hostProc: boolean;
};
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
@@ -29,7 +28,6 @@ export async function getCapabilities(): Promise<SystemCapabilities> {
async function detectCapabilities(): Promise<SystemCapabilities> {
return {
docker: await detectDocker(),
hostProc: await detectHostProc(),
};
}
@@ -55,23 +53,3 @@ async function detectDocker(): Promise<boolean> {
return false;
}
}
/**
* Checks if host proc is available by attempting to access /host/proc/1/ns/mnt
* This allows using nsenter to execute mount commands in the host namespace
*/
async function detectHostProc(): Promise<boolean> {
try {
await fs.access("/host/proc/1/ns/mnt");
logger.info("Host proc capability: enabled");
return true;
} catch (_) {
logger.warn(
"Host proc capability: disabled. " +
"To enable: mount /proc:/host/proc:ro in docker-compose.yml. " +
"Mounts will be executed in container namespace instead of host namespace.",
);
return false;
}
}

View File

@@ -1,31 +1,14 @@
import { execFile as execFileCb } from "node:child_process";
import * as fs from "node:fs/promises";
import * as npath from "node:path";
import { promisify } from "node:util";
import { getCapabilities } from "../../../core/capabilities";
import { OPERATION_TIMEOUT } from "../../../core/constants";
import { toMessage } from "../../../utils/errors";
import { logger } from "../../../utils/logger";
const execFile = promisify(execFileCb);
import { $ } from "bun";
export const executeMount = async (args: string[]): Promise<void> => {
const capabilities = await getCapabilities();
let stderr: string | undefined;
if (capabilities.hostProc) {
const result = await execFile("nsenter", ["--mount=/host/proc/1/ns/mnt", "mount", ...args], {
timeout: OPERATION_TIMEOUT,
maxBuffer: 1024 * 1024,
});
stderr = result.stderr;
} else {
const result = await execFile("mount", args, {
timeout: OPERATION_TIMEOUT,
maxBuffer: 1024 * 1024,
});
stderr = result.stderr;
}
const result = await $`mount ${args}`.nothrow();
stderr = result.stderr.toString();
if (stderr?.trim()) {
logger.warn(stderr.trim());
@@ -33,22 +16,10 @@ export const executeMount = async (args: string[]): Promise<void> => {
};
export const executeUnmount = async (path: string): Promise<void> => {
const capabilities = await getCapabilities();
let stderr: string | undefined;
if (capabilities.hostProc) {
const result = await execFile("nsenter", ["--mount=/host/proc/1/ns/mnt", "umount", "-l", "-f", path], {
timeout: OPERATION_TIMEOUT,
maxBuffer: 1024 * 1024,
});
stderr = result.stderr;
} else {
const result = await execFile("umount", ["-l", "-f", path], {
timeout: OPERATION_TIMEOUT,
maxBuffer: 1024 * 1024,
});
stderr = result.stderr;
}
const result = await $`umount -l -f ${path}`.nothrow();
stderr = result.stderr.toString();
if (stderr?.trim()) {
logger.warn(stderr.trim());

View File

@@ -189,7 +189,7 @@ const backup = async (
let stdout = "";
await safeSpawn({
const res = await safeSpawn({
command: "restic",
args,
env,
@@ -210,6 +210,11 @@ const backup = async (
},
});
if (res.exitCode !== 0) {
logger.error(`Restic backup failed: ${res.stderr}`);
throw new Error(`Restic backup failed: ${res.stderr}`);
}
const lastLine = stdout.trim();
const resSummary = JSON.parse(lastLine ?? "{}");

View File

@@ -12,10 +12,19 @@ interface Params {
finally?: () => Promise<void> | void;
}
type SpawnResult = {
exitCode: number;
stdout: string;
stderr: string;
};
export const safeSpawn = (params: Params) => {
const { command, args, env = {}, signal, ...callbacks } = params;
return new Promise((resolve, reject) => {
return new Promise<SpawnResult>((resolve) => {
let stdoutData = "";
let stderrData = "";
const child = spawn(command, args, {
env: { ...process.env, ...env },
signal: signal,
@@ -24,12 +33,16 @@ export const safeSpawn = (params: Params) => {
child.stdout.on("data", (data) => {
if (callbacks.onStdout) {
callbacks.onStdout(data.toString());
} else {
stdoutData += data.toString();
}
});
child.stderr.on("data", (data) => {
if (callbacks.onStderr) {
callbacks.onStderr(data.toString());
} else {
stderrData += data.toString();
}
});
@@ -40,7 +53,12 @@ export const safeSpawn = (params: Params) => {
if (callbacks.finally) {
await callbacks.finally();
}
reject(error);
resolve({
exitCode: -1,
stdout: stdoutData,
stderr: stderrData,
});
});
child.on("close", async (code) => {
@@ -50,7 +68,12 @@ export const safeSpawn = (params: Params) => {
if (callbacks.finally) {
await callbacks.finally();
}
resolve(code);
resolve({
exitCode: code === null ? -1 : code,
stdout: stdoutData,
stderr: stderrData,
});
});
});
};