Release v1.5.79
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c4aefb6175
commit
253b1868ec
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.5.78",
|
"version": "1.5.79",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { APP_VERSION } from "./constants";
|
|||||||
import { DownloadManager } from "./download-manager";
|
import { DownloadManager } from "./download-manager";
|
||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
|
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
import { MegaWebFallback } from "./mega-web-fallback";
|
||||||
import { addHistoryEntry, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage";
|
import { addHistoryEntry, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage";
|
||||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
@ -52,6 +53,7 @@ export class AppController {
|
|||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
configureLogger(this.storagePaths.baseDir);
|
configureLogger(this.storagePaths.baseDir);
|
||||||
|
initSessionLog(this.storagePaths.baseDir);
|
||||||
this.settings = loadSettings(this.storagePaths);
|
this.settings = loadSettings(this.storagePaths);
|
||||||
const session = loadSession(this.storagePaths);
|
const session = loadSession(this.storagePaths);
|
||||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||||
@ -224,6 +226,10 @@ export class AppController {
|
|||||||
this.manager.extractNow(packageId);
|
this.manager.extractNow(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetPackage(packageId: string): void {
|
||||||
|
this.manager.resetPackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
public cancelPackage(packageId: string): void {
|
public cancelPackage(packageId: string): void {
|
||||||
this.manager.cancelPackage(packageId);
|
this.manager.cancelPackage(packageId);
|
||||||
}
|
}
|
||||||
@ -281,11 +287,16 @@ export class AppController {
|
|||||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSessionLogPath(): string | null {
|
||||||
|
return getSessionLogPath();
|
||||||
|
}
|
||||||
|
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
stopDebugServer();
|
stopDebugServer();
|
||||||
abortActiveUpdateDownload();
|
abortActiveUpdateDownload();
|
||||||
this.manager.prepareForShutdown();
|
this.manager.prepareForShutdown();
|
||||||
this.megaWebFallback.dispose();
|
this.megaWebFallback.dispose();
|
||||||
|
shutdownSessionLog();
|
||||||
logger.info("App beendet");
|
logger.info("App beendet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,19 @@ const LOG_BUFFER_LIMIT_CHARS = 1_000_000;
|
|||||||
const LOG_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
const LOG_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
||||||
const rotateCheckAtByFile = new Map<string, number>();
|
const rotateCheckAtByFile = new Map<string, number>();
|
||||||
|
|
||||||
|
type LogListener = (line: string) => void;
|
||||||
|
let logListener: LogListener | null = null;
|
||||||
|
|
||||||
let pendingLines: string[] = [];
|
let pendingLines: string[] = [];
|
||||||
let pendingChars = 0;
|
let pendingChars = 0;
|
||||||
let flushTimer: NodeJS.Timeout | null = null;
|
let flushTimer: NodeJS.Timeout | null = null;
|
||||||
let flushInFlight = false;
|
let flushInFlight = false;
|
||||||
let exitHookAttached = false;
|
let exitHookAttached = false;
|
||||||
|
|
||||||
|
export function setLogListener(listener: LogListener | null): void {
|
||||||
|
logListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
export function configureLogger(baseDir: string): void {
|
export function configureLogger(baseDir: string): void {
|
||||||
logFilePath = path.join(baseDir, "rd_downloader.log");
|
logFilePath = path.join(baseDir, "rd_downloader.log");
|
||||||
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
|
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
|
||||||
@ -188,6 +195,10 @@ function write(level: "INFO" | "WARN" | "ERROR", message: string): void {
|
|||||||
pendingLines.push(line);
|
pendingLines.push(line);
|
||||||
pendingChars += line.length;
|
pendingChars += line.length;
|
||||||
|
|
||||||
|
if (logListener) {
|
||||||
|
try { logListener(line); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
||||||
const removed = pendingLines.shift();
|
const removed = pendingLines.shift();
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
|
|||||||
@ -326,6 +326,10 @@ function registerIpcHandlers(): void {
|
|||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
return controller.extractNow(packageId);
|
return controller.extractNow(packageId);
|
||||||
});
|
});
|
||||||
|
ipcMain.handle(IPC_CHANNELS.RESET_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
|
validateString(packageId, "packageId");
|
||||||
|
return controller.resetPackage(packageId);
|
||||||
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
|
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
|
||||||
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
|
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
|
||||||
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
||||||
@ -396,6 +400,13 @@ function registerIpcHandlers(): void {
|
|||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
|
||||||
|
const logPath = controller.getSessionLogPath();
|
||||||
|
if (logPath) {
|
||||||
|
await shell.openPath(logPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async () => {
|
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async () => {
|
||||||
const options = {
|
const options = {
|
||||||
properties: ["openFile"] as Array<"openFile">,
|
properties: ["openFile"] as Array<"openFile">,
|
||||||
|
|||||||
123
src/main/session-log.ts
Normal file
123
src/main/session-log.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { setLogListener } from "./logger";
|
||||||
|
|
||||||
|
const SESSION_LOG_FLUSH_INTERVAL_MS = 200;
|
||||||
|
|
||||||
|
let sessionLogPath: string | null = null;
|
||||||
|
let sessionLogsDir: string | null = null;
|
||||||
|
let pendingLines: string[] = [];
|
||||||
|
let flushTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
function formatTimestamp(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(now.getDate()).padStart(2, "0");
|
||||||
|
const h = String(now.getHours()).padStart(2, "0");
|
||||||
|
const mi = String(now.getMinutes()).padStart(2, "0");
|
||||||
|
const s = String(now.getSeconds()).padStart(2, "0");
|
||||||
|
return `${y}-${mo}-${d}_${h}-${mi}-${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushPending(): void {
|
||||||
|
if (pendingLines.length === 0 || !sessionLogPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunk = pendingLines.join("");
|
||||||
|
pendingLines = [];
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(sessionLogPath, chunk, "utf8");
|
||||||
|
} catch {
|
||||||
|
// ignore write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFlush(): void {
|
||||||
|
if (flushTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
flushTimer = null;
|
||||||
|
flushPending();
|
||||||
|
}, SESSION_LOG_FLUSH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendToSessionLog(line: string): void {
|
||||||
|
if (!sessionLogPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingLines.push(line);
|
||||||
|
scheduleFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupOldSessionLogs(dir: string, maxAgeDays: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const files = await fs.promises.readdir(dir);
|
||||||
|
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.startsWith("session_") || !file.endsWith(".txt")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
try {
|
||||||
|
const stat = await fs.promises.stat(filePath);
|
||||||
|
if (stat.mtimeMs < cutoff) {
|
||||||
|
await fs.promises.unlink(filePath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore - file may be locked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore - dir may not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSessionLog(baseDir: string): void {
|
||||||
|
sessionLogsDir = path.join(baseDir, "session-logs");
|
||||||
|
fs.mkdirSync(sessionLogsDir, { recursive: true });
|
||||||
|
|
||||||
|
const timestamp = formatTimestamp();
|
||||||
|
sessionLogPath = path.join(sessionLogsDir, `session_${timestamp}.txt`);
|
||||||
|
|
||||||
|
const isoTimestamp = new Date().toISOString();
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(sessionLogPath, `=== Session gestartet: ${isoTimestamp} ===\n`, "utf8");
|
||||||
|
} catch {
|
||||||
|
sessionLogPath = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogListener((line) => appendToSessionLog(line));
|
||||||
|
|
||||||
|
void cleanupOldSessionLogs(sessionLogsDir, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSessionLogPath(): string | null {
|
||||||
|
return sessionLogPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shutdownSessionLog(): void {
|
||||||
|
if (!sessionLogPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any pending lines
|
||||||
|
if (flushTimer) {
|
||||||
|
clearTimeout(flushTimer);
|
||||||
|
flushTimer = null;
|
||||||
|
}
|
||||||
|
flushPending();
|
||||||
|
|
||||||
|
// Write closing line
|
||||||
|
const isoTimestamp = new Date().toISOString();
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(sessionLogPath, `=== Session beendet: ${isoTimestamp} ===\n`, "utf8");
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogListener(null);
|
||||||
|
sessionLogPath = null;
|
||||||
|
}
|
||||||
@ -49,8 +49,10 @@ const api: ElectronApi = {
|
|||||||
exportBackup: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_BACKUP),
|
exportBackup: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_BACKUP),
|
||||||
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
|
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
|
||||||
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
|
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
|
||||||
|
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
|
||||||
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||||
|
resetPackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RESET_PACKAGE, packageId),
|
||||||
getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY),
|
getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY),
|
||||||
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
|
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
|
||||||
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
|
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
|
||||||
|
|||||||
@ -1983,6 +1983,9 @@ export function App(): ReactElement {
|
|||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog(); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog(); }}>
|
||||||
<span>Log öffnen</span>
|
<span>Log öffnen</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog(); }}>
|
||||||
|
<span>Session-Log öffnen</span>
|
||||||
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
||||||
<span>Suche Aktualisierungen</span>
|
<span>Suche Aktualisierungen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -33,8 +33,10 @@ export const IPC_CHANNELS = {
|
|||||||
EXPORT_BACKUP: "app:export-backup",
|
EXPORT_BACKUP: "app:export-backup",
|
||||||
IMPORT_BACKUP: "app:import-backup",
|
IMPORT_BACKUP: "app:import-backup",
|
||||||
OPEN_LOG: "app:open-log",
|
OPEN_LOG: "app:open-log",
|
||||||
|
OPEN_SESSION_LOG: "app:open-session-log",
|
||||||
RETRY_EXTRACTION: "queue:retry-extraction",
|
RETRY_EXTRACTION: "queue:retry-extraction",
|
||||||
EXTRACT_NOW: "queue:extract-now",
|
EXTRACT_NOW: "queue:extract-now",
|
||||||
|
RESET_PACKAGE: "queue:reset-package",
|
||||||
GET_HISTORY: "history:get",
|
GET_HISTORY: "history:get",
|
||||||
CLEAR_HISTORY: "history:clear",
|
CLEAR_HISTORY: "history:clear",
|
||||||
REMOVE_HISTORY_ENTRY: "history:remove-entry"
|
REMOVE_HISTORY_ENTRY: "history:remove-entry"
|
||||||
|
|||||||
@ -44,8 +44,10 @@ export interface ElectronApi {
|
|||||||
exportBackup: () => Promise<{ saved: boolean }>;
|
exportBackup: () => Promise<{ saved: boolean }>;
|
||||||
importBackup: () => Promise<{ restored: boolean; message: string }>;
|
importBackup: () => Promise<{ restored: boolean; message: string }>;
|
||||||
openLog: () => Promise<void>;
|
openLog: () => Promise<void>;
|
||||||
|
openSessionLog: () => Promise<void>;
|
||||||
retryExtraction: (packageId: string) => Promise<void>;
|
retryExtraction: (packageId: string) => Promise<void>;
|
||||||
extractNow: (packageId: string) => Promise<void>;
|
extractNow: (packageId: string) => Promise<void>;
|
||||||
|
resetPackage: (packageId: string) => Promise<void>;
|
||||||
getHistory: () => Promise<HistoryEntry[]>;
|
getHistory: () => Promise<HistoryEntry[]>;
|
||||||
clearHistory: () => Promise<void>;
|
clearHistory: () => Promise<void>;
|
||||||
removeHistoryEntry: (entryId: string) => Promise<void>;
|
removeHistoryEntry: (entryId: string) => Promise<void>;
|
||||||
|
|||||||
164
tests/session-log.test.ts
Normal file
164
tests/session-log.test.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "../src/main/session-log";
|
||||||
|
import { setLogListener } from "../src/main/logger";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Ensure listener is cleared between tests
|
||||||
|
setLogListener(null);
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("session-log", () => {
|
||||||
|
it("initSessionLog creates directory and file", () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const logPath = getSessionLogPath();
|
||||||
|
expect(logPath).not.toBeNull();
|
||||||
|
expect(fs.existsSync(logPath!)).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(baseDir, "session-logs"))).toBe(true);
|
||||||
|
expect(path.basename(logPath!)).toMatch(/^session_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.txt$/);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(logPath!, "utf8");
|
||||||
|
expect(content).toContain("=== Session gestartet:");
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logger listener writes to session log", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const logPath = getSessionLogPath()!;
|
||||||
|
|
||||||
|
// Simulate a log line via the listener
|
||||||
|
const { logger } = await import("../src/main/logger");
|
||||||
|
logger.info("Test-Nachricht für Session-Log");
|
||||||
|
|
||||||
|
// Wait for flush (200ms interval + margin)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||||
|
|
||||||
|
const content = fs.readFileSync(logPath, "utf8");
|
||||||
|
expect(content).toContain("Test-Nachricht für Session-Log");
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shutdownSessionLog writes closing line", () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const logPath = getSessionLogPath()!;
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
|
||||||
|
const content = fs.readFileSync(logPath, "utf8");
|
||||||
|
expect(content).toContain("=== Session beendet:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shutdownSessionLog removes listener", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const logPath = getSessionLogPath()!;
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
|
||||||
|
// Log after shutdown - should NOT appear in session log
|
||||||
|
const { logger } = await import("../src/main/logger");
|
||||||
|
logger.info("Nach-Shutdown-Nachricht");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||||
|
|
||||||
|
const content = fs.readFileSync(logPath, "utf8");
|
||||||
|
expect(content).not.toContain("Nach-Shutdown-Nachricht");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cleanupOldSessionLogs deletes old files", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
const logsDir = path.join(baseDir, "session-logs");
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create a fake old session log
|
||||||
|
const oldFile = path.join(logsDir, "session_2020-01-01_00-00-00.txt");
|
||||||
|
fs.writeFileSync(oldFile, "old session");
|
||||||
|
// Set mtime to 30 days ago
|
||||||
|
const oldTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
fs.utimesSync(oldFile, oldTime, oldTime);
|
||||||
|
|
||||||
|
// Create a recent file
|
||||||
|
const newFile = path.join(logsDir, "session_2099-01-01_00-00-00.txt");
|
||||||
|
fs.writeFileSync(newFile, "new session");
|
||||||
|
|
||||||
|
// initSessionLog triggers cleanup
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
|
||||||
|
// Wait for async cleanup
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
expect(fs.existsSync(oldFile)).toBe(false);
|
||||||
|
expect(fs.existsSync(newFile)).toBe(true);
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cleanupOldSessionLogs keeps recent files", async () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
const logsDir = path.join(baseDir, "session-logs");
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create a file from 2 days ago (should be kept)
|
||||||
|
const recentFile = path.join(logsDir, "session_2025-12-01_00-00-00.txt");
|
||||||
|
fs.writeFileSync(recentFile, "recent session");
|
||||||
|
const recentTime = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
||||||
|
fs.utimesSync(recentFile, recentTime, recentTime);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
expect(fs.existsSync(recentFile)).toBe(true);
|
||||||
|
|
||||||
|
shutdownSessionLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multiple sessions create different files", () => {
|
||||||
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const path1 = getSessionLogPath();
|
||||||
|
shutdownSessionLog();
|
||||||
|
|
||||||
|
// Small delay to ensure different timestamp
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < 1100) {
|
||||||
|
// busy-wait for 1.1 seconds to get different second in filename
|
||||||
|
}
|
||||||
|
|
||||||
|
initSessionLog(baseDir);
|
||||||
|
const path2 = getSessionLogPath();
|
||||||
|
shutdownSessionLog();
|
||||||
|
|
||||||
|
expect(path1).not.toBeNull();
|
||||||
|
expect(path2).not.toBeNull();
|
||||||
|
expect(path1).not.toBe(path2);
|
||||||
|
expect(fs.existsSync(path1!)).toBe(true);
|
||||||
|
expect(fs.existsSync(path2!)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user