import child_process from "node:child_process"; import { platform } from "node:process"; import fs from "node:fs"; import pathModule from "node:path"; import { fileURLToPath } from "node:url"; import { Result$Ok, Result$Error, Result$isOk, Result$Ok$0, BitArray, BitArray$BitArray, } from "./gleam.mjs"; import * as List from "../gleam_stdlib/gleam/list.mjs"; import { Option$isSome, Option$Some$0 } from "../gleam_stdlib/gleam/option.mjs"; import { StartError$FileNotFound, StartError$FileNotExecutable, StartError$RuntimeLimitReached, Output$Output, } from "../child_process/child_process.mjs"; import { Mode$isInherit, Mode$isNull, Mode$isCapture, Mode$Capture$capture_stderr, Stdio$Stdio$mode, Stdio$Stdio$initial, Stdio$Stdio$on_data, Stdio$Stdio$on_exit, Os$Windows, Os$Macos, Os$Linux, } from "../child_process/child_process/internal.mjs"; const isWindows = platform === "win32"; const isMacos = platform === "darwin"; function toArray(list) { const arr = []; List.each(list, (item) => arr.push(item)); return arr; } // Find an executable in PATH (based on node-which implementation) export function find_executable(cmd) { const { pathEnv, pathExt } = getPathInfo(cmd); for (const envPart of pathEnv) { const p = getPathPart(envPart, cmd); for (const ext of pathExt) { const withExt = p + ext; if (isExecutable(withExt)) { return Result$Ok(withExt); } } } return Result$Error(undefined); } const textDecoder = new TextDecoder(); export function bits_to_string(binary) { // TODO: replace once $data is stable if (binary.bitSize % 8 !== 0 || binary.bitOffset !== 0) { throw new Error("unaligned bitarray"); } return textDecoder.decode(binary.rawBuffer ?? binary.buffer); } const rSlash = /[\\\\\/]/; const rRel = new RegExp(`^\\.${rSlash.source}`); // Get path part, handling quotes and relative paths function getPathPart(raw, cmd) { const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw; const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : ""; return prefix + pathModule.join(pathPart, cmd); } // Get path information similar to node-which function getPathInfo(cmd) { const path = process.env.PATH || ""; const pathExt = process.env.PATHEXT; const delimiter = pathModule.delimiter; // If it has a slash, just check the file itself const pathEnv = cmd.match(rSlash) ? [""] : [ // Windows always checks the cwd first ...(isWindows ? [process.cwd()] : []), ...path.split(delimiter), ]; if (isWindows) { const pathExtParts = pathExt ? pathExt.split(delimiter).flatMap((item) => [item, item.toLowerCase()]) : [".EXE", ".exe", ".CMD", ".cmd", "BAT", ".bat", ".COM", ".com"]; if (cmd.includes(".") && pathExtParts[0] !== "") { pathExt.unshift(""); } return { pathEnv, pathExt: pathExtParts }; } return { pathEnv, pathExt: [""] }; } // Check if a file is executable function isExecutable(path) { const stats = fs.statSync(path, { throwIfNoEntry: false }); if (!stats) { return false; } if (!stats.isFile()) { return false; } if (isWindows) { // On Windows, if the file exists and is a regular file, it's executable // if its extension is inside the PATHEXT variable. const pathExt = process.env.PATHEXT; const pathExtParts = pathExt ? pathExt.split(delimiter).flatMap((item) => [item, item.toLowerCase()]) : [".EXE", ".exe", ".CMD", ".cmd", "BAT", ".bat", ".COM", ".com"]; for (const ext of pathExtParts) { if (path.endsWith(ext)) { return true; } } return false; } // On Unix, check execute permission if (stats.mode & 0o001) { return true; } // check user or group permissions if (!process.getuid || !process.getgid) { return false; } const uid = process.getuid(); if (stats.mode & 0o100 && stats.uid === uid) { return true; } const gid = process.getgid(); const groups = process.getgroups?.() ?? []; if (stats.mode & 0x010 && (stats.gid === gid || groups.includes(stats.gid))) { return true; } if (stats.mode & 0o110 && uid === 0) { return true; } return false; } // Get the native newline character for the operating system export function newline() { return isWindows ? "\r\n" : "\n"; } export function run(executable, args, env, cwd, mode) { const options = { env: getEnv(env), cwd: Option$isSome(cwd) ? Option$Some$0(cwd) : undefined, encoding: Mode$isInherit(mode) ? "buffer" : "utf8", stdio: Mode$isInherit(mode) ? "inherit" : ["ignore", "pipe", "pipe"], windowsHide: true, maxBuffer: Infinity, }; const result = child_process.spawnSync(executable, toArray(args), options); if (result.error) { switch (result.error.code) { case "ENOENT": return Result$Error(StartError$FileNotFound(executable)); case "EACESS": return Result$Error(StartError$FileNotExecutable(executable)); case "ENOMEM": return Result$Error(StartError$OutOfMemory()); case "EAGAIN": return Result$Error(StartError$OsProcessLimitReached()); case "ENFILE": return Result$Error(StartError$OsFileLimitReached()); case "ENAMETOOLONG": return Result$Error(StartError$CommandTooLong()); case "EMFILE": return Result$Error(StartError$NotEnoughFileDescriptors()); default: return Result$Error(StartError$RuntimeLimitReached()); } } // I have not found a good way to mix stdout and stderr in this case. const output = (result.stdout || "") + (result.stderr || ""); const status_code = result.status ?? -1; return Result$Ok(Output$Output(status_code, output)); } export function spawn(executable, args, env, cwd, stdio) { const result = spawn_raw(executable, args, env, cwd, Stdio$Stdio$mode(stdio)); if (!Result$isOk(result)) { return result; } const process = Result$Ok$0(result); let state = Stdio$Stdio$initial(stdio); function handleData(chunk) { const bits = BitArray$BitArray(new Uint8Array(chunk)); state = Stdio$Stdio$on_data(stdio)(state, bits); } function handleExit(code) { Stdio$Stdio$on_exit(stdio)(state, code ?? -1); } process.stdout?.on("data", handleData); process.stderr?.on("data", handleData); process.on("exit", handleExit); process.on("error", () => handleExit(-2)); return result; } export function spawn_raw(executable, args, envVars, workingDirectory, mode) { const stat = fs.statSync(executable, { throwIfNoEntry: false }); if (stat == null || !stat.isFile()) { return Result$Error(StartError$FileNotFound(executable)); } if (!isExecutable(executable)) { return Result$Error(StartError$FileNotExecutable(executable)); } const cwd = Option$isSome(workingDirectory) ? Option$Some$0(workingDirectory) : undefined; if (cwd !== undefined) { const cwdStat = fs.statSync(cwd, { throwIfNoEntry: false }); if (!cwdStat || !cwdStat.isDirectory) { // NOTE: we could report a better error here, but for consistency with erlang we just do this. return Result$Error(StartError$FileNotFound(executable)); } } const env = getEnv(envVars); let stdio; if (Mode$isInherit(mode)) { stdio = "inherit"; } else if (Mode$isNull(mode)) { stdio = ["pipe", "ignore", "ignore"]; // stdin is pipe to allow writing } else if (Mode$isCapture(mode)) { const stderr = Mode$Capture$capture_stderr(mode) ? "pipe" : "inherit"; stdio = ["pipe", "pipe", stderr]; } else { throw new Error("invalid mode"); } const options = { env, cwd, stdio, windowsHide: true }; try { const child = child_process.spawn(executable, toArray(args), options); return Result$Ok(child); } catch { return Result$Error(StartError$RuntimeLimitReached()); } } function getEnv(env) { if (List.is_empty(env)) { return process.env; } return List.fold(env, { ...process.env }, (env, [key, value]) => { env[key] = value; return env; }); } // Write text to process stdin export function write(process, text) { if (process.exitCode !== null || process.stdin.destroyed) { return Result$Error(WriteError$ProcessExited()); } // TODO: change this when 1.15 releases if (text instanceof BitArray) { if (text.bitOffset !== 0 || text.bitSize % 8 !== 0) { throw new globalThis.Error( "child_process.write_bits does not support unaligned bit arrays", ); } text = text.rawBuffer; } if (process.stdin.write(text)) { return Result$Ok(); } else { return Result$Error(WriteError$WriteAborted()); } } // Close process stdin export function close(process) { if (process.exitCode !== null || process.stdin.destroyed) { return; } process.stdin.end(); } // Stop process gracefully (SIGTERM) export function term(process) { if (process.exitCode !== null) { return; } process.kill("SIGTERM"); } // Kill process forcefully (SIGKILL) export function kill(process) { if (process.exitCode !== null) { return; } process.kill("SIGKILL"); } // Get OS process ID export function os_process_id(process) { if (process.exitCode !== null) { return Result$Error(); } return Result$Ok(process.pid); } // Detect the operating system export function os() { if (isWindows) return Os$Windows(); if (isMacos) return Os$Macos(); return Os$Linux(); } // Get the priv directory for this package const __dirname = pathModule.dirname(fileURLToPath(import.meta.url)); export function priv_directory() { return pathModule.join(__dirname, "..", "priv"); }