* Execute a remote or local speedtest command safely using spawn/execFile * to prevent shell injection via user-supplied server config fields.
(
command: string,
args: string[],
options: { timeout: number; env?: NodeJS.ProcessEnv },
label: string
)
| 80 | /** |
| 81 | * Execute a remote or local speedtest command safely using spawn/execFile |
| 82 | * to prevent shell injection via user-supplied server config fields. |
| 83 | */ |
| 84 | async function runSpeedtestCommand( |
| 85 | command: string, |
| 86 | args: string[], |
| 87 | options: { timeout: number; env?: NodeJS.ProcessEnv }, |
| 88 | label: string |
| 89 | ): Promise<{ stdout: string; stderr: string }> { |
| 90 | return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { |
| 91 | const child = spawn(command, args, { |
| 92 | timeout: options.timeout, |
| 93 | env: options.env, |
| 94 | killSignal: "SIGKILL" as const, |
| 95 | shell: false, |
| 96 | }); |
| 97 | let stdout = ""; |
| 98 | let stderr = ""; |
| 99 | child.stdout?.on("data", (data: Buffer) => { stdout += data.toString(); }); |
| 100 | child.stderr?.on("data", (data: Buffer) => { stderr += data.toString(); }); |
| 101 | child.on("error", (err: Error) => { reject(err); }); |
| 102 | child.on("close", (code: number | null) => { |
| 103 | if (code === 0) { |
| 104 | resolve({ stdout, stderr }); |
| 105 | } else { |
| 106 | const err: any = new Error(`${label} exited with code ${code}: ${stderr || stdout}`); |
| 107 | err.stderr = stderr; |
| 108 | reject(err); |
| 109 | } |
| 110 | }); |
| 111 | // Force-kill on timeout (spawn timeout only signals, doesn't kill by default) |
| 112 | const timer = setTimeout(() => { |
| 113 | child.kill("SIGKILL"); |
| 114 | reject(new Error(`${label} timed out after ${options.timeout}ms`)); |
| 115 | }, options.timeout); |
| 116 | child.on("close", () => { clearTimeout(timer); }); |
| 117 | }); |
| 118 | } |
| 119 |