fix: 서버 오픈 핸들러를 00초에 맞추도록 변경경
This commit is contained in:
+39
-32
@@ -2,6 +2,7 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import fsPromise from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -15,10 +16,10 @@ const FETCH_TIMEOUT_MS = nenv("FETCH_TIMEOUT_MS", 60_000);
|
|||||||
const IF_LOCKED_WAIT_MS = nenv("IF_LOCKED_WAIT_MS", 10_000);
|
const IF_LOCKED_WAIT_MS = nenv("IF_LOCKED_WAIT_MS", 10_000);
|
||||||
const IF_UPDATED_WAIT_MS = nenv("IF_UPDATED_WAIT_MS", 1_000);
|
const IF_UPDATED_WAIT_MS = nenv("IF_UPDATED_WAIT_MS", 1_000);
|
||||||
const DEFAULT_WAIT_MS = nenv("DEFAULT_WAIT_MS", 8_000);
|
const DEFAULT_WAIT_MS = nenv("DEFAULT_WAIT_MS", 8_000);
|
||||||
const AUTO_RESET_PERIOD_MS = nenv("AUTO_RESET_PERIOD_MS", 60_000);
|
|
||||||
const BASE_SCAN_DEPTH_ONE = (process.env.BASE_SCAN_DEPTH_ONE ?? "true").toLowerCase() === "true";
|
const BASE_SCAN_DEPTH_ONE = (process.env.BASE_SCAN_DEPTH_ONE ?? "true").toLowerCase() === "true";
|
||||||
const RESCAN_INTERVAL_MS = nenv("RESCAN_INTERVAL_MS", 300_000); // 5분
|
const RESCAN_INTERVAL_MS = nenv("RESCAN_INTERVAL_MS", 300_000); // 5분
|
||||||
const SHUTDOWN_TIMEOUT_MS = nenv("SHUTDOWN_TIMEOUT_MS", 15_000);
|
const SHUTDOWN_TIMEOUT_MS = nenv("SHUTDOWN_TIMEOUT_MS", 15_000);
|
||||||
|
const AUTO_RESET_JITTER_MS = nenv("AUTO_RESET_JITTER_MS", 0); // 0초 직후 지연(밀리초)
|
||||||
|
|
||||||
function nenv(key: string, def: number): number {
|
function nenv(key: string, def: number): number {
|
||||||
const v = process.env[key];
|
const v = process.env[key];
|
||||||
@@ -45,12 +46,14 @@ function nowStr() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function log(...args: any[]) { console.log(nowStr(), ...args); }
|
function log(...args: any[]) { console.log(nowStr(), ...args); }
|
||||||
|
|
||||||
function fileExists(fp: string): boolean {
|
async function fileExistsAsync(fp: string): Promise<boolean> {
|
||||||
try { fs.accessSync(fp, fs.constants.F_OK); return true; } catch { return false; }
|
try { await fsPromise.access(fp, fs.constants.F_OK); return true; } catch { return false; }
|
||||||
}
|
}
|
||||||
function readFileSafe(fp: string): string | null {
|
|
||||||
try { return fs.readFileSync(fp, "utf8"); } catch { return null; }
|
async function readFileSafeAsync(fp: string): Promise<string | null> {
|
||||||
|
try { return await fsPromise.readFile(fp, "utf8"); } catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function listDirsOnce(base: string): string[] {
|
function listDirsOnce(base: string): string[] {
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
for (const name of fs.readdirSync(base)) {
|
for (const name of fs.readdirSync(base)) {
|
||||||
@@ -65,11 +68,19 @@ function joinUrl(base: string, suffix: string): string {
|
|||||||
return base + suffix;
|
return base + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 분 0초 정렬 대기 시간 계산: 지금 시각에서 다음 분의 0초(+지터)까지 남은 ms
|
||||||
|
function msUntilNextMinuteZero(jitterMs = 0): number {
|
||||||
|
const now = Date.now();
|
||||||
|
const nextMinuteStart = Math.ceil(now / 60_000) * 60_000;
|
||||||
|
const target = nextMinuteStart + Math.max(0, jitterMs);
|
||||||
|
return Math.max(0, target - now);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 경로/루트 URL ============
|
// ============ 경로/루트 URL ============
|
||||||
const basepath = path.dirname(path.dirname(path.resolve(__filename)));
|
const basepath = path.dirname(path.dirname(path.resolve(__filename)));
|
||||||
function parseWebBaseFromCommonPath(base: string): string {
|
async function parseWebBaseFromCommonPath(base: string): Promise<string> {
|
||||||
const jsPath = path.join(base, "d_shared", "common_path.js");
|
const jsPath = path.join(base, "d_shared", "common_path.js");
|
||||||
const text = readFileSafe(jsPath);
|
const text = await readFileSafeAsync(jsPath);
|
||||||
if (!text) { log("[WARN] common_path.js 읽기 실패 → fallback=http://127.0.0.1"); return "http://127.0.0.1"; }
|
if (!text) { log("[WARN] common_path.js 읽기 실패 → fallback=http://127.0.0.1"); return "http://127.0.0.1"; }
|
||||||
for (const line of text.split(/\r?\n/)) {
|
for (const line of text.split(/\r?\n/)) {
|
||||||
if (line.includes("root:")) {
|
if (line.includes("root:")) {
|
||||||
@@ -81,7 +92,7 @@ function parseWebBaseFromCommonPath(base: string): string {
|
|||||||
log("[WARN] root: 항목 없음 → fallback=http://127.0.0.1");
|
log("[WARN] root: 항목 없음 → fallback=http://127.0.0.1");
|
||||||
return "http://127.0.0.1";
|
return "http://127.0.0.1";
|
||||||
}
|
}
|
||||||
const webBase = process.env.PUBLIC_CHECK_URL || parseWebBaseFromCommonPath(basepath);
|
const webBase = process.env.PUBLIC_CHECK_URL || await parseWebBaseFromCommonPath(basepath);
|
||||||
log("BASEPATH =", basepath);
|
log("BASEPATH =", basepath);
|
||||||
log("WEBBASE =", webBase);
|
log("WEBBASE =", webBase);
|
||||||
|
|
||||||
@@ -89,13 +100,12 @@ log("WEBBASE =", webBase);
|
|||||||
type ServerEntry = {
|
type ServerEntry = {
|
||||||
name: string; // servRelPath
|
name: string; // servRelPath
|
||||||
abs: string; // 절대 경로
|
abs: string; // 절대 경로
|
||||||
isHidden: () => boolean; // .htaccess 존재 여부
|
isHidden: () => Promise<boolean>; // .htaccess 존재 여부
|
||||||
hasAutoReset: boolean; // j_autoreset.php 파일 존재
|
|
||||||
procUrl: string;
|
procUrl: string;
|
||||||
autoresetUrl: string | null;
|
autoresetUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function scanOnce(): ServerEntry[] {
|
async function scanOnce(): Promise<ServerEntry[]> {
|
||||||
const dirs = listDirsOnce(basepath); // 깊이는 1단계만
|
const dirs = listDirsOnce(basepath); // 깊이는 1단계만
|
||||||
const out: ServerEntry[] = [];
|
const out: ServerEntry[] = [];
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
@@ -103,20 +113,17 @@ function scanOnce(): ServerEntry[] {
|
|||||||
// 필요하면 확장 로직 배치
|
// 필요하면 확장 로직 배치
|
||||||
}
|
}
|
||||||
const dbphp = path.join(dir, "d_setting", "DB.php");
|
const dbphp = path.join(dir, "d_setting", "DB.php");
|
||||||
if (!fileExists(dbphp)) continue;
|
if (!await fileExistsAsync(dbphp)) continue;
|
||||||
|
|
||||||
const name = path.relative(basepath, dir);
|
const name = path.relative(basepath, dir);
|
||||||
const isHidden = () => fileExists(path.join(dir, ".htaccess"));
|
const isHidden = () => fileExistsAsync(path.join(dir, ".htaccess"));
|
||||||
const resetAbs = path.join(dir, "j_autoreset.php");
|
|
||||||
const hasAutoReset = fileExists(resetAbs);
|
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
name,
|
name,
|
||||||
abs: dir,
|
abs: dir,
|
||||||
isHidden,
|
isHidden,
|
||||||
hasAutoReset,
|
|
||||||
procUrl: joinUrl(webBase, `/${name}/proc.php`),
|
procUrl: joinUrl(webBase, `/${name}/proc.php`),
|
||||||
autoresetUrl: hasAutoReset ? joinUrl(webBase, `/${name}/j_autoreset.php`) : null,
|
autoresetUrl:joinUrl(webBase, `/${name}/j_autoreset.php`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -132,7 +139,7 @@ async function isPublicReachable(signal?: AbortSignal): Promise<boolean> {
|
|||||||
const res = await fetch(webBase, { method: "GET", signal: composite });
|
const res = await fetch(webBase, { method: "GET", signal: composite });
|
||||||
if (!res.ok) { log(`[PUBLIC] 응답 ${res.status} → 비공개로 간주`); return false; }
|
if (!res.ok) { log(`[PUBLIC] 응답 ${res.status} → 비공개로 간주`); return false; }
|
||||||
return true;
|
return true;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.name !== "AbortError") log(`[PUBLIC] 체크 실패: ${e?.name || e}`);
|
if (e?.name !== "AbortError") log(`[PUBLIC] 체크 실패: ${e?.name || e}`);
|
||||||
return false;
|
return false;
|
||||||
@@ -217,24 +224,24 @@ class ServerRunner {
|
|||||||
const pub = await isPublicReachable(this.#stopCtrl.signal);
|
const pub = await isPublicReachable(this.#stopCtrl.signal);
|
||||||
if (!pub) { await delay(15_000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; }
|
if (!pub) { await delay(15_000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; }
|
||||||
|
|
||||||
const hidden = this.#entry.isHidden();
|
const hidden = await this.#entry.isHidden();
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
// 닫힘: autoreset
|
// ★ 매 분 0초에 맞춰 호출
|
||||||
if (this.#entry.autoresetUrl) {
|
const waitMs = msUntilNextMinuteZero(AUTO_RESET_JITTER_MS);
|
||||||
const now = Date.now();
|
if (waitMs > 0) {
|
||||||
if (now - this.#lastAutoResetAt >= AUTO_RESET_PERIOD_MS) {
|
// 신호 안전하게 대기
|
||||||
log(`[${this.name()}] 닫힘 - autoreset 호출`);
|
await delay(waitMs, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
|
||||||
await httpGetJson<Json>(this.#entry.autoresetUrl, this.#stopCtrl.signal);
|
if (this.#stopCtrl.signal.aborted) break;
|
||||||
this.#lastAutoResetAt = now;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(`[${this.name()}] 닫힘 - autoreset 없음`);
|
|
||||||
}
|
}
|
||||||
await delay(1000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
|
log(`[${this.name()}] 닫힘 - (정렬) autoreset 호출 @ mm:00${AUTO_RESET_JITTER_MS ? `+${AUTO_RESET_JITTER_MS}ms` : ""}`);
|
||||||
|
await httpGetJson<Json>(this.#entry.autoresetUrl, this.#stopCtrl.signal);
|
||||||
|
// 같은 분에 중복 호출 방지: 0초 직후 잠깐 쉬고 다음 루프는 자연스럽게 다음 분으로 정렬됨
|
||||||
|
await delay(200, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 열림: proc
|
// 열림: proc
|
||||||
const data = await httpGetJson<ProcResult>(this.#entry.procUrl, this.#stopCtrl.signal);
|
const data = await httpGetJson<ProcResult>(this.#entry.procUrl, this.#stopCtrl.signal);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -306,7 +313,7 @@ class DaemonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #rescanOnce() {
|
async #rescanOnce() {
|
||||||
const entries = scanOnce();
|
const entries = await scanOnce();
|
||||||
log(`[RESCAN] 서버 ${entries.length}개 발견`);
|
log(`[RESCAN] 서버 ${entries.length}개 발견`);
|
||||||
|
|
||||||
// 추가/갱신/삭제 판정
|
// 추가/갱신/삭제 판정
|
||||||
|
|||||||
Reference in New Issue
Block a user