import { spawn } from "node:child_process" ;
import {
materializeWindowsSpawnProgram,
resolveWindowsSpawnProgram,
} from "../../plugin-sdk/windows-spawn.js" ;
export type CliSpawnInvocation = {
command: string;
argv: string[];
shell?: boolean ;
windowsHide?: boolean ;
};
export type QmdBinaryAvailability = {
available: boolean ;
error?: string;
};
export function resolveCliSpawnInvocation(params: {
command: string;
args: string[];
env: NodeJS.ProcessEnv;
packageName: string;
}): CliSpawnInvocation {
const program = resolveWindowsSpawnProgram({
command: params.command,
platform: process.platform,
env: params.env,
execPath: process.execPath,
packageName: params.packageName,
allowShellFallback: false ,
});
return materializeWindowsSpawnProgram(program, params.args);
}
export async function checkQmdBinaryAvailability(params: {
command: string;
env: NodeJS.ProcessEnv;
cwd?: string;
timeoutMs?: number;
}): Promise<QmdBinaryAvailability> {
let spawnInvocation: CliSpawnInvocation;
try {
spawnInvocation = resolveCliSpawnInvocation({
command: params.command,
args: [],
env: params.env,
packageName: "qmd" ,
});
} catch (err) {
return { available: false , error: formatQmdAvailabilityError(err) };
}
return await new Promise((resolve) => {
let settled = false ;
let didSpawn = false ;
const finish = (result: QmdBinaryAvailability) => {
if (settled) {
return ;
}
settled = true ;
if (timer) {
clearTimeout(timer);
}
resolve(result);
};
const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
env: params.env,
cwd: params.cwd ?? process.cwd(),
shell: spawnInvocation.shell,
windowsHide: spawnInvocation.windowsHide,
stdio: "ignore" ,
});
const timer = setTimeout(() => {
child.kill("SIGKILL" );
finish({
available: false ,
error: `spawn ${params.command} timed out after ${params.timeoutMs ?? 2 _000 }ms`,
});
}, params.timeoutMs ?? 2 _000 );
child.once("error" , (err) => {
finish({ available: false , error: formatQmdAvailabilityError(err) });
});
child.once("spawn" , () => {
didSpawn = true ;
child.kill();
finish({ available: true });
});
child.once("close" , () => {
if (!didSpawn) {
return ;
}
finish({ available: true });
});
});
}
export async function runCliCommand(params: {
commandSummary: string;
spawnInvocation: CliSpawnInvocation;
env: NodeJS.ProcessEnv;
cwd: string;
timeoutMs?: number;
maxOutputChars: number;
discardStdout?: boolean ;
}): Promise<{ stdout: string; stderr: string }> {
return await new Promise((resolve, reject) => {
const child = spawn(params.spawnInvocation.command, params.spawnInvocation.argv, {
env: params.env,
cwd: params.cwd,
shell: params.spawnInvocation.shell,
windowsHide: params.spawnInvocation.windowsHide,
});
let stdout = "" ;
let stderr = "" ;
let stdoutTruncated = false ;
let stderrTruncated = false ;
const discardStdout = params.discardStdout === true ;
const timer = params.timeoutMs
? setTimeout(() => {
child.kill("SIGKILL" );
reject(new Error(`${params.commandSummary} timed out after ${params.timeoutMs}ms`));
}, params.timeoutMs)
: null ;
child.stdout.on("data" , (data) => {
if (discardStdout) {
return ;
}
const next = appendOutputWithCap(stdout, data.toString("utf8" ), params.maxOutputChars);
stdout = next.text;
stdoutTruncated = stdoutTruncated || next.truncated;
});
child.stderr.on("data" , (data) => {
const next = appendOutputWithCap(stderr, data.toString("utf8" ), params.maxOutputChars);
stderr = next.text;
stderrTruncated = stderrTruncated || next.truncated;
});
child.on("error" , (err) => {
if (timer) {
clearTimeout(timer);
}
reject(err);
});
child.on("close" , (code) => {
if (timer) {
clearTimeout(timer);
}
if (!discardStdout && (stdoutTruncated || stderrTruncated)) {
reject(
new Error(
`${params.commandSummary} produced too much output (limit ${params.maxOutputChars} chars)`,
),
);
return ;
}
if (code === 0 ) {
resolve({ stdout, stderr });
} else {
reject(new Error(`${params.commandSummary} failed (code ${code}): ${stderr || stdout}`));
}
});
});
}
function appendOutputWithCap(
current: string,
chunk: string,
maxChars: number,
): { text: string; truncated: boolean } {
const appended = current + chunk;
if (appended.length <= maxChars) {
return { text: appended, truncated: false };
}
return { text: appended.slice(-maxChars), truncated: true };
}
function formatQmdAvailabilityError(err: unknown): string {
if (err instanceof Error && err.message) {
return err.message;
}
return String(err);
}
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland