371 lines
11 KiB
TypeScript
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;
|