Files
WYF-koubo/electron/mapi/app/index.ts
T
2026-06-19 18:45:55 +08:00

371 lines
11 KiB
TypeScript

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: () => Promise<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;
});
const waitForExit = async () => {
if (end) {
return;
}
await new Promise<void>(resolve => {
let settled = false;
const finish = () => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeoutId);
spawnProcess.off("exit", finish);
spawnProcess.off("error", finish);
resolve();
};
const timeoutId = setTimeout(finish, 5000);
spawnProcess.once("exit", finish);
spawnProcess.once("error", finish);
});
};
return {
stop: async () => {
Log.info("App.spawnShell.stop");
if (end) {
return;
}
if (isWin) {
await new Promise<void>(resolve => {
_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})));
resolve();
}
);
});
} else {
spawnProcess.kill("SIGINT");
}
await waitForExit();
},
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> => {
try {
args.unshift(extraResolveBin(binary));
} catch (error) {
// 如果extraResolveBin抛出错误,通过error回调通知调用者
if (option?.error) {
option.error(error.message, -1, null);
}
throw error;
}
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 = [];
// URL-encode app name to handle Chinese characters in HTTP headers
const appName = encodeURIComponent(AppConfig.name);
param.push(`AppOpen/${appName}/${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;