Release v1.5.79
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c4aefb6175
commit
253b1868ec
@@ -19,6 +19,7 @@ import { APP_VERSION } from "./constants";
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { parseCollectorInput } from "./link-parser";
|
||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||
import { MegaWebFallback } from "./mega-web-fallback";
|
||||
import { addHistoryEntry, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage";
|
||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||
@@ -52,6 +53,7 @@ export class AppController {
|
||||
|
||||
public constructor() {
|
||||
configureLogger(this.storagePaths.baseDir);
|
||||
initSessionLog(this.storagePaths.baseDir);
|
||||
this.settings = loadSettings(this.storagePaths);
|
||||
const session = loadSession(this.storagePaths);
|
||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||
@@ -224,6 +226,10 @@ export class AppController {
|
||||
this.manager.extractNow(packageId);
|
||||
}
|
||||
|
||||
public resetPackage(packageId: string): void {
|
||||
this.manager.resetPackage(packageId);
|
||||
}
|
||||
|
||||
public cancelPackage(packageId: string): void {
|
||||
this.manager.cancelPackage(packageId);
|
||||
}
|
||||
@@ -281,11 +287,16 @@ export class AppController {
|
||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||
}
|
||||
|
||||
public getSessionLogPath(): string | null {
|
||||
return getSessionLogPath();
|
||||
}
|
||||
|
||||
public shutdown(): void {
|
||||
stopDebugServer();
|
||||
abortActiveUpdateDownload();
|
||||
this.manager.prepareForShutdown();
|
||||
this.megaWebFallback.dispose();
|
||||
shutdownSessionLog();
|
||||
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 rotateCheckAtByFile = new Map<string, number>();
|
||||
|
||||
type LogListener = (line: string) => void;
|
||||
let logListener: LogListener | null = null;
|
||||
|
||||
let pendingLines: string[] = [];
|
||||
let pendingChars = 0;
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
let flushInFlight = false;
|
||||
let exitHookAttached = false;
|
||||
|
||||
export function setLogListener(listener: LogListener | null): void {
|
||||
logListener = listener;
|
||||
}
|
||||
|
||||
export function configureLogger(baseDir: string): void {
|
||||
logFilePath = path.join(baseDir, "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);
|
||||
pendingChars += line.length;
|
||||
|
||||
if (logListener) {
|
||||
try { logListener(line); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
||||
const removed = pendingLines.shift();
|
||||
if (!removed) {
|
||||
|
||||
@@ -326,6 +326,10 @@ function registerIpcHandlers(): void {
|
||||
validateString(packageId, "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.CLEAR_HISTORY, () => controller.clearHistory());
|
||||
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
||||
@@ -396,6 +400,13 @@ function registerIpcHandlers(): void {
|
||||
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 () => {
|
||||
const options = {
|
||||
properties: ["openFile"] as Array<"openFile">,
|
||||
|
||||
@@ -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),
|
||||
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
|
||||
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),
|
||||
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),
|
||||
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
|
||||
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(); }}>
|
||||
<span>Log öffnen</span>
|
||||
</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(); }}>
|
||||
<span>Suche Aktualisierungen</span>
|
||||
</button>
|
||||
|
||||
@@ -33,8 +33,10 @@ export const IPC_CHANNELS = {
|
||||
EXPORT_BACKUP: "app:export-backup",
|
||||
IMPORT_BACKUP: "app:import-backup",
|
||||
OPEN_LOG: "app:open-log",
|
||||
OPEN_SESSION_LOG: "app:open-session-log",
|
||||
RETRY_EXTRACTION: "queue:retry-extraction",
|
||||
EXTRACT_NOW: "queue:extract-now",
|
||||
RESET_PACKAGE: "queue:reset-package",
|
||||
GET_HISTORY: "history:get",
|
||||
CLEAR_HISTORY: "history:clear",
|
||||
REMOVE_HISTORY_ENTRY: "history:remove-entry"
|
||||
|
||||
@@ -44,8 +44,10 @@ export interface ElectronApi {
|
||||
exportBackup: () => Promise<{ saved: boolean }>;
|
||||
importBackup: () => Promise<{ restored: boolean; message: string }>;
|
||||
openLog: () => Promise<void>;
|
||||
openSessionLog: () => Promise<void>;
|
||||
retryExtraction: (packageId: string) => Promise<void>;
|
||||
extractNow: (packageId: string) => Promise<void>;
|
||||
resetPackage: (packageId: string) => Promise<void>;
|
||||
getHistory: () => Promise<HistoryEntry[]>;
|
||||
clearHistory: () => Promise<void>;
|
||||
removeHistoryEntry: (entryId: string) => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user