Harden support logging and debug setup
This commit is contained in:
parent
af73934b0f
commit
fc4fafa0d6
12
README.md
12
README.md
@ -189,6 +189,8 @@ Runtime files are stored in Electron's `userData` directory, including:
|
|||||||
- `package-logs/package_*.txt`
|
- `package-logs/package_*.txt`
|
||||||
- `item-logs/item_*.txt`
|
- `item-logs/item_*.txt`
|
||||||
|
|
||||||
|
`audit.log` and `trace.log` are rotated automatically. The current file is kept plus one `.old` backup, and outdated backups are purged automatically.
|
||||||
|
|
||||||
### Remote debug server
|
### Remote debug server
|
||||||
|
|
||||||
For headless or server-style troubleshooting, the app can expose a small authenticated HTTP debug API with live status and log tails.
|
For headless or server-style troubleshooting, the app can expose a small authenticated HTTP debug API with live status and log tails.
|
||||||
@ -204,12 +206,15 @@ Enable it by creating these files in the same runtime folder that contains `rd_d
|
|||||||
|
|
||||||
After startup, the app also writes `debug_ai_manifest.json` into the same runtime folder. This file is meant for support tooling and AI agents: it lists all available endpoints, the auth method, the related runtime files, and the one remaining external value the assistant may still need from you for remote access: the server IP or DNS name.
|
After startup, the app also writes `debug_ai_manifest.json` into the same runtime folder. This file is meant for support tooling and AI agents: it lists all available endpoints, the auth method, the related runtime files, and the one remaining external value the assistant may still need from you for remote access: the server IP or DNS name.
|
||||||
|
|
||||||
If you want extra support detail during a flaky or hard-to-reproduce issue, the app also maintains a `trace.log` plus `trace_config.json`. You can enable or disable the support trace from the app menu or remotely via the debug API.
|
If you want extra support detail during a flaky or hard-to-reproduce issue, the app also maintains a `trace.log` plus `trace_config.json`. You can enable or disable the support trace from the app menu or remotely via the debug API. By default, the support trace now auto-disables again after 2 hours so it does not stay enabled forever by accident.
|
||||||
|
|
||||||
|
The app menu under `Hilfe` also includes a `Debug-Setup prüfen` action. It verifies the current host/port/token/AI-manifest/trace setup locally and shows the exact local and remote URLs that support tooling can use.
|
||||||
|
|
||||||
Available endpoints after restart:
|
Available endpoints after restart:
|
||||||
|
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /meta`
|
- `GET /meta`
|
||||||
|
- `GET /debug/setup`
|
||||||
- `GET /host/diagnostics`
|
- `GET /host/diagnostics`
|
||||||
- `GET /status`
|
- `GET /status`
|
||||||
- `GET /settings`
|
- `GET /settings`
|
||||||
@ -226,7 +231,7 @@ Available endpoints after restart:
|
|||||||
- `GET /logs/session?lines=100&grep=keyword`
|
- `GET /logs/session?lines=100&grep=keyword`
|
||||||
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
||||||
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
||||||
- `GET /trace/config?enable=1¬e=support`
|
- `GET /trace/config?enable=1¬e=support&durationMinutes=120`
|
||||||
- `GET /support/bundle`
|
- `GET /support/bundle`
|
||||||
- `GET /diagnostics?package=Release&lines=150`
|
- `GET /diagnostics?package=Release&lines=150`
|
||||||
|
|
||||||
@ -243,9 +248,10 @@ Invoke-RestMethod "http://SERVER:9868/settings?token=YOUR_TOKEN"
|
|||||||
Invoke-RestMethod "http://SERVER:9868/accounts?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/accounts?token=YOUR_TOKEN"
|
||||||
Invoke-RestMethod "http://SERVER:9868/stats?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/stats?token=YOUR_TOKEN"
|
||||||
Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
|
Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
|
||||||
|
Invoke-RestMethod "http://SERVER:9868/debug/setup?token=YOUR_TOKEN"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/trace?token=YOUR_TOKEN&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/trace?token=YOUR_TOKEN&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1¬e=support"
|
Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1¬e=support&durationMinutes=120"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
||||||
|
|||||||
@ -37,10 +37,11 @@ import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } fro
|
|||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||||
|
import { getDebugSetupCheck } from "./debug-setup";
|
||||||
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
||||||
import type { SupportTraceConfig } from "../shared/types";
|
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||||
|
|
||||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||||
@ -198,13 +199,17 @@ export class AppController {
|
|||||||
return rotated;
|
return rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||||
|
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
||||||
logAuditEvent(level, message, fields);
|
logAuditEvent(level, message, fields);
|
||||||
logTraceEvent(level, "audit", message, fields);
|
logTraceEvent(level, "audit", message, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
||||||
const next = setTraceEnabled(enabled, note);
|
const next = setTraceEnabled(enabled, note, durationMs);
|
||||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import path from "node:path";
|
|||||||
|
|
||||||
type AuditLevel = "INFO" | "WARN" | "ERROR";
|
type AuditLevel = "INFO" | "WARN" | "ERROR";
|
||||||
|
|
||||||
|
const AUDIT_LOG_MAX_FILE_BYTES = Number(process.env.RD_AUDIT_LOG_MAX_BYTES || 10 * 1024 * 1024);
|
||||||
|
const AUDIT_LOG_RETENTION_DAYS = Number(process.env.RD_AUDIT_LOG_RETENTION_DAYS || 30);
|
||||||
|
|
||||||
let auditLogPath: string | null = null;
|
let auditLogPath: string | null = null;
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
function sanitizeFieldValue(value: unknown): string {
|
||||||
@ -32,10 +35,46 @@ function formatFields(fields?: Record<string, unknown>): string {
|
|||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rotateIfNeeded(filePath: string): void {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size < AUDIT_LOG_MAX_FILE_BYTES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const backup = `${filePath}.old`;
|
||||||
|
try {
|
||||||
|
fs.rmSync(backup, { force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
fs.renameSync(filePath, backup);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupOldBackup(filePath: string): void {
|
||||||
|
const backup = `${filePath}.old`;
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(backup);
|
||||||
|
const cutoff = Date.now() - AUDIT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
|
if (stat.mtimeMs < cutoff) {
|
||||||
|
fs.rmSync(backup, { force: true });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initAuditLog(baseDir: string): void {
|
export function initAuditLog(baseDir: string): void {
|
||||||
auditLogPath = path.join(baseDir, "audit.log");
|
auditLogPath = path.join(baseDir, "audit.log");
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
||||||
|
cleanupOldBackup(auditLogPath);
|
||||||
|
if (!fs.existsSync(auditLogPath)) {
|
||||||
|
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||||
|
}
|
||||||
|
rotateIfNeeded(auditLogPath);
|
||||||
if (!fs.existsSync(auditLogPath)) {
|
if (!fs.existsSync(auditLogPath)) {
|
||||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||||
}
|
}
|
||||||
@ -50,6 +89,10 @@ export function logAuditEvent(level: AuditLevel, message: string, fields?: Recor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
rotateIfNeeded(auditLogPath);
|
||||||
|
if (!fs.existsSync(auditLogPath)) {
|
||||||
|
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||||
|
}
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
auditLogPath,
|
auditLogPath,
|
||||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { getAuditLogPath } from "./audit-log";
|
import { getAuditLogPath } from "./audit-log";
|
||||||
|
import { getDebugSetupCheck } from "./debug-setup";
|
||||||
import { logger, getLogFilePath } from "./logger";
|
import { logger, getLogFilePath } from "./logger";
|
||||||
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
||||||
import { getSessionLogPath } from "./session-log";
|
import { getSessionLogPath } from "./session-log";
|
||||||
@ -11,7 +12,7 @@ import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
|||||||
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
||||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, updateTraceConfig } from "./trace-log";
|
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
|
||||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||||
import type { DownloadManager } from "./download-manager";
|
import type { DownloadManager } from "./download-manager";
|
||||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||||
@ -31,6 +32,7 @@ type DebugEndpointDescriptor = {
|
|||||||
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||||
{ method: "GET", path: "/health", description: "Basic health, uptime, and memory information." },
|
{ method: "GET", path: "/health", description: "Basic health, uptime, and memory information." },
|
||||||
{ method: "GET", path: "/meta", description: "Lists runtime metadata and all available endpoints." },
|
{ method: "GET", path: "/meta", description: "Lists runtime metadata and all available endpoints." },
|
||||||
|
{ method: "GET", path: "/debug/setup", description: "Checks whether the local debug setup is configured for support." },
|
||||||
{ method: "GET", path: "/host/diagnostics", description: "Returns Windows host crash and dump diagnostics." },
|
{ method: "GET", path: "/host/diagnostics", description: "Returns Windows host crash and dump diagnostics." },
|
||||||
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
|
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
|
||||||
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
|
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
|
||||||
@ -39,7 +41,7 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
|||||||
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
||||||
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
||||||
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
||||||
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support", description: "Reads or updates the support trace configuration." },
|
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
||||||
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
||||||
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
||||||
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
||||||
@ -236,6 +238,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
||||||
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
||||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
||||||
|
"Use /debug/setup to quickly verify whether token, host, manifest, and trace files are in a good support state.",
|
||||||
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
||||||
"If a full handoff is needed, download /support/bundle as a ZIP."
|
"If a full handoff is needed, download /support/bundle as a ZIP."
|
||||||
],
|
],
|
||||||
@ -270,6 +273,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
||||||
remoteHostHint
|
remoteHostHint
|
||||||
},
|
},
|
||||||
|
setupCheckEndpoint: "/debug/setup",
|
||||||
askUserFor: [
|
askUserFor: [
|
||||||
"Server IP or DNS name, if remote access is required and not already known."
|
"Server IP or DNS name, if remote access is required and not already known."
|
||||||
],
|
],
|
||||||
@ -470,6 +474,9 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
traceConfig: getTraceConfigPath(),
|
traceConfig: getTraceConfigPath(),
|
||||||
traceLog: getTraceLogPath()
|
traceLog: getTraceLogPath()
|
||||||
},
|
},
|
||||||
|
supportChecks: {
|
||||||
|
setup: "/debug/setup"
|
||||||
|
},
|
||||||
logPaths: {
|
logPaths: {
|
||||||
main: getLogFilePath(),
|
main: getLogFilePath(),
|
||||||
audit: getAuditLogPath(),
|
audit: getAuditLogPath(),
|
||||||
@ -481,6 +488,11 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === "/debug/setup") {
|
||||||
|
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname === "/host/diagnostics") {
|
if (pathname === "/host/diagnostics") {
|
||||||
jsonResponse(res, 200, getWindowsHostDiagnostics());
|
jsonResponse(res, 200, getWindowsHostDiagnostics());
|
||||||
return;
|
return;
|
||||||
@ -554,11 +566,21 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
patch.logDebugRequests = logDebugRequests;
|
patch.logDebugRequests = logDebugRequests;
|
||||||
}
|
}
|
||||||
const note = String(url.searchParams.get("note") || "").trim();
|
const note = String(url.searchParams.get("note") || "").trim();
|
||||||
const config = Object.keys(patch).length > 0
|
const durationMinutesRaw = Number(url.searchParams.get("durationMinutes") || "120");
|
||||||
? updateTraceConfig({ ...patch, ...(note ? { updatedAt: new Date().toISOString() } : {}) })
|
const durationMinutes = Number.isFinite(durationMinutesRaw) && durationMinutesRaw > 0
|
||||||
: getTraceConfig();
|
? Math.min(Math.floor(durationMinutesRaw), 24 * 60)
|
||||||
|
: 120;
|
||||||
|
let config = getTraceConfig();
|
||||||
|
if (enabled !== null) {
|
||||||
|
config = setTraceEnabled(enabled, note, durationMinutes * 60 * 1000);
|
||||||
|
}
|
||||||
|
const configPatch = { ...patch };
|
||||||
|
delete configPatch.enabled;
|
||||||
|
if (Object.keys(configPatch).length > 0) {
|
||||||
|
config = updateTraceConfig(configPatch);
|
||||||
|
}
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note });
|
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note, durationMinutes });
|
||||||
}
|
}
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
path: getTraceConfigPath(),
|
path: getTraceConfigPath(),
|
||||||
@ -797,7 +819,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
debugServer: {
|
debugServer: {
|
||||||
host: bindHost,
|
host: bindHost,
|
||||||
port: bindPort
|
port: bindPort
|
||||||
}
|
},
|
||||||
|
setup: getDebugSetupCheck(runtimeBaseDir)
|
||||||
},
|
},
|
||||||
status: buildStatusPayload(snapshot),
|
status: buildStatusPayload(snapshot),
|
||||||
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
||||||
|
|||||||
133
src/main/debug-setup.ts
Normal file
133
src/main/debug-setup.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 9868;
|
||||||
|
const DEFAULT_HOST = "127.0.0.1";
|
||||||
|
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
||||||
|
|
||||||
|
function readToken(baseDir: string): string {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(baseDir, "debug_token.txt"), "utf8").trim();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPort(baseDir: string): number {
|
||||||
|
try {
|
||||||
|
const raw = Number(fs.readFileSync(path.join(baseDir, "debug_port.txt"), "utf8").trim());
|
||||||
|
if (Number.isFinite(raw) && raw >= 1024 && raw <= 65535) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHost(baseDir: string): string {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(path.join(baseDir, "debug_host.txt"), "utf8").trim();
|
||||||
|
if (!raw) {
|
||||||
|
return DEFAULT_HOST;
|
||||||
|
}
|
||||||
|
if (/^(localhost|0\.0\.0\.0|127\.0\.0\.1|::1)$/i.test(raw)) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (/^[a-z0-9.-]+$/i.test(raw)) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return DEFAULT_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTraceConfig(baseDir: string): SupportTraceConfig {
|
||||||
|
const fallback: SupportTraceConfig = {
|
||||||
|
enabled: false,
|
||||||
|
includeMainLog: true,
|
||||||
|
includeAudit: true,
|
||||||
|
logDebugRequests: true,
|
||||||
|
autoDisableAt: null,
|
||||||
|
updatedAt: new Date(0).toISOString()
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const filePath = path.join(baseDir, "trace_config.json");
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as Partial<SupportTraceConfig>;
|
||||||
|
return {
|
||||||
|
enabled: Boolean(parsed.enabled),
|
||||||
|
includeMainLog: parsed.includeMainLog === undefined ? true : Boolean(parsed.includeMainLog),
|
||||||
|
includeAudit: parsed.includeAudit === undefined ? true : Boolean(parsed.includeAudit),
|
||||||
|
logDebugRequests: parsed.logDebugRequests === undefined ? true : Boolean(parsed.logDebugRequests),
|
||||||
|
autoDisableAt: typeof parsed.autoDisableAt === "string" && parsed.autoDisableAt.trim() ? parsed.autoDisableAt : null,
|
||||||
|
updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.trim() ? parsed.updatedAt : fallback.updatedAt
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||||
|
const host = readHost(baseDir);
|
||||||
|
const port = readPort(baseDir);
|
||||||
|
const token = readToken(baseDir);
|
||||||
|
const tokenPath = path.join(baseDir, "debug_token.txt");
|
||||||
|
const aiManifestPath = path.join(baseDir, AI_MANIFEST_FILE);
|
||||||
|
const traceConfigPath = path.join(baseDir, "trace_config.json");
|
||||||
|
const traceLogPath = path.join(baseDir, "trace.log");
|
||||||
|
const traceConfig = readTraceConfig(baseDir);
|
||||||
|
const localOnly = /^(127\.0\.0\.1|localhost|::1)$/i.test(host);
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const notes: string[] = [];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
||||||
|
}
|
||||||
|
if (localOnly) {
|
||||||
|
warnings.push("Der Debug-Server ist aktuell nur lokal erreichbar. Für Remote-Support debug_host.txt auf 0.0.0.0 setzen.");
|
||||||
|
} else {
|
||||||
|
notes.push("Der Debug-Server ist für Remote-Zugriff konfiguriert. Firewall oder Provider-Regeln müssen separat offen sein.");
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(aiManifestPath)) {
|
||||||
|
warnings.push("debug_ai_manifest.json fehlt. App einmal neu starten, damit die KI-Support-Datei neu geschrieben wird.");
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(traceConfigPath)) {
|
||||||
|
warnings.push("trace_config.json fehlt. Trace-Funktionen sind lokal noch nicht initialisiert.");
|
||||||
|
}
|
||||||
|
if (traceConfig.enabled && !traceConfig.autoDisableAt) {
|
||||||
|
warnings.push("Support-Trace ist aktiv ohne automatische Abschaltzeit. Einmal neu aktivieren, damit die 2-Stunden-Begrenzung gesetzt wird.");
|
||||||
|
}
|
||||||
|
if (traceConfig.enabled && traceConfig.autoDisableAt) {
|
||||||
|
notes.push(`Support-Trace aktiv bis ${traceConfig.autoDisableAt}.`);
|
||||||
|
}
|
||||||
|
notes.push("Die App kann Netzwerk-Firewalls oder Provider-Sicherheitsgruppen nicht direkt prüfen.");
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: Boolean(token),
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
localOnly,
|
||||||
|
tokenConfigured: Boolean(token),
|
||||||
|
tokenPath,
|
||||||
|
aiManifestPath,
|
||||||
|
aiManifestPresent: fs.existsSync(aiManifestPath),
|
||||||
|
traceConfigPath: fs.existsSync(traceConfigPath) ? traceConfigPath : null,
|
||||||
|
traceLogPath: fs.existsSync(traceLogPath) ? traceLogPath : null,
|
||||||
|
traceEnabled: traceConfig.enabled,
|
||||||
|
traceAutoDisableAt: traceConfig.autoDisableAt,
|
||||||
|
warnings,
|
||||||
|
notes,
|
||||||
|
localUrls: {
|
||||||
|
health: `http://127.0.0.1:${port}/health?token=${token || "<TOKEN>"}`,
|
||||||
|
meta: `http://127.0.0.1:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||||
|
diagnostics: `http://127.0.0.1:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||||
|
},
|
||||||
|
remoteUrlTemplates: {
|
||||||
|
health: `http://<SERVER_IP_OR_DNS>:${port}/health?token=${token || "<TOKEN>"}`,
|
||||||
|
meta: `http://<SERVER_IP_OR_DNS>:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||||
|
diagnostics: `http://<SERVER_IP_OR_DNS>:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -538,16 +538,21 @@ function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string) => {
|
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
|
||||||
if (typeof enabled !== "boolean") {
|
if (typeof enabled !== "boolean") {
|
||||||
throw new Error("enabled muss ein Boolean sein");
|
throw new Error("enabled muss ein Boolean sein");
|
||||||
}
|
}
|
||||||
if (note !== undefined) {
|
if (note !== undefined) {
|
||||||
validateString(note, "note");
|
validateString(note, "note");
|
||||||
}
|
}
|
||||||
return controller.setTraceEnabled(enabled, note);
|
if (durationMinutes !== undefined && (!Number.isFinite(durationMinutes) || durationMinutes <= 0)) {
|
||||||
|
throw new Error("durationMinutes muss eine positive Zahl sein");
|
||||||
|
}
|
||||||
|
return controller.setTraceEnabled(enabled, note, durationMinutes ? durationMinutes * 60 * 1000 : undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import path from "node:path";
|
|||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { getAuditLogPath } from "./audit-log";
|
import { getAuditLogPath } from "./audit-log";
|
||||||
|
import { getDebugSetupCheck } from "./debug-setup";
|
||||||
import { getLogFilePath } from "./logger";
|
import { getLogFilePath } from "./logger";
|
||||||
import { getPackageLogPath } from "./package-log";
|
import { getPackageLogPath } from "./package-log";
|
||||||
import { getSessionLogPath } from "./session-log";
|
import { getSessionLogPath } from "./session-log";
|
||||||
@ -89,6 +90,7 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
addJson(zip, "overview/debug-setup.json", getDebugSetupCheck(baseDir));
|
||||||
addJson(zip, "overview/history.json", {
|
addJson(zip, "overview/history.json", {
|
||||||
total: history.length,
|
total: history.length,
|
||||||
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
||||||
@ -115,8 +117,10 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
||||||
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
||||||
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
||||||
|
addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
|
||||||
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
||||||
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
||||||
|
addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old");
|
||||||
|
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
||||||
|
|||||||
@ -7,12 +7,16 @@ type TraceLevel = "INFO" | "WARN" | "ERROR";
|
|||||||
|
|
||||||
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
||||||
const TRACE_CONFIG_FILE = "trace_config.json";
|
const TRACE_CONFIG_FILE = "trace_config.json";
|
||||||
|
const TRACE_LOG_MAX_FILE_BYTES = Number(process.env.RD_TRACE_LOG_MAX_BYTES || 10 * 1024 * 1024);
|
||||||
|
const TRACE_LOG_RETENTION_DAYS = Number(process.env.RD_TRACE_LOG_RETENTION_DAYS || 30);
|
||||||
|
const TRACE_DEFAULT_AUTO_DISABLE_MS = Number(process.env.RD_TRACE_AUTO_DISABLE_MS || 2 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
includeMainLog: true,
|
includeMainLog: true,
|
||||||
includeAudit: true,
|
includeAudit: true,
|
||||||
logDebugRequests: true,
|
logDebugRequests: true,
|
||||||
|
autoDisableAt: null,
|
||||||
updatedAt: new Date(0).toISOString()
|
updatedAt: new Date(0).toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -21,6 +25,7 @@ let traceConfigPath: string | null = null;
|
|||||||
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||||
let pendingLines: string[] = [];
|
let pendingLines: string[] = [];
|
||||||
let flushTimer: NodeJS.Timeout | null = null;
|
let flushTimer: NodeJS.Timeout | null = null;
|
||||||
|
let autoDisableTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
function sanitizeFieldValue(value: unknown): string {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
@ -62,6 +67,37 @@ function flushPending(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rotateIfNeeded(filePath: string): void {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size < TRACE_LOG_MAX_FILE_BYTES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const backup = `${filePath}.old`;
|
||||||
|
try {
|
||||||
|
fs.rmSync(backup, { force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
fs.renameSync(filePath, backup);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupOldBackup(filePath: string): void {
|
||||||
|
const backup = `${filePath}.old`;
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(backup);
|
||||||
|
const cutoff = Date.now() - TRACE_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
|
if (stat.mtimeMs < cutoff) {
|
||||||
|
fs.rmSync(backup, { force: true });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleFlush(): void {
|
function scheduleFlush(): void {
|
||||||
if (flushTimer) {
|
if (flushTimer) {
|
||||||
return;
|
return;
|
||||||
@ -76,6 +112,14 @@ function appendTraceLine(line: string): void {
|
|||||||
if (!traceLogPath) {
|
if (!traceLogPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rotateIfNeeded(traceLogPath);
|
||||||
|
if (!fs.existsSync(traceLogPath)) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
pendingLines.push(line);
|
pendingLines.push(line);
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
}
|
}
|
||||||
@ -90,6 +134,9 @@ function normalizeTraceConfig(raw: unknown): SupportTraceConfig {
|
|||||||
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
||||||
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
||||||
logDebugRequests: value.logDebugRequests === undefined ? DEFAULT_TRACE_CONFIG.logDebugRequests : Boolean(value.logDebugRequests),
|
logDebugRequests: value.logDebugRequests === undefined ? DEFAULT_TRACE_CONFIG.logDebugRequests : Boolean(value.logDebugRequests),
|
||||||
|
autoDisableAt: typeof value.autoDisableAt === "string" && value.autoDisableAt.trim()
|
||||||
|
? value.autoDisableAt
|
||||||
|
: null,
|
||||||
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
||||||
? value.updatedAt
|
? value.updatedAt
|
||||||
: DEFAULT_TRACE_CONFIG.updatedAt
|
: DEFAULT_TRACE_CONFIG.updatedAt
|
||||||
@ -126,11 +173,58 @@ const mainLogListener = (line: string): void => {
|
|||||||
appendTraceLine(line);
|
appendTraceLine(line);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function clearAutoDisableTimer(): void {
|
||||||
|
if (autoDisableTimer) {
|
||||||
|
clearTimeout(autoDisableTimer);
|
||||||
|
autoDisableTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableTraceDueToExpiry(): void {
|
||||||
|
clearAutoDisableTimer();
|
||||||
|
if (!traceConfig.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
traceConfig = normalizeTraceConfig({
|
||||||
|
...traceConfig,
|
||||||
|
enabled: false,
|
||||||
|
autoDisableAt: null,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
persistTraceConfig();
|
||||||
|
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace automatisch deaktiviert | reason=expired\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleAutoDisable(): void {
|
||||||
|
clearAutoDisableTimer();
|
||||||
|
if (!traceConfig.enabled || !traceConfig.autoDisableAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const until = Date.parse(traceConfig.autoDisableAt);
|
||||||
|
if (!Number.isFinite(until)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const remainingMs = until - Date.now();
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
disableTraceDueToExpiry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
autoDisableTimer = setTimeout(() => {
|
||||||
|
autoDisableTimer = null;
|
||||||
|
disableTraceDueToExpiry();
|
||||||
|
}, Math.min(remainingMs, 2_147_483_647));
|
||||||
|
}
|
||||||
|
|
||||||
export function initTraceLog(baseDir: string): void {
|
export function initTraceLog(baseDir: string): void {
|
||||||
traceLogPath = path.join(baseDir, "trace.log");
|
traceLogPath = path.join(baseDir, "trace.log");
|
||||||
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(baseDir, { recursive: true });
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
|
cleanupOldBackup(traceLogPath);
|
||||||
|
if (!fs.existsSync(traceLogPath)) {
|
||||||
|
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||||
|
}
|
||||||
|
rotateIfNeeded(traceLogPath);
|
||||||
if (!fs.existsSync(traceLogPath)) {
|
if (!fs.existsSync(traceLogPath)) {
|
||||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||||
}
|
}
|
||||||
@ -144,6 +238,7 @@ export function initTraceLog(baseDir: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addLogListener(mainLogListener);
|
addLogListener(mainLogListener);
|
||||||
|
scheduleAutoDisable();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTraceLogPath(): string | null {
|
export function getTraceLogPath(): string | null {
|
||||||
@ -171,13 +266,17 @@ export function updateTraceConfig(patch: Partial<SupportTraceConfig>): SupportTr
|
|||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
});
|
});
|
||||||
persistTraceConfig();
|
persistTraceConfig();
|
||||||
|
scheduleAutoDisable();
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
||||||
return getTraceConfig();
|
return getTraceConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
export function setTraceEnabled(enabled: boolean, note = "", durationMs: number = TRACE_DEFAULT_AUTO_DISABLE_MS): SupportTraceConfig {
|
||||||
const next = updateTraceConfig({ enabled });
|
const autoDisableAt = enabled && durationMs > 0
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note })}\n`);
|
? new Date(Date.now() + durationMs).toISOString()
|
||||||
|
: null;
|
||||||
|
const next = updateTraceConfig({ enabled, autoDisableAt });
|
||||||
|
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note, autoDisableAt })}\n`);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +297,7 @@ export function logTraceEvent(
|
|||||||
|
|
||||||
export function shutdownTraceLog(): void {
|
export function shutdownTraceLog(): void {
|
||||||
removeLogListener(mainLogListener);
|
removeLogListener(mainLogListener);
|
||||||
|
clearAutoDisableTimer();
|
||||||
if (!traceLogPath) {
|
if (!traceLogPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,8 +63,9 @@ const api: ElectronApi = {
|
|||||||
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
|
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
|
||||||
openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId),
|
openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId),
|
||||||
openItemLog: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ITEM_LOG, itemId),
|
openItemLog: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ITEM_LOG, itemId),
|
||||||
|
getDebugSetupCheck: () => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK),
|
||||||
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
||||||
setTraceEnabled: (enabled: boolean, note?: string) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note),
|
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note, durationMinutes),
|
||||||
rotateDebugToken: (): Promise<{ path: string }> => ipcRenderer.invoke(IPC_CHANNELS.ROTATE_DEBUG_TOKEN),
|
rotateDebugToken: (): Promise<{ path: string }> => ipcRenderer.invoke(IPC_CHANNELS.ROTATE_DEBUG_TOKEN),
|
||||||
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
||||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
AppSettings,
|
AppSettings,
|
||||||
AppTheme,
|
AppTheme,
|
||||||
BandwidthScheduleEntry,
|
BandwidthScheduleEntry,
|
||||||
|
DebugSetupCheckResult,
|
||||||
DebridFallbackProvider,
|
DebridFallbackProvider,
|
||||||
DebridLinkHostLimitInfo,
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
@ -49,8 +50,10 @@ interface ConfirmPromptState {
|
|||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
confirmLabel: string;
|
confirmLabel: string;
|
||||||
|
cancelLabel?: string;
|
||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
details?: string;
|
details?: string;
|
||||||
|
detailsLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContextMenuState {
|
interface ContextMenuState {
|
||||||
@ -137,6 +140,44 @@ interface ConfiguredAccountEntry {
|
|||||||
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||||
|
const lines: string[] = [
|
||||||
|
`Debug-Server aktiv: ${setup.enabled ? "ja" : "nein"}`,
|
||||||
|
`Host: ${setup.host}`,
|
||||||
|
`Port: ${setup.port}`,
|
||||||
|
`Token-Datei: ${setup.tokenPath}`,
|
||||||
|
`KI-Manifest: ${setup.aiManifestPresent ? "vorhanden" : "fehlt"} (${setup.aiManifestPath})`,
|
||||||
|
`Trace aktiv: ${setup.traceEnabled ? "ja" : "nein"}`,
|
||||||
|
`Trace-Auto-Ende: ${setup.traceAutoDisableAt || "nicht gesetzt"}`,
|
||||||
|
"",
|
||||||
|
"Lokale URLs:",
|
||||||
|
setup.localUrls.health,
|
||||||
|
setup.localUrls.meta,
|
||||||
|
setup.localUrls.diagnostics,
|
||||||
|
"",
|
||||||
|
"Remote-Vorlagen:",
|
||||||
|
setup.remoteUrlTemplates.health,
|
||||||
|
setup.remoteUrlTemplates.meta,
|
||||||
|
setup.remoteUrlTemplates.diagnostics
|
||||||
|
];
|
||||||
|
|
||||||
|
if (setup.warnings.length > 0) {
|
||||||
|
lines.push("", "Warnungen:");
|
||||||
|
for (const warning of setup.warnings) {
|
||||||
|
lines.push(`- ${warning}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setup.notes.length > 0) {
|
||||||
|
lines.push("", "Hinweise:");
|
||||||
|
for (const note of setup.notes) {
|
||||||
|
lines.push(`- ${note}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
const ACCOUNT_OPTIONS: AccountOption[] = [
|
const ACCOUNT_OPTIONS: AccountOption[] = [
|
||||||
{
|
{
|
||||||
kind: "realdebrid-api",
|
kind: "realdebrid-api",
|
||||||
@ -3413,14 +3454,34 @@ export function App(): ReactElement {
|
|||||||
closeMenus();
|
closeMenus();
|
||||||
const nextEnabled = !supportTraceEnabled;
|
const nextEnabled = !supportTraceEnabled;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle");
|
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle", 120);
|
||||||
setSupportTraceEnabled(result.enabled);
|
setSupportTraceEnabled(result.enabled);
|
||||||
showToast(result.enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", 2400);
|
showToast(result.enabled ? "Support-Trace für 2 Stunden aktiviert" : "Support-Trace deaktiviert", 2600);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Support-Trace fehlgeschlagen: ${String(error)}`, 2800);
|
showToast(`Support-Trace fehlgeschlagen: ${String(error)}`, 2800);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRunDebugSetupCheck = async (): Promise<void> => {
|
||||||
|
closeMenus();
|
||||||
|
try {
|
||||||
|
const setup = await window.rd.getDebugSetupCheck();
|
||||||
|
const warningText = setup.warnings.length > 0 ? `Warnungen: ${setup.warnings.length}` : "Keine akuten Warnungen";
|
||||||
|
const reachabilityText = setup.localOnly ? "Nur lokal gebunden" : "Remote-fähig konfiguriert";
|
||||||
|
const details = buildDebugSetupDetails(setup);
|
||||||
|
await askConfirmPrompt({
|
||||||
|
title: "Debug-Setup prüfen",
|
||||||
|
message: `${warningText}\n${reachabilityText}\nHost: ${setup.host}:${setup.port}`,
|
||||||
|
confirmLabel: "Schließen",
|
||||||
|
cancelLabel: "Schließen",
|
||||||
|
details,
|
||||||
|
detailsLabel: "Details anzeigen"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Debug-Setup-Check fehlgeschlagen: ${String(error)}`, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onRotateDebugToken = async (): Promise<void> => {
|
const onRotateDebugToken = async (): Promise<void> => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
const confirmed = await askConfirmPrompt({
|
const confirmed = await askConfirmPrompt({
|
||||||
@ -3767,6 +3828,9 @@ export function App(): ReactElement {
|
|||||||
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
||||||
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}>
|
||||||
|
<span>Debug-Setup prüfen</span>
|
||||||
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
||||||
<span>Debug-Token rotieren</span>
|
<span>Debug-Token rotieren</span>
|
||||||
</button>
|
</button>
|
||||||
@ -4855,12 +4919,12 @@ export function App(): ReactElement {
|
|||||||
<p style={{ whiteSpace: "pre-line" }}>{confirmPrompt.message}</p>
|
<p style={{ whiteSpace: "pre-line" }}>{confirmPrompt.message}</p>
|
||||||
{confirmPrompt.details && (
|
{confirmPrompt.details && (
|
||||||
<details className="modal-details">
|
<details className="modal-details">
|
||||||
<summary>Changelog anzeigen</summary>
|
<summary>{confirmPrompt.detailsLabel || "Details anzeigen"}</summary>
|
||||||
<pre>{confirmPrompt.details}</pre>
|
<pre>{confirmPrompt.details}</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn" onClick={() => closeConfirmPrompt(false)}>Abbrechen</button>
|
<button className="btn" onClick={() => closeConfirmPrompt(false)}>{confirmPrompt.cancelLabel || "Abbrechen"}</button>
|
||||||
<button
|
<button
|
||||||
className={confirmPrompt.danger ? "btn danger" : "btn"}
|
className={confirmPrompt.danger ? "btn danger" : "btn"}
|
||||||
onClick={() => closeConfirmPrompt(true)}
|
onClick={() => closeConfirmPrompt(true)}
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export const IPC_CHANNELS = {
|
|||||||
OPEN_TRACE_LOG: "app:open-trace-log",
|
OPEN_TRACE_LOG: "app:open-trace-log",
|
||||||
OPEN_PACKAGE_LOG: "app:open-package-log",
|
OPEN_PACKAGE_LOG: "app:open-package-log",
|
||||||
OPEN_ITEM_LOG: "app:open-item-log",
|
OPEN_ITEM_LOG: "app:open-item-log",
|
||||||
|
GET_DEBUG_SETUP_CHECK: "app:get-debug-setup-check",
|
||||||
GET_TRACE_CONFIG: "app:get-trace-config",
|
GET_TRACE_CONFIG: "app:get-trace-config",
|
||||||
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
||||||
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type {
|
|||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
|
DebugSetupCheckResult,
|
||||||
DebridLinkHostLimitInfo,
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
@ -59,8 +60,9 @@ export interface ElectronApi {
|
|||||||
openTraceLog: () => Promise<void>;
|
openTraceLog: () => Promise<void>;
|
||||||
openPackageLog: (packageId: string) => Promise<void>;
|
openPackageLog: (packageId: string) => Promise<void>;
|
||||||
openItemLog: (itemId: string) => Promise<void>;
|
openItemLog: (itemId: string) => Promise<void>;
|
||||||
|
getDebugSetupCheck: () => Promise<DebugSetupCheckResult>;
|
||||||
getTraceConfig: () => Promise<SupportTraceConfig>;
|
getTraceConfig: () => Promise<SupportTraceConfig>;
|
||||||
setTraceEnabled: (enabled: boolean, note?: string) => Promise<SupportTraceConfig>;
|
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => Promise<SupportTraceConfig>;
|
||||||
rotateDebugToken: () => Promise<{ path: string }>;
|
rotateDebugToken: () => Promise<{ path: string }>;
|
||||||
openRealDebridLogin: () => Promise<void>;
|
openRealDebridLogin: () => Promise<void>;
|
||||||
openAllDebridLogin: () => Promise<void>;
|
openAllDebridLogin: () => Promise<void>;
|
||||||
|
|||||||
@ -342,9 +342,37 @@ export interface SupportTraceConfig {
|
|||||||
includeMainLog: boolean;
|
includeMainLog: boolean;
|
||||||
includeAudit: boolean;
|
includeAudit: boolean;
|
||||||
logDebugRequests: boolean;
|
logDebugRequests: boolean;
|
||||||
|
autoDisableAt: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DebugSetupCheckResult {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
localOnly: boolean;
|
||||||
|
tokenConfigured: boolean;
|
||||||
|
tokenPath: string;
|
||||||
|
aiManifestPath: string;
|
||||||
|
aiManifestPresent: boolean;
|
||||||
|
traceConfigPath: string | null;
|
||||||
|
traceLogPath: string | null;
|
||||||
|
traceEnabled: boolean;
|
||||||
|
traceAutoDisableAt: string | null;
|
||||||
|
warnings: string[];
|
||||||
|
notes: string[];
|
||||||
|
localUrls: {
|
||||||
|
health: string;
|
||||||
|
meta: string;
|
||||||
|
diagnostics: string;
|
||||||
|
};
|
||||||
|
remoteUrlTemplates: {
|
||||||
|
health: string;
|
||||||
|
meta: string;
|
||||||
|
diagnostics: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -29,4 +29,20 @@ describe("audit-log", () => {
|
|||||||
expect(content).toContain("Settings changed");
|
expect(content).toContain("Settings changed");
|
||||||
expect(content).toContain("changedKeys");
|
expect(content).toContain("changedKeys");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rotates oversized audit logs on startup", () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-alog-rotate-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
const oversizedPath = path.join(baseDir, "audit.log");
|
||||||
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
|
fs.writeFileSync(oversizedPath, "x".repeat(10 * 1024 * 1024 + 256), "utf8");
|
||||||
|
|
||||||
|
initAuditLog(baseDir);
|
||||||
|
|
||||||
|
expect(fs.existsSync(oversizedPath)).toBe(true);
|
||||||
|
expect(fs.existsSync(`${oversizedPath}.old`)).toBe(true);
|
||||||
|
const content = fs.readFileSync(oversizedPath, "utf8");
|
||||||
|
expect(content).toContain("Audit-Log Start");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -341,6 +341,7 @@ describe("debug-server", () => {
|
|||||||
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
||||||
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
||||||
expect(manifest.quickstart?.[1]).toContain("server IP");
|
expect(manifest.quickstart?.[1]).toContain("server IP");
|
||||||
|
expect(manifest.setupCheckEndpoint).toBe("/debug/setup");
|
||||||
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
||||||
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
||||||
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
||||||
@ -351,6 +352,24 @@ describe("debug-server", () => {
|
|||||||
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
||||||
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
||||||
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
||||||
|
expect(metaPayload.supportChecks?.setup).toBe("/debug/setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serves a debug setup check with trace expiry details", async () => {
|
||||||
|
const fixture = await createFixture();
|
||||||
|
const response = await fetch(`${fixture.baseUrl}/debug/setup?token=${fixture.token}`);
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
const payload = await response.json() as Record<string, any>;
|
||||||
|
|
||||||
|
expect(payload.enabled).toBe(true);
|
||||||
|
expect(payload.host).toBe("0.0.0.0");
|
||||||
|
expect(payload.localOnly).toBe(false);
|
||||||
|
expect(payload.tokenConfigured).toBe(true);
|
||||||
|
expect(payload.aiManifestPresent).toBe(true);
|
||||||
|
expect(payload.traceEnabled).toBe(true);
|
||||||
|
expect(payload.traceAutoDisableAt).toBeTruthy();
|
||||||
|
expect(payload.remoteUrlTemplates?.health).toContain("<SERVER_IP_OR_DNS>");
|
||||||
|
expect(Array.isArray(payload.notes)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves package details and package log by package query", async () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
@ -449,6 +468,7 @@ describe("debug-server", () => {
|
|||||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
const entries = zip.getEntries().map((entry) => entry.entryName);
|
||||||
expect(entries).toContain("overview/settings.json");
|
expect(entries).toContain("overview/settings.json");
|
||||||
expect(entries).toContain("overview/accounts.json");
|
expect(entries).toContain("overview/accounts.json");
|
||||||
|
expect(entries).toContain("overview/debug-setup.json");
|
||||||
expect(entries).toContain("overview/trace-config.json");
|
expect(entries).toContain("overview/trace-config.json");
|
||||||
expect(entries).toContain("logs/audit.log");
|
expect(entries).toContain("logs/audit.log");
|
||||||
expect(entries).toContain("logs/trace.log");
|
expect(entries).toContain("logs/trace.log");
|
||||||
|
|||||||
@ -48,6 +48,43 @@ describe("trace-log", () => {
|
|||||||
|
|
||||||
const traceConfig = getTraceConfig();
|
const traceConfig = getTraceConfig();
|
||||||
expect(traceConfig.enabled).toBe(true);
|
expect(traceConfig.enabled).toBe(true);
|
||||||
|
expect(traceConfig.autoDisableAt).toBeTruthy();
|
||||||
expect(JSON.parse(fs.readFileSync(traceConfigPath!, "utf8")).enabled).toBe(true);
|
expect(JSON.parse(fs.readFileSync(traceConfigPath!, "utf8")).enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("auto-disables support trace after the requested duration", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-tlog-expire-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
configureLogger(baseDir);
|
||||||
|
initTraceLog(baseDir);
|
||||||
|
setTraceEnabled(true, "expire-test", 50);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||||
|
|
||||||
|
const traceConfig = getTraceConfig();
|
||||||
|
expect(traceConfig.enabled).toBe(false);
|
||||||
|
expect(traceConfig.autoDisableAt).toBeNull();
|
||||||
|
|
||||||
|
const traceLogPath = getTraceLogPath();
|
||||||
|
expect(traceLogPath).not.toBeNull();
|
||||||
|
const traceContent = fs.readFileSync(traceLogPath!, "utf8");
|
||||||
|
expect(traceContent).toContain("Support-Trace automatisch deaktiviert");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rotates oversized trace logs on startup", () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-tlog-rotate-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
const oversizedPath = path.join(baseDir, "trace.log");
|
||||||
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
|
fs.writeFileSync(oversizedPath, "x".repeat(10 * 1024 * 1024 + 256), "utf8");
|
||||||
|
|
||||||
|
initTraceLog(baseDir);
|
||||||
|
|
||||||
|
expect(fs.existsSync(oversizedPath)).toBe(true);
|
||||||
|
expect(fs.existsSync(`${oversizedPath}.old`)).toBe(true);
|
||||||
|
const currentContent = fs.readFileSync(oversizedPath, "utf8");
|
||||||
|
expect(currentContent).toContain("Trace-Log Start");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user