1
0

fix: 安全性与代码质量加固(异常保护、外键级联、竞态修复、优雅关机)

This commit is contained in:
2026-05-11 14:24:12 +08:00
parent 35ba56888b
commit 0ee10b47c9
17 changed files with 132 additions and 33 deletions

View File

@@ -218,7 +218,12 @@ function createTargetsResponse(store: ProbeStore): TargetStatus[] {
function mapCheckResult(row: StoredCheckResult): CheckResult {
let failure: CheckFailure | null = null;
if (row.failure) {
failure = JSON.parse(row.failure) as CheckFailure;
try {
failure = JSON.parse(row.failure) as CheckFailure;
} catch {
console.warn(`无法解析 failure 数据: target_id=${row.target_id}, timestamp=${row.timestamp}`);
failure = null;
}
}
return {

View File

@@ -9,7 +9,7 @@ import type {
ResolvedCommandTarget,
ResolvedHttpTarget,
ResolvedTarget,
RuntimeConfig,
EngineRuntimeConfig,
TargetConfig,
TargetType,
} from "./types";
@@ -77,7 +77,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
return { host, port, dataDir, configDir, maxConcurrentChecks, targets };
}
function validateRuntime(runtime: RuntimeConfig): number {
function validateRuntime(runtime: EngineRuntimeConfig): number {
if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
if (

View File

@@ -15,7 +15,7 @@ export class ProbeEngine {
constructor(store: ProbeStore, targets: ResolvedTarget[], maxConcurrentChecks?: number) {
this.store = store;
this.targets = targets;
this.maxConcurrentChecks = maxConcurrentChecks ?? 10;
this.maxConcurrentChecks = maxConcurrentChecks ?? 20;
this.refreshCache();
}
@@ -86,6 +86,8 @@ export class ProbeEngine {
for (const result of results) {
if (result.status === "fulfilled") {
this.writeResult(result.value);
} else {
console.warn("探针执行失败:", result.reason);
}
}
}

View File

@@ -16,7 +16,3 @@ export function parseSize(value: string | number): number {
if (unit === "MB") return num * 1024 * 1024;
return num * 1024 * 1024 * 1024;
}
export const DEFAULT_MAX_BODY_BYTES = parseSize("100MB");
export const DEFAULT_MAX_OUTPUT_BYTES = parseSize("100MB");
export const DEFAULT_MAX_CONCURRENT_CHECKS = 20;

View File

@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS check_results (
duration_ms REAL,
status_detail TEXT,
failure TEXT,
FOREIGN KEY (target_id) REFERENCES targets(id)
FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE
)
`;
@@ -43,6 +43,7 @@ export class ProbeStore {
ensureDir(dirname(dbPath));
this.db = new Database(dbPath, { create: true });
this.db.run("PRAGMA journal_mode = WAL");
this.db.run("PRAGMA foreign_keys = ON");
this.db.run(CREATE_TARGETS_TABLE);
this.db.run(CREATE_CHECK_RESULTS_TABLE);
this.db.run(CREATE_INDEX);

View File

@@ -2,7 +2,7 @@ export type TargetType = "http" | "command";
export interface ProbeConfig {
server?: ServerConfig;
runtime?: RuntimeConfig;
runtime?: EngineRuntimeConfig;
defaults?: DefaultsConfig;
targets: TargetConfig[];
}
@@ -13,7 +13,7 @@ export interface ServerConfig {
dataDir?: string;
}
export interface RuntimeConfig {
export interface EngineRuntimeConfig {
maxConcurrentChecks?: number;
}

View File

@@ -14,6 +14,14 @@ async function main() {
const engine = new ProbeEngine(store, config.targets, config.maxConcurrentChecks);
engine.start();
const shutdown = () => {
engine.stop();
store.close();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
startServer({
config: { host: config.host, port: config.port },
mode: "development",

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { subtractHours } from "../utils/time";
interface TimeRangePickerProps {
from: string;
@@ -18,12 +19,6 @@ function toLocalDatetimeInput(date: Date): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function subtractHours(date: Date, hours: number): Date {
const result = new Date(date);
result.setTime(result.getTime() - hours * 60 * 60 * 1000);
return result;
}
export function TimeRangePicker({ from, to, onChange }: TimeRangePickerProps) {
const [activeShortcut, setActiveShortcut] = useState<string | null>("24h");

View File

@@ -1,21 +1,27 @@
import { useCallback, useState } from "react";
import { useCallback, useRef, useState } from "react";
import type { HistoryResponse } from "../../shared/api";
export function useHistory(targetId: number | null) {
const [data, setData] = useState<HistoryResponse>({ items: [], total: 0, page: 1, pageSize: 15 });
const [data, setData] = useState<HistoryResponse>({ items: [], total: 0, page: 1, pageSize: 20 });
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const fetchHistory = useCallback(
async (from: string, to: string, page = 1, pageSize = 15) => {
async (from: string, to: string, page = 1, pageSize = 20) => {
if (targetId === null) return;
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/targets/${targetId}/history?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&page=${page}&pageSize=${pageSize}`,
{ signal: controller.signal },
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -23,6 +29,7 @@ export function useHistory(targetId: number | null) {
const result = (await response.json()) as HistoryResponse;
setData(result);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(false);

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import type { TargetStatus } from "../../shared/api";
import { useTrend } from "./useTrend";
import { useHistory } from "./useHistory";
import { subtractHours } from "../utils/time";
export function useTargetDetail() {
const [selectedTarget, setSelectedTarget] = useState<TargetStatus | null>(null);
@@ -69,9 +70,3 @@ export function useTargetDetail() {
handlePageChange,
};
}
function subtractHours(date: Date, hours: number): Date {
const result = new Date(date);
result.setTime(result.getTime() - hours * 60 * 60 * 1000);
return result;
}

View File

@@ -1,21 +1,27 @@
import { useCallback, useState } from "react";
import { useCallback, useRef, useState } from "react";
import type { TrendPoint } from "../../shared/api";
export function useTrend(targetId: number | null) {
const [data, setData] = useState<TrendPoint[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const fetchTrend = useCallback(
async (from: string, to: string) => {
if (targetId === null) return;
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/targets/${targetId}/trend?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
{ signal: controller.signal },
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -23,6 +29,7 @@ export function useTrend(targetId: number | null) {
const result = (await response.json()) as TrendPoint[];
setData(result);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(false);

5
src/web/utils/time.ts Normal file
View File

@@ -0,0 +1,5 @@
export function subtractHours(date: Date, hours: number): Date {
const result = new Date(date);
result.setTime(result.getTime() - hours * 60 * 60 * 1000);
return result;
}