Files
TrueGrowth/vendor/aigcpanel/electron/mapi/app/index.ts
Jammy 52636c91ae
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Track bundled vendor runtime sources
2026-07-07 10:05:50 +08:00

363 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import iconv from "iconv-lite";
import { exec as _exec, spawn } from "node:child_process";
import net from "node:net";
import util from "node:util";
import { AppConfig } from "../../../src/config";
import {
extraResolveBin,
isLinux,
isMac,
isWin,
platformArch,
platformName,
platformUUID,
platformVersion,
} from "../../lib/env";
import { IconvUtil, ShellUtil, StrUtil } from "../../lib/util";
import { Log } from "../log/index";
const exec = util.promisify(_exec);
const outputStringConvert = (outputEncoding: "utf8" | "cp936", data: any) => {
if (!data) {
return "";
}
if (outputEncoding === "utf8") {
return data.toString();
}
let dataEncoding = "binary";
if (Buffer.isBuffer(data)) {
dataEncoding = IconvUtil.detect(data as any);
if ("UTF-8" === dataEncoding) {
return data.toString("utf8");
}
}
// dataEncoding UTF-8 cp936
// dataEncoding ISO-8859-1 cp936
// console.log('dataEncoding', dataEncoding, outputEncoding)
return iconv.decode(Buffer.from(data, dataEncoding as any), outputEncoding);
};
const shell = async (
command: string,
option?: {
cwd?: string;
outputEncoding?: string;
shell?: boolean;
},
) => {
option = Object.assign(
{
cwd: process.cwd(),
outputEncoding: isWin ? "cp936" : "utf8",
shell: true,
},
option,
);
const result = await exec(command, {
env: { ...process.env },
shell: option.shell,
encoding: "binary",
cwd: option["cwd"],
} as any);
return {
stdout: outputStringConvert(
option.outputEncoding as any,
result.stdout,
),
stderr: outputStringConvert(
option.outputEncoding as any,
result.stderr,
),
};
};
const spawnShell = async (
command: string | string[],
option: {
stdout?: (data: string, process: any) => void;
stderr?: (data: string, process: any) => void;
success?: (process: any) => void;
error?: (msg: string, exitCode: number, process: any) => void;
cwd?: string;
outputEncoding?: string;
env?: Record<string, any>;
shell?: boolean;
} | null = null,
): Promise<{
stop: () => void;
send: (data: any) => void;
result: () => Promise<string>;
}> => {
option = Object.assign(
{
cwd: process.cwd(),
outputEncoding: isWin ? "cp936" : "utf8",
env: {},
shell: true,
},
option,
);
let commandEntry = "",
args = [];
if (Array.isArray(command)) {
commandEntry = command[0];
args = command.slice(1);
} else {
args = ShellUtil.parseCommandArgs(command);
commandEntry = args.shift() as string;
}
Log.info("App.spawnShell", {
commandEntry,
args,
option: {
cwd: option["cwd"],
outputEncoding: option["outputEncoding"],
},
});
const spawnProcess = spawn(commandEntry, args, {
env: { ...process.env, ...option.env },
cwd: option["cwd"],
shell: option.shell,
encoding: "binary",
} as any);
let end = false;
let isSuccess = false;
let exitCode = -1;
const stdoutList: string[] = [];
const stderrList: string[] = [];
spawnProcess.stdout?.on("data", (data) => {
// console.log('App.spawnShell.stdout', data)
let dataString = outputStringConvert(
option.outputEncoding as any,
data,
);
Log.info("App.spawnShell.stdout", dataString);
stdoutList.push(dataString);
option.stdout?.(dataString, spawnProcess);
});
spawnProcess.stderr?.on("data", (data) => {
// console.log('App.spawnShell.stderr', data)
let dataString = outputStringConvert(
option.outputEncoding as any,
data,
);
Log.info("App.spawnShell.stderr", dataString);
stderrList.push(dataString);
option.stderr?.(dataString, spawnProcess);
});
spawnProcess.on("exit", (code, signal) => {
// console.log('App.spawnShell.exit', code)
Log.info("App.spawnShell.exit", { code, signal });
exitCode = code;
if (isWin) {
if (0 === code || 1 === code) {
isSuccess = true;
}
} else {
if (null === code || 0 === code) {
isSuccess = true;
}
}
if (isSuccess) {
option.success?.(spawnProcess);
} else {
option.error?.(
`command ${command} failed with code ${code}`,
exitCode,
spawnProcess,
);
}
end = true;
});
spawnProcess.on("error", (err) => {
// console.log('App.spawnShell.error', err)
Log.info("App.spawnShell.error", err);
option.error?.(err.toString(), -1, spawnProcess);
end = true;
});
return {
stop: () => {
Log.info("App.spawnShell.stop");
if (isWin) {
_exec(
`taskkill /pid ${spawnProcess.pid} /T /F`,
{
encoding: "binary",
},
(err, stdout, stderr) => {
if (stdout) {
stdout = outputStringConvert(
option.outputEncoding as any,
stdout,
);
}
if (stderr) {
stderr = outputStringConvert(
option.outputEncoding as any,
stderr,
);
}
Log.info(
"App.spawnShell.stop.taskkill",
JSON.parse(JSON.stringify({ err, stdout, stderr })),
);
},
);
} else {
spawnProcess.kill("SIGINT");
}
},
send: (data) => {
Log.info("App.spawnShell.send", data);
spawnProcess.stdin.write(data);
},
result: async (): Promise<string> => {
if (end) {
return stdoutList.join("") + stderrList.join("");
}
return new Promise((resolve, reject) => {
const watchEnd = () => {
setTimeout(() => {
if (!end) {
watchEnd();
return;
}
if (isSuccess) {
resolve(stdoutList.join("") + stderrList.join(""));
} else {
reject(
[
`command ${command} failed with code ${exitCode} : `,
stdoutList.join(""),
stderrList.join(""),
].join(""),
);
}
}, 10);
};
watchEnd();
});
},
};
};
const spawnBinary = async (
binary: string,
args: string[],
option: {
stdout?: (data: string, process: any) => void;
stderr?: (data: string, process: any) => void;
success?: (process: any) => void;
error?: (msg: string, exitCode: number, process: any) => void;
cwd?: string;
outputEncoding?: string;
env?: Record<string, any>;
shell?: boolean;
} | null = null,
): Promise<string> => {
args.unshift(extraResolveBin(binary));
const res = await Apps.spawnShell(args, {
...(option || {}),
shell: false,
});
return await res.result();
};
const availablePortLock: {
[port: number]: {
lockKey: string;
lockTime: number;
};
} = {};
/**
* 获取一个可用的端口
* @param start 开始的端口
* @param lockKey 锁定的key避免其他进程获取默认会创建一个随机的key
* @param lockTime 锁定时间,避免在本次获取后未启动服务导致其他进程重复获取
*/
const availablePort = async (
start: number,
lockKey?: string,
lockTime?: number,
): Promise<number> => {
lockKey = lockKey || StrUtil.randomString(8);
lockTime = lockTime || 60;
// expire lock
const now = Date.now();
for (const port in availablePortLock) {
const lockInfo = availablePortLock[port];
if (lockInfo.lockTime < now) {
delete availablePortLock[port];
}
}
for (let i = start; i < 65535; i++) {
const available = await isPortAvailable(i, "0.0.0.0");
const availableLocal = await isPortAvailable(i, "127.0.0.1");
// console.log('isPortAvailable', i, available, availableLocal)
if (available && availableLocal) {
const lockInfo = availablePortLock[i];
if (lockInfo) {
if (lockInfo.lockKey === lockKey) {
return i;
} else {
// other lockKey lock the port
continue;
}
}
availablePortLock[i] = {
lockKey,
lockTime: Date.now() + lockTime * 1000,
};
return i;
}
}
throw new Error("no available port");
};
const isPortAvailable = async (
port: number,
host?: string,
): Promise<boolean> => {
return new Promise((resolve) => {
const server = net.createServer();
server.listen(port, host);
server.on("listening", () => {
server.close();
resolve(true);
});
server.on("error", () => {
resolve(false);
});
});
};
const fixExecutable = async (executable: string) => {
if (isMac || isLinux) {
// chmod +x executable
await shell(`chmod +x "${executable}"`);
}
};
const getUserAgent = () => {
let param = [];
param.push(`AppOpen/${AppConfig.name}/${AppConfig.version}`);
param.push(
`Platform/${platformName()}/${platformArch()}/${platformVersion()}/${platformUUID()}`,
);
return param.join(" ");
};
export const Apps = {
shell,
spawnShell,
spawnBinary,
availablePort,
isPortAvailable,
fixExecutable,
getUserAgent,
};
export default Apps;