kaven-utils
Version:
Utils for Node.js.
205 lines (204 loc) • 8.05 kB
JavaScript
/********************************************************************
* @author: Kaven
* @email: kaven@wuwenkai.com
* @website: http://blog.kaven.xyz
* @file: [Kaven-Utils] /src/KavenUtility.ChildProcess.ts
* @create: 2023-11-25 22:29:30.048
* @modify: 2025-10-23 23:25:06.133
* @version: 6.1.2
* @times: 43
* @lines: 240
* @copyright: Copyright © 2023-2025 Kaven. All Rights Reserved.
* @description: [description]
* @license: [license]
********************************************************************/
import pkgIconvLite from "iconv-lite";
import { KavenCache } from "kaven-basic";
import { exec, execSync, spawn } from "node:child_process";
import { EOL } from "node:os";
import { PassThrough, Writable } from "node:stream";
import { IsWin32 } from "./KavenUtility.Constant.js";
/**
* @since 6.0.5
* @version 2025-10-11
*/
export function Tee(...targets) {
return new Writable({
write(chunk, enc, cb) {
for (const t of targets) {
t.write(chunk, enc);
}
cb();
},
final(cb) {
for (const t of targets) {
t.end();
}
cb();
},
});
}
/**
* @since 6.0.5
* @version 2025-10-11
*/
export function GetSystemEncoding(options) {
const key = "KavenUtility.ChildProcess.GetSystemEncoding";
let cachedEncoding = KavenCache.GetCache(key);
if (cachedEncoding) {
return cachedEncoding; // ✅ cached result
}
try {
if (IsWin32) {
const output = execSync("chcp", { encoding: "utf8" });
const match = output.match(/:\s*(\d+)/);
const cp = match ? parseInt(match[1], 10) : 65001;
const map = {
65001: "utf8",
936: "gbk",
950: "big5",
932: "shift_jis",
1252: "windows-1252",
437: "ibm437",
};
cachedEncoding = map[cp] || "utf8";
}
else {
// Unix-like: locale charmap is usually constant too
const output = execSync("locale charmap", { encoding: "utf8" }).trim();
cachedEncoding = output || "utf8";
}
}
catch (ex) {
options?.logger?.Warn(ex);
cachedEncoding = "utf8";
}
KavenCache.SetCacheWithHour(key, cachedEncoding, 2);
return cachedEncoding;
}
export async function Execute(p1, p2) {
const decodeStream = pkgIconvLite.decodeStream;
return new Promise((resolve, reject) => {
try {
let command = typeof p1 === "string" ? p1 : p1.command;
const options = typeof p1 === "string" ? (p2 ?? {}) : (p1.options ?? {});
const logger = options.logger;
logger?.Info(`Execute: ${command}`);
if (options.decoderEncoding === undefined && options.autoDetectDecoderEncoding) {
options.decoderEncoding = GetSystemEncoding({ logger });
logger?.Info(`GetSystemEncoding: ${options.decoderEncoding}`);
}
if (options.suppressingInteractiveInput) {
if (IsWin32) {
command = `${command} < NUL`;
}
else {
command = `${command} < /dev/null`;
}
}
const stderrTee = new PassThrough();
const stderrChunks = [];
stderrTee.on("data", data => {
stderrChunks.push(data);
});
if (options.stderr) {
options.stderr = Tee(options.stderr, stderrTee);
}
else {
options.stderr = stderrTee;
}
const done = (code, signal) => {
if (code !== 0 && !options.resolveNonZeroExitCodes) {
let message = `Child process closed with code ${code} and signal ${signal}`;
if (stderrChunks.length > 0) {
const error = Buffer.concat(stderrChunks).toString();
message = error + EOL + message;
}
reject(new Error(message));
}
else {
resolve(code);
}
};
if (options.spawn) {
if (options.spawnArgs === undefined) {
// command = command.trim();
// if (command.startsWith(Strings_DoubleQuotes)) {
// const index = command.indexOf(Strings_DoubleQuotes, 1);
// if (index > 0) {
// command = command.substring(0, index);
// options.spawnArgs = command.substring(index + 1).trim().split(Strings_WhiteSpace);
// }
// } else if (command.startsWith("'")) {
// const index = command.indexOf("'", 1);
// if (index > 0) {
// command = command.substring(0, index);
// options.spawnArgs = command.substring(index + 1).trim().split(Strings_WhiteSpace);
// }
// } else {
// [command, ...options.spawnArgs] = command.trim().split(Strings_WhiteSpace);
// }
command = command.trim();
// Regular expression to match quoted or unquoted parts
const regex = /(?:[^\s'"]+|"[^"]*"|'[^']*')+/g;
// Extract the matched parts
const parts = command.match(regex) || [];
// The first part is the command
command = parts.shift() ?? command;
// The remaining parts are the arguments
options.spawnArgs = parts.map(arg => arg.replace(/^(['"])(.*)\1$/, "$2"));
}
const p = spawn(command, options.spawnArgs, options.spawnOptions);
p.on("close", (code, signal) => done(code, signal));
p.on("error", data => {
logger?.Error(data);
});
if (options.decoderEncoding) {
if (options.stdout) {
p.stdout.pipe(decodeStream(options.decoderEncoding)).pipe(options.stdout);
}
if (options.stderr) {
p.stderr.pipe(decodeStream(options.decoderEncoding)).pipe(options.stderr);
}
}
else {
if (options.stdout) {
p.stdout.pipe(options.stdout);
}
if (options.stderr) {
p.stderr.pipe(options.stderr);
}
}
}
else {
const p = exec(command, {
encoding: "buffer",
...options.execOptions,
});
p.on("close", (code, signal) => done(code, signal));
p.on("error", data => {
logger?.Error(data);
});
if (options.stdout && p.stdout) {
if (options.decoderEncoding) {
p.stdout.pipe(decodeStream(options.decoderEncoding)).pipe(options.stdout);
}
else {
p.stdout.pipe(options.stdout);
}
}
if (options.stderr && p.stderr) {
if (options.decoderEncoding) {
p.stderr.pipe(decodeStream(options.decoderEncoding)).pipe(options.stderr);
}
else {
p.stderr.pipe(options.stderr);
}
}
}
}
catch (ex) {
reject(ex);
}
});
}