Enhance server health check functionality and update development script. Added server health check logic in main process, integrated with preload and renderer components, and modified the dev script to include a build step for the main process.
This commit is contained in:
parent
6e347224d3
commit
9a88d44769
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,7 @@
|
|||
"main": "dist/main/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
|
||||
"dev": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
|
||||
"build:renderer": "vite build",
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
"build": "yarn build:renderer && yarn build:main",
|
||||
|
|
|
|||
113
src/main/main.ts
113
src/main/main.ts
|
|
@ -1,14 +1,89 @@
|
|||
import { app, BrowserWindow } from "electron";
|
||||
import path from "node:path";
|
||||
import { app, BrowserWindow, ipcMain, net, session } from "electron";
|
||||
import * as path from "node:path";
|
||||
|
||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
||||
|
||||
type ServerHealthResult = {
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
latencyMs?: number;
|
||||
error?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
// SPT 서버는 기본적으로 self-signed TLS(자체서명 인증서)를 쓰는 경우가 많아서
|
||||
// 런처에서는 HTTPS + 인증서 예외 처리(certificate exception)를 고려합니다.
|
||||
const SERVER_BASE_URL = "https://pandoli365.com:5069/";
|
||||
const SERVER_HEALTHCHECK_PATH = "/launcher/ping";
|
||||
const SERVER_HEALTHCHECK_TIMEOUT_MS = 2000;
|
||||
|
||||
const checkServerHealth = async (): Promise<ServerHealthResult> => {
|
||||
const startedAt = Date.now();
|
||||
const url = new URL(SERVER_HEALTHCHECK_PATH, SERVER_BASE_URL).toString();
|
||||
|
||||
console.info(`[spt-launcher] healthcheck -> GET ${url}`);
|
||||
|
||||
return await new Promise<ServerHealthResult>((resolve) => {
|
||||
const request = net.request({
|
||||
method: "GET",
|
||||
url
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
request.abort();
|
||||
resolve({
|
||||
ok: false,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
error: "timeout",
|
||||
url
|
||||
});
|
||||
}, SERVER_HEALTHCHECK_TIMEOUT_MS);
|
||||
|
||||
request.on("response", (response) => {
|
||||
// data를 소비(consumption)하지 않으면 연결이 정리되지 않을 수 있어 drain 처리
|
||||
response.on("data", () => undefined);
|
||||
response.on("end", () => undefined);
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
const status = response.statusCode;
|
||||
console.info(
|
||||
`[spt-launcher] healthcheck <- ${status} (${Date.now() - startedAt}ms)`
|
||||
);
|
||||
resolve({
|
||||
ok: typeof status === "number" ? status >= 200 && status < 400 : false,
|
||||
status,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
url
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`[spt-launcher] healthcheck !! ${message} (${Date.now() - startedAt}ms)`
|
||||
);
|
||||
resolve({
|
||||
ok: false,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
error: message,
|
||||
url
|
||||
});
|
||||
});
|
||||
|
||||
request.end();
|
||||
});
|
||||
};
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 720,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/preload.js")
|
||||
preload: path.join(__dirname, "../preload/preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -20,6 +95,38 @@ const createWindow = () => {
|
|||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const expectedServerUrl = new URL(SERVER_BASE_URL);
|
||||
const expectedServerOrigin = expectedServerUrl.origin;
|
||||
const expectedServerHost = expectedServerUrl.hostname;
|
||||
// `electron.net` 요청은 self-signed TLS에서 막히는 경우가 많아서,
|
||||
// 특정 호스트(host)만 인증서 검증을 우회(bypass)합니다.
|
||||
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
||||
if (request.hostname === expectedServerHost) {
|
||||
callback(0);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(-3);
|
||||
});
|
||||
|
||||
app.on("certificate-error", (event, _webContents, url, _error, _certificate, callback) => {
|
||||
if (url.startsWith(expectedServerOrigin)) {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(false);
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:checkServerHealth", async () => {
|
||||
return await checkServerHealth();
|
||||
});
|
||||
|
||||
// 개발/운영 모두에서 부팅 시 1회 즉시 체크(instant check) 로그를 남겨
|
||||
// 엔드포인트(endpoint) 확인이 가능하도록 합니다.
|
||||
void checkServerHealth();
|
||||
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { contextBridge } from "electron";
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
console.info("[spt-launcher] preload loaded");
|
||||
|
||||
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||
appName: "SPT Launcher"
|
||||
appName: "SPT Launcher",
|
||||
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth")
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,38 +1,78 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type Screen = "serverCheck" | "login" | "signup" | "resetPassword" | "main";
|
||||
|
||||
const HEALTHCHECK_INTERVAL_MS = 10000;
|
||||
const HEALTHCHECK_ENDPOINT_FALLBACK = "https://pandoli365.com:5069/launcher/ping";
|
||||
|
||||
const App = () => {
|
||||
const [screen, setScreen] = useState<Screen>("serverCheck");
|
||||
const [serverHealthy, setServerHealthy] = useState(true);
|
||||
const [serverStatusCode, setServerStatusCode] = useState<number | undefined>();
|
||||
const [serverLatencyMs, setServerLatencyMs] = useState<number | undefined>();
|
||||
const [serverError, setServerError] = useState<string | undefined>();
|
||||
const [serverCheckedUrl, setServerCheckedUrl] = useState<string | undefined>();
|
||||
const [serverCheckInProgress, setServerCheckInProgress] = useState(false);
|
||||
const [serverCheckedAt, setServerCheckedAt] = useState<number | undefined>();
|
||||
const [hasSession, setHasSession] = useState(false);
|
||||
const [syncInProgress, setSyncInProgress] = useState(false);
|
||||
const [simulateSyncFail, setSimulateSyncFail] = useState(false);
|
||||
const healthCheckInFlightRef = useRef(false);
|
||||
|
||||
const runHealthCheck = useCallback(async () => {
|
||||
if (healthCheckInFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
healthCheckInFlightRef.current = true;
|
||||
setServerCheckInProgress(true);
|
||||
setServerError(undefined);
|
||||
|
||||
try {
|
||||
if (!window.sptLauncher?.checkServerHealth) {
|
||||
throw new Error("preload(preload) 미로딩: window.sptLauncher가 없습니다.");
|
||||
}
|
||||
|
||||
const result = await window.sptLauncher.checkServerHealth();
|
||||
setServerHealthy(result.ok);
|
||||
setServerStatusCode(result.status);
|
||||
setServerLatencyMs(result.latencyMs);
|
||||
setServerError(result.error);
|
||||
setServerCheckedUrl(result.url ?? HEALTHCHECK_ENDPOINT_FALLBACK);
|
||||
setServerCheckedAt(Date.now());
|
||||
|
||||
if (screen === "serverCheck" && result.ok) {
|
||||
setScreen(hasSession ? "main" : "login");
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setServerHealthy(false);
|
||||
setServerStatusCode(undefined);
|
||||
setServerLatencyMs(undefined);
|
||||
setServerError(message);
|
||||
setServerCheckedUrl(undefined);
|
||||
setServerCheckedAt(Date.now());
|
||||
} finally {
|
||||
healthCheckInFlightRef.current = false;
|
||||
setServerCheckInProgress(false);
|
||||
}
|
||||
}, [screen, hasSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (screen !== "serverCheck") {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setServerHealthy(true);
|
||||
setScreen(hasSession ? "main" : "login");
|
||||
}, 900);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [screen, hasSession]);
|
||||
void runHealthCheck();
|
||||
}, [screen, runHealthCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (screen !== "main") {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
setServerHealthy((current) => current);
|
||||
}, 5000);
|
||||
void runHealthCheck();
|
||||
}, HEALTHCHECK_INTERVAL_MS);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [screen]);
|
||||
}, [runHealthCheck]);
|
||||
|
||||
const serverStatusLabel = useMemo(() => {
|
||||
return serverHealthy ? "정상" : "불가";
|
||||
|
|
@ -92,8 +132,28 @@ const App = () => {
|
|||
{screen === "serverCheck" && (
|
||||
<section className="card hero">
|
||||
<h2>서버 상태 확인 중</h2>
|
||||
<p className="status idle">체크 중...</p>
|
||||
<p className="helper">서버 상태 확인 후 로그인 화면으로 이동합니다.</p>
|
||||
<p className={`status ${serverCheckInProgress ? "idle" : serverHealthy ? "ok" : "blocked"}`}>
|
||||
{serverCheckInProgress ? "체크 중..." : serverHealthy ? "정상" : "불가"}
|
||||
</p>
|
||||
<p className="helper">
|
||||
{serverHealthy
|
||||
? "서버 상태 확인 완료. 로그인 화면으로 이동합니다."
|
||||
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
|
||||
</p>
|
||||
<p className="notice">
|
||||
{serverCheckedUrl ? `Endpoint(endpoint): ${serverCheckedUrl}` : "Endpoint(endpoint): (unknown)"}
|
||||
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
|
||||
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
|
||||
{serverError ? ` · ${serverError}` : ""}
|
||||
{serverCheckedAt ? ` · ${new Date(serverCheckedAt).toLocaleTimeString()}` : ""}
|
||||
</p>
|
||||
{!serverCheckInProgress && !serverHealthy && (
|
||||
<>
|
||||
<button type="button" onClick={() => void runHealthCheck()}>
|
||||
다시 시도
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,12 @@
|
|||
interface Window {
|
||||
sptLauncher: {
|
||||
appName: string;
|
||||
checkServerHealth: () => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
latencyMs?: number;
|
||||
error?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue