feat: SPT Launcher Electron 애플리케이션의 초기 구현으로, 서버 헬스 체크, 명령어 실행, 세션 관리 등 핵심 기능들을 포함합니다.
This commit is contained in:
parent
2328cb9b5f
commit
1573df675b
287
src/main/main.ts
287
src/main/main.ts
|
|
@ -24,6 +24,18 @@ const COMMAND_TIMEOUT_MS = APP_CONFIG.commandTimeoutMs;
|
|||
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
||||
const SESSION_TTL_MS = APP_CONFIG.sessionTtlMs;
|
||||
|
||||
const CLIENT_PROCESS_NAMES = [
|
||||
"EscapeFromTarkov.exe",
|
||||
"EscapeFromTarkov_BE.exe",
|
||||
];
|
||||
|
||||
const SERVER_PROCESS_NAMES = [
|
||||
"Aki.Server.exe",
|
||||
"SPT.Server.exe"
|
||||
];
|
||||
|
||||
const GAME_PROCESS_NAMES = [...CLIENT_PROCESS_NAMES, ...SERVER_PROCESS_NAMES];
|
||||
|
||||
type CommandCheckResult = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
|
|
@ -89,7 +101,9 @@ type SessionLookupResult =
|
|||
|
||||
type InstallStep = {
|
||||
name: string;
|
||||
result: CommandRunResult;
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
result?: CommandRunResult;
|
||||
};
|
||||
|
||||
type InstallGitToolsResult = {
|
||||
|
|
@ -191,6 +205,45 @@ const checkGitTools = async () => {
|
|||
};
|
||||
};
|
||||
|
||||
const checkGameRunning = async (): Promise<{ running: boolean; serverRunning: boolean; clientRunning: boolean; processes: string[] }> => {
|
||||
if (process.platform !== "win32") {
|
||||
return { running: false, serverRunning: false, clientRunning: false, processes: [] };
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
execFile("tasklist", ["/FO", "CSV", "/NH"], (error, stdout) => {
|
||||
if (error) {
|
||||
console.warn("tasklist failed", error);
|
||||
resolve({ running: false, serverRunning: false, clientRunning: false, processes: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const output = stdout.toString().toLowerCase();
|
||||
const runningProcesses: string[] = [];
|
||||
let serverRunning = false;
|
||||
let clientRunning = false;
|
||||
|
||||
for (const procName of GAME_PROCESS_NAMES) {
|
||||
if (output.includes(procName.toLowerCase())) {
|
||||
runningProcesses.push(procName);
|
||||
if (SERVER_PROCESS_NAMES.includes(procName)) {
|
||||
serverRunning = true;
|
||||
} else if (CLIENT_PROCESS_NAMES.includes(procName)) {
|
||||
clientRunning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
running: runningProcesses.length > 0,
|
||||
serverRunning,
|
||||
clientRunning,
|
||||
processes: runningProcesses
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resolveCommandPath = async (
|
||||
command: string,
|
||||
): Promise<CommandPathResult> => {
|
||||
|
|
@ -741,6 +794,8 @@ const ensureModRepo = async (
|
|||
}
|
||||
onProgress?.({
|
||||
name: "git fetch",
|
||||
ok: fetchResult.ok,
|
||||
message: fetchResult.error,
|
||||
result: fetchResult,
|
||||
});
|
||||
return;
|
||||
|
|
@ -764,6 +819,8 @@ const ensureModRepo = async (
|
|||
}
|
||||
onProgress?.({
|
||||
name: "git clone",
|
||||
ok: cloneResult.ok,
|
||||
message: cloneResult.error,
|
||||
result: cloneResult,
|
||||
});
|
||||
};
|
||||
|
|
@ -792,6 +849,8 @@ const checkoutRepoTag = async (
|
|||
}
|
||||
onProgress?.({
|
||||
name: `git checkout ${tag}`,
|
||||
ok: checkoutResult.ok,
|
||||
message: checkoutResult.error,
|
||||
result: checkoutResult,
|
||||
});
|
||||
};
|
||||
|
|
@ -819,6 +878,8 @@ const pullLfs = async (
|
|||
}
|
||||
onProgress?.({
|
||||
name: "git lfs pull",
|
||||
ok: pullResult.ok,
|
||||
message: pullResult.error,
|
||||
result: pullResult,
|
||||
});
|
||||
};
|
||||
|
|
@ -836,6 +897,7 @@ const syncDirectories = async (
|
|||
// Initial progress event for starting file copy
|
||||
onProgress?.({
|
||||
name: "copying files",
|
||||
ok: true,
|
||||
result: { ok: true, command: "copy", args: [] }, // Intermediate state
|
||||
});
|
||||
|
||||
|
|
@ -873,6 +935,7 @@ const syncDirectories = async (
|
|||
});
|
||||
onProgress?.({
|
||||
name: "copying files",
|
||||
ok: true,
|
||||
result: { ok: true, command: "copy", args: [] },
|
||||
});
|
||||
};
|
||||
|
|
@ -910,7 +973,9 @@ const runModSync = async (
|
|||
}
|
||||
};
|
||||
|
||||
const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
||||
const installGitTools = async (
|
||||
onProgress?: (step: InstallStep) => void,
|
||||
): Promise<InstallGitToolsResult> => {
|
||||
if (process.platform !== "win32") {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -919,26 +984,49 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
|||
};
|
||||
}
|
||||
|
||||
const steps: InstallStep[] = [];
|
||||
|
||||
const wingetPath = await resolveCommandPath("winget");
|
||||
if (!wingetPath.ok) {
|
||||
const errorStep = {
|
||||
name: "winget 확인",
|
||||
ok: false,
|
||||
message: wingetPath.error,
|
||||
result: {
|
||||
ok: false,
|
||||
command: "winget",
|
||||
args: ["--version"],
|
||||
error: wingetPath.error,
|
||||
},
|
||||
};
|
||||
steps.push(errorStep);
|
||||
onProgress?.(errorStep);
|
||||
return {
|
||||
ok: false,
|
||||
error: "winget이 설치되어 있지 않습니다.",
|
||||
steps: [
|
||||
{
|
||||
name: "winget 확인",
|
||||
result: {
|
||||
ok: false,
|
||||
command: "winget",
|
||||
args: ["--version"],
|
||||
error: wingetPath.error,
|
||||
},
|
||||
},
|
||||
],
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
const steps: InstallStep[] = [];
|
||||
// Winget check success
|
||||
/*
|
||||
const wingetStep = {
|
||||
name: "winget 확인",
|
||||
result: { ok: true, command: "winget", args: ["--version"] }
|
||||
};
|
||||
steps.push(wingetStep);
|
||||
onProgress?.(wingetStep);
|
||||
*/
|
||||
// skipping separate success step for winget to match previous behavior purely?
|
||||
// User asked for "same template", so seeing "Winget Check... OK" is good.
|
||||
// I will add it for clarity.
|
||||
const wingetStep = {
|
||||
name: "winget 확인",
|
||||
ok: true,
|
||||
result: { ok: true, command: "winget", args: ["--version"] }
|
||||
};
|
||||
steps.push(wingetStep);
|
||||
onProgress?.(wingetStep);
|
||||
|
||||
const gitInstall = await runCommand(
|
||||
"winget",
|
||||
|
|
@ -952,10 +1040,21 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
|||
"--accept-source-agreements",
|
||||
"--accept-package-agreements",
|
||||
"--silent",
|
||||
"--disable-interactivity" // Ensure no blocking prompts
|
||||
],
|
||||
INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
steps.push({ name: "Git 설치", result: gitInstall });
|
||||
steps.push({
|
||||
name: "Git 설치",
|
||||
ok: gitInstall.ok,
|
||||
message: gitInstall.output || gitInstall.error, // Prioritize output
|
||||
result: gitInstall
|
||||
});
|
||||
onProgress?.({
|
||||
name: "Git 설치",
|
||||
ok: gitInstall.ok,
|
||||
message: gitInstall.output || gitInstall.error // Prioritize output
|
||||
});
|
||||
|
||||
const lfsInstall = await runCommand(
|
||||
"winget",
|
||||
|
|
@ -969,12 +1068,23 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
|||
"--accept-source-agreements",
|
||||
"--accept-package-agreements",
|
||||
"--silent",
|
||||
"--disable-interactivity"
|
||||
],
|
||||
INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
steps.push({ name: "Git LFS 설치", result: lfsInstall });
|
||||
steps.push({
|
||||
name: "Git LFS 설치",
|
||||
ok: lfsInstall.ok,
|
||||
message: lfsInstall.output || lfsInstall.error, // Prioritize output
|
||||
result: lfsInstall
|
||||
});
|
||||
onProgress?.({
|
||||
name: "Git LFS 설치",
|
||||
ok: lfsInstall.ok,
|
||||
message: lfsInstall.output || lfsInstall.error // Prioritize output
|
||||
});
|
||||
|
||||
const ok = steps.every((step) => step.result.ok);
|
||||
const ok = steps.every((step) => step.ok);
|
||||
const check = await checkGitTools();
|
||||
|
||||
return {
|
||||
|
|
@ -1254,8 +1364,147 @@ app.whenReady().then(() => {
|
|||
return await getGitPaths();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:installGitTools", async () => {
|
||||
return await installGitTools();
|
||||
ipcMain.handle("spt:installGitTools", async (event) => {
|
||||
return await installGitTools((step) => {
|
||||
event.sender.send("spt:gitInstallProgress", step);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:launchGame", async (event) => {
|
||||
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
const installInfo = await resolveSptInstall();
|
||||
|
||||
if (!installInfo.path) {
|
||||
return { ok: false, error: "no_spt_path" };
|
||||
}
|
||||
|
||||
// Double check running processes
|
||||
const running = await checkGameRunning();
|
||||
if (running.clientRunning) {
|
||||
return { ok: false, error: "already_running", details: running.processes };
|
||||
}
|
||||
|
||||
// Double check mod version (optional but safer)
|
||||
const modStatus = await getModVersionStatus();
|
||||
if (!modStatus.ok) {
|
||||
return { ok: false, error: "mod_verification_failed" };
|
||||
}
|
||||
|
||||
// Launch Server if not running (Local only)
|
||||
const serverUrl = new URL(SERVER_BASE_URL);
|
||||
const isLocal = serverUrl.hostname === "127.0.0.1" || serverUrl.hostname === "localhost";
|
||||
|
||||
if (isLocal && !running.serverRunning) {
|
||||
let serverExePath: string | undefined;
|
||||
for (const exe of SERVER_PROCESS_NAMES) {
|
||||
// Check direct path or SPT subdirectory
|
||||
const direct = path.join(installInfo.path, exe);
|
||||
if (await pathExists(direct)) {
|
||||
serverExePath = direct;
|
||||
break;
|
||||
}
|
||||
const nested = path.join(installInfo.path, "SPT", exe);
|
||||
if (await pathExists(nested)) {
|
||||
serverExePath = nested;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (serverExePath) {
|
||||
console.log(`[spt-launcher] Starting Server: ${serverExePath}`);
|
||||
const serverBat = stripExe(serverExePath) + ".bat"; // Sometimes users use bat, but exe should work if arguments aren't complex
|
||||
|
||||
// Prefer EXE for now.
|
||||
const child = require("child_process").spawn(serverExePath, [], {
|
||||
cwd: path.dirname(serverExePath),
|
||||
detached: true,
|
||||
stdio: "ignore"
|
||||
});
|
||||
child.unref();
|
||||
|
||||
// Wait for server health
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // 30 seconds approx
|
||||
while(attempts < maxAttempts) {
|
||||
const health = await checkServerHealth();
|
||||
if (health.ok) {
|
||||
break;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Launch Client
|
||||
try {
|
||||
// Prioritize EFT executable
|
||||
const candidates = [
|
||||
"EscapeFromTarkov.exe",
|
||||
"EscapeFromTarkov_BE.exe",
|
||||
"SPT/EscapeFromTarkov.exe",
|
||||
"SPT/EscapeFromTarkov_BE.exe"
|
||||
];
|
||||
|
||||
let executablePath: string | undefined;
|
||||
for (const exe of candidates) {
|
||||
const fullPath = path.join(installInfo.path, exe);
|
||||
if (await pathExists(fullPath)) {
|
||||
executablePath = fullPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!executablePath) {
|
||||
return { ok: false, error: "executable_not_found" };
|
||||
}
|
||||
|
||||
// Get Session ID for args
|
||||
const session = await readSessionRecord();
|
||||
if (!session || isSessionExpired(session)) {
|
||||
return { ok: false, error: "session_required" };
|
||||
}
|
||||
|
||||
// Build Args
|
||||
const configArg = JSON.stringify({
|
||||
BackendUrl: SERVER_BASE_URL,
|
||||
Version: "live"
|
||||
});
|
||||
const args = [`-token=${session.sessionId}`, `-config=${configArg}`];
|
||||
console.log(`[spt-launcher] Launching: ${executablePath} with args`, args);
|
||||
|
||||
// Sanitize Environment
|
||||
// Strip Electron/Node specifics to prevent BepInEx/Plugin conflicts
|
||||
const sanitizedEnv = { ...process.env };
|
||||
for (const key of Object.keys(sanitizedEnv)) {
|
||||
if (key.startsWith("ELECTRON_") || key.startsWith("NODE_") || key === "VITE_DEV_SERVER_URL") {
|
||||
delete sanitizedEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn detached
|
||||
const child = require("child_process").spawn(executablePath, args, {
|
||||
cwd: path.dirname(executablePath),
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
env: sanitizedEnv
|
||||
});
|
||||
child.unref();
|
||||
|
||||
mainWindow?.minimize();
|
||||
return { ok: true, executedPath: executablePath };
|
||||
} catch (e: any) {
|
||||
console.error("Launch failed", e);
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
function stripExe(filename: string) {
|
||||
return filename.replace(/\.exe$/i, "");
|
||||
}
|
||||
|
||||
ipcMain.handle("spt:checkGameRunning", async () => {
|
||||
return await checkGameRunning();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:getModVersionStatus", async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ console.info("[spt-launcher] preload loaded");
|
|||
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||
appName: "SPT Launcher",
|
||||
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
||||
launchGame: () => ipcRenderer.invoke("spt:launchGame"),
|
||||
checkGameRunning: () => ipcRenderer.invoke("spt:checkGameRunning"),
|
||||
checkGitTools: () => ipcRenderer.invoke("spt:checkGitTools"),
|
||||
getGitPaths: () => ipcRenderer.invoke("spt:getGitPaths"),
|
||||
installGitTools: () => ipcRenderer.invoke("spt:installGitTools"),
|
||||
|
|
@ -33,4 +35,12 @@ contextBridge.exposeInMainWorld("sptLauncher", {
|
|||
ipcRenderer.removeListener("spt:modSyncProgress", subscription);
|
||||
};
|
||||
},
|
||||
onGitInstallProgress: (callback: (event: unknown, step: unknown) => void) => {
|
||||
const subscription = (_event: unknown, step: unknown) =>
|
||||
callback(_event, step);
|
||||
ipcRenderer.on("spt:gitInstallProgress", subscription);
|
||||
return () => {
|
||||
ipcRenderer.removeListener("spt:gitInstallProgress", subscription);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ const App = () => {
|
|||
const sessionResumeAttemptedRef = useRef(false);
|
||||
const transitionTimeoutRef = useRef<number | undefined>();
|
||||
const simulateToolsMissingRef = useRef(false);
|
||||
const [modalTitle, setModalTitle] = useState("모드 적용 중...");
|
||||
const [syncSteps, setSyncSteps] = useState<Array<{ name: string; ok: boolean; message?: string }>>([]);
|
||||
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
||||
|
||||
|
|
@ -120,6 +121,16 @@ const App = () => {
|
|||
if (rawName === "copying files") {
|
||||
return { title: "모드 파일 적용", command: "Copying files to Game Directory..." };
|
||||
}
|
||||
// Git Install Steps
|
||||
if (rawName === "winget 확인") {
|
||||
return { title: "패키지 관리자(winget) 확인", command: "checking winget..." };
|
||||
}
|
||||
if (rawName === "Git 설치") {
|
||||
return { title: "Git 설치 (시간이 걸릴 수 있습니다)", command: "winget install Git.Git" };
|
||||
}
|
||||
if (rawName === "Git LFS 설치") {
|
||||
return { title: "Git LFS 설치", command: "winget install GitHub.GitLFS" };
|
||||
}
|
||||
return { title: rawName, command: "" };
|
||||
};
|
||||
|
||||
|
|
@ -636,12 +647,25 @@ const App = () => {
|
|||
|
||||
setInstallInProgress(true);
|
||||
setInstallMessage(undefined);
|
||||
|
||||
// Modal Setup
|
||||
setSyncSteps([]);
|
||||
setSyncResult(undefined);
|
||||
setModalTitle("Git 도구 설치 중...");
|
||||
setSyncInProgress(true);
|
||||
|
||||
const cleanup = window.sptLauncher.onGitInstallProgress((_e, step: any) => {
|
||||
setSyncSteps((prev) => [...prev, step]);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await window.sptLauncher.installGitTools();
|
||||
if (!result.ok) {
|
||||
setInstallMessage(result.error ?? "자동 설치에 실패했습니다.");
|
||||
setSyncResult({ ok: false, error: result.error });
|
||||
} else {
|
||||
setInstallMessage("자동 설치가 완료되었습니다.");
|
||||
setSyncResult({ ok: true });
|
||||
}
|
||||
if (result.check) {
|
||||
setGitCheckResult(result.check);
|
||||
|
|
@ -650,8 +674,11 @@ const App = () => {
|
|||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setInstallMessage(message);
|
||||
setSyncResult({ ok: false, error: message });
|
||||
} finally {
|
||||
cleanup();
|
||||
setInstallInProgress(false);
|
||||
setSyncInProgress(false); // Show close button
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -895,6 +922,8 @@ const App = () => {
|
|||
|
||||
setSyncSteps([]);
|
||||
setSyncResult(undefined);
|
||||
setModalTitle("모드 적용 중...");
|
||||
setSyncInProgress(true);
|
||||
|
||||
// Subscribe to progress events
|
||||
const cleanup = window.sptLauncher.onModSyncProgress
|
||||
|
|
@ -991,7 +1020,33 @@ const App = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
window.alert("게임을 실행합니다.");
|
||||
if (!window.sptLauncher?.launchGame) {
|
||||
window.alert("게임 시작 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.sptLauncher.launchGame();
|
||||
if (!result.ok) {
|
||||
if (result.error === "already_running") {
|
||||
window.alert(`게임이 이미 실행 중입니다.\n(발견된 프로세스: ${(result.details ?? []).join(", ")})`);
|
||||
} else if (result.error === "mod_verification_failed") {
|
||||
window.alert("모드 버전 검증에 실패했습니다. 버전을 다시 확인해주세요.");
|
||||
} else if (result.error === "no_spt_path") {
|
||||
window.alert("SPT 설치 경로가 설정되지 않았습니다.");
|
||||
} else if (result.error === "executable_not_found") {
|
||||
window.alert("실행 파일(Launcher/Server)을 찾을 수 없습니다.");
|
||||
} else {
|
||||
window.alert(`게임 실행 실패: ${result.error}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Success
|
||||
window.alert(`게임이 실행되었습니다.\n(${result.executedPath})`);
|
||||
} catch (e: any) {
|
||||
window.alert(`게임 실행 중 오류가 발생했습니다: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -1413,8 +1468,8 @@ const App = () => {
|
|||
<header className="modal-header">
|
||||
<h3>
|
||||
{syncResult
|
||||
? (syncResult.ok ? "모드 적용 완료" : "오류 발생")
|
||||
: "모드 적용 중..."}
|
||||
? (syncResult.ok ? (modalTitle.replace("중...", "완료")) : (syncResult.error ? `오류: ${syncResult.error}` : "오류 발생"))
|
||||
: modalTitle}
|
||||
</h3>
|
||||
{!syncInProgress && (
|
||||
<button type="button" className="ghost" onClick={closeSyncModal}>
|
||||
|
|
@ -1445,7 +1500,7 @@ const App = () => {
|
|||
</div>
|
||||
{syncResult && (
|
||||
<div className={`notice ${syncResult.ok ? "info" : "error"}`}>
|
||||
{syncResult.ok ? "모드 적용이 완료되었습니다." : `실패: ${syncResult.error}`}
|
||||
{syncResult.ok ? "모드 적용이 완료되었습니다." : (syncResult.error ? `실패: ${syncResult.error}` : "오류가 발생했습니다.")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ interface Window {
|
|||
error?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
launchGame: () => Promise<{ ok: boolean; error?: string; details?: string[]; executedPath?: string }>;
|
||||
checkGameRunning: () => Promise<{ running: boolean; processes: string[] }>;
|
||||
checkGitTools: () => Promise<{
|
||||
git: {
|
||||
ok: boolean;
|
||||
|
|
@ -156,6 +158,12 @@ interface Window {
|
|||
step: { name: string; result?: { ok: boolean; message?: string } }
|
||||
) => void
|
||||
) => () => void;
|
||||
onGitInstallProgress: (
|
||||
callback: (
|
||||
event: unknown,
|
||||
step: { name: string; result?: { ok: boolean; message?: string } }
|
||||
) => void
|
||||
) => () => void;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const APP_CONFIG = {
|
||||
serverBaseUrl: "https://pandoli365.com:5069/",
|
||||
serverBaseUrl: "https://pandoli365.com:5069",
|
||||
serverHealthcheckPath: "/launcher/ping",
|
||||
serverHealthcheckTimeoutMs: 2000,
|
||||
serverRequestTimeoutMs: 4000,
|
||||
|
|
|
|||
Loading…
Reference in New Issue