Fix critical upload stuck-at-queued bug and settings display
Root cause: startBatch() ran synchronously inside ipcMain.handle() callback, causing webContents.send() events to conflict with the handle response and never reach the renderer. Fix: defer startBatch() via process.nextTick so IPC response is sent first, then upload events flow correctly. Also: - Add .catch() on startBatch to surface hidden errors - Fix settings panel not updating after save (renderSettings) - Add select-folder IPC handler (was in preload but missing) - Add debug-log and debug-test-upload IPC for diagnostics - Add upload-debug.log file for tracing upload flow - Add unhandledRejection handler for main process - Add scramble defaults to config-store globalSettings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
52b2e0a1e4
commit
49655dc154
@ -25,7 +25,14 @@ const DEFAULTS = {
|
||||
},
|
||||
globalSettings: {
|
||||
alwaysOnTop: false,
|
||||
shutdownAfterFinish: 'nothing' // nothing | sleep | shutdown | restart
|
||||
shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
|
||||
scramble: {
|
||||
active: false,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
chars: 'both', // 'letters' | 'numbers' | 'both'
|
||||
length: 0 // 0 = same as original basename length
|
||||
}
|
||||
},
|
||||
history: []
|
||||
};
|
||||
|
||||
107
main.js
107
main.js
@ -1,4 +1,4 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, clipboard } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ConfigStore = require('./lib/config-store');
|
||||
@ -12,6 +12,26 @@ const configStore = new ConfigStore(app);
|
||||
let uploadManager = null;
|
||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
||||
|
||||
// --- Debug logging (writes to upload-debug.log next to the app) ---
|
||||
function getDebugLogPath() {
|
||||
const baseDir = app.isPackaged
|
||||
? path.dirname(process.execPath)
|
||||
: __dirname;
|
||||
return path.join(baseDir, 'upload-debug.log');
|
||||
}
|
||||
|
||||
function debugLog(msg) {
|
||||
try {
|
||||
const ts = new Date().toISOString();
|
||||
fs.appendFileSync(getDebugLogPath(), `[${ts}] ${msg}\n`, 'utf-8');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Catch unhandled rejections from fire-and-forget async calls
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
||||
});
|
||||
|
||||
function withTimeout(promise, timeoutMs, label) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
@ -210,6 +230,7 @@ function createWindow() {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
// Auto-check for updates after 3 seconds
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@ -227,6 +248,12 @@ app.on('window-all-closed', () => {
|
||||
|
||||
// --- IPC Handlers ---
|
||||
|
||||
// Debug log from renderer
|
||||
ipcMain.handle('debug-log', (_event, msg) => {
|
||||
debugLog(`[RENDERER] ${msg}`);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-config', () => {
|
||||
return configStore.load();
|
||||
});
|
||||
@ -257,35 +284,82 @@ ipcMain.handle('select-files', async () => {
|
||||
return result.canceled ? null : result.filePaths;
|
||||
});
|
||||
|
||||
// Debug self-test: runs a minimal upload in the main process to verify events work
|
||||
ipcMain.handle('debug-test-upload', async () => {
|
||||
const testFile = path.join(__dirname, 'test-self-check.txt');
|
||||
try {
|
||||
fs.writeFileSync(testFile, 'selftest ' + Date.now(), 'utf-8');
|
||||
const mgr = new UploadManager({ 'voe.sx': { retries: 0, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } });
|
||||
const events = [];
|
||||
return new Promise((resolve) => {
|
||||
mgr.on('progress', (data) => { events.push({ s: data.status, e: data.error || null }); });
|
||||
mgr.on('batch-done', (summary) => {
|
||||
try { fs.unlinkSync(testFile); } catch {}
|
||||
resolve({ ok: true, events, summary: { ok: summary.succeeded, fail: summary.failed } });
|
||||
});
|
||||
mgr.startBatch([{ file: testFile, hoster: 'voe.sx', apiKey: 'invalid-test-key' }]);
|
||||
setTimeout(() => {
|
||||
try { fs.unlinkSync(testFile); } catch {}
|
||||
resolve({ ok: false, events, timeout: true });
|
||||
}, 20000);
|
||||
});
|
||||
} catch (err) {
|
||||
try { fs.unlinkSync(testFile); } catch {}
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('select-folder', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
return result.canceled ? null : result.filePaths;
|
||||
});
|
||||
|
||||
ipcMain.handle('start-upload', (_event, payload) => {
|
||||
const config = configStore.load();
|
||||
const { files, hosters } = payload;
|
||||
|
||||
debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}`);
|
||||
|
||||
// Build tasks with credentials
|
||||
const tasks = [];
|
||||
for (const file of files) {
|
||||
for (const hoster of hosters) {
|
||||
const hosterConfig = config.hosters[hoster];
|
||||
if (!hosterConfig) continue;
|
||||
if (!hosterConfig) {
|
||||
debugLog(` skip ${hoster}: no config`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hoster === 'vidmoly.me') {
|
||||
// Vidmoly uses username/password login
|
||||
if (!hosterConfig.username || !hosterConfig.password) continue;
|
||||
if (!hosterConfig.username || !hosterConfig.password) {
|
||||
debugLog(` skip ${hoster}: missing username/password`);
|
||||
continue;
|
||||
}
|
||||
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
||||
} else {
|
||||
// Other hosters use API key
|
||||
if (!hosterConfig.apiKey) continue;
|
||||
if (!hosterConfig.apiKey) {
|
||||
debugLog(` skip ${hoster}: missing apiKey`);
|
||||
continue;
|
||||
}
|
||||
tasks.push({ file, hoster, apiKey: hosterConfig.apiKey });
|
||||
debugLog(` task: ${hoster} key=${hosterConfig.apiKey.slice(0, 6)}...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(` tasks built: ${tasks.length}`);
|
||||
|
||||
if (tasks.length === 0) return { error: 'Keine gueltigen Zugangsdaten fuer die gewaehlten Hoster.' };
|
||||
|
||||
// Pass hoster settings to the upload manager
|
||||
uploadManager = new UploadManager(config.hosterSettings || {});
|
||||
|
||||
uploadManager.on('progress', (data) => {
|
||||
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('upload-progress', data);
|
||||
}
|
||||
@ -298,6 +372,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
||||
});
|
||||
|
||||
uploadManager.on('batch-done', (summary) => {
|
||||
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||
configStore.appendHistory(summary);
|
||||
// Write successful uploads to fileuploader.log
|
||||
for (const file of summary.files || []) {
|
||||
@ -319,7 +394,29 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
||||
handleShutdownAfterFinish();
|
||||
});
|
||||
|
||||
uploadManager.startBatch(tasks);
|
||||
// Defer startBatch to next tick so the IPC response is sent first.
|
||||
// This ensures webContents.send() calls from upload events
|
||||
// are not interleaved with the handle() response.
|
||||
process.nextTick(() => {
|
||||
debugLog('nextTick: calling startBatch now');
|
||||
uploadManager.startBatch(tasks).catch((err) => {
|
||||
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
||||
// Forward error to renderer as batch-done with failure
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('upload-batch-done', {
|
||||
id: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
total: tasks.length,
|
||||
succeeded: 0,
|
||||
failed: tasks.length,
|
||||
files: [],
|
||||
error: err ? err.message : 'Unbekannter Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
debugLog(`start-upload returning started=true (startBatch deferred to nextTick)`);
|
||||
return { started: true, taskCount: tasks.length };
|
||||
});
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
|
||||
// File selection
|
||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||
|
||||
// Upload control
|
||||
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
||||
@ -47,6 +48,10 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.on('app:update-progress', (_event, data) => callback(data));
|
||||
},
|
||||
|
||||
// Debug
|
||||
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
||||
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
||||
|
||||
// Events (main -> renderer)
|
||||
onUploadProgress: (callback) => {
|
||||
ipcRenderer.on('upload-progress', (_event, data) => callback(data));
|
||||
|
||||
@ -40,12 +40,23 @@ async function init() {
|
||||
window.api.onUpdateAvailable(showUpdateBanner);
|
||||
window.api.onUpdateProgress(handleUpdateProgress);
|
||||
|
||||
// Upload event listeners
|
||||
window.api.onUploadProgress(handleProgress);
|
||||
window.api.onUploadBatchDone(handleBatchDone);
|
||||
window.api.onUploadStats(handleStats);
|
||||
// Upload event listeners — with debug logging to file
|
||||
window.api.onUploadProgress((data) => {
|
||||
window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || ''));
|
||||
handleProgress(data);
|
||||
});
|
||||
window.api.onUploadBatchDone((data) => {
|
||||
window.api.debugLog('RX upload-batch-done');
|
||||
handleBatchDone(data);
|
||||
});
|
||||
window.api.onUploadStats((data) => {
|
||||
window.api.debugLog('RX upload-stats: state=' + data.state + ' active=' + data.activeJobs);
|
||||
handleStats(data);
|
||||
});
|
||||
window.api.onShutdownCountdown(handleShutdownCountdown);
|
||||
|
||||
window.api.debugLog('init complete, all listeners registered');
|
||||
|
||||
// Restore always-on-top state
|
||||
try {
|
||||
const onTop = await window.api.getAlwaysOnTop();
|
||||
@ -453,10 +464,13 @@ async function startUpload() {
|
||||
document.getElementById('startUploadBtn').style.display = 'none';
|
||||
document.getElementById('cancelUploadBtn').style.display = 'inline-block';
|
||||
|
||||
const result = await window.api.startUpload({
|
||||
const uploadPayload = {
|
||||
files: selectedFiles.map(f => f.path),
|
||||
hosters
|
||||
});
|
||||
};
|
||||
console.log('[startUpload] sending payload:', uploadPayload);
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
console.log('[startUpload] response:', result);
|
||||
|
||||
if (result && result.error) {
|
||||
alert(result.error);
|
||||
@ -476,6 +490,7 @@ async function cancelUpload() {
|
||||
|
||||
// --- Progress handling ---
|
||||
function handleProgress(data) {
|
||||
console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || '');
|
||||
// Find matching job by fileName + hoster, or by uploadId
|
||||
let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null;
|
||||
if (!job) {
|
||||
@ -517,6 +532,7 @@ function handleProgress(data) {
|
||||
}
|
||||
|
||||
function handleBatchDone(summary) {
|
||||
console.log('[batch-done]', summary);
|
||||
uploading = false;
|
||||
selectedFiles = []; // Clear selected files after batch
|
||||
document.getElementById('startUploadBtn').style.display = 'inline-block';
|
||||
@ -529,6 +545,7 @@ function handleBatchDone(summary) {
|
||||
}
|
||||
|
||||
function handleStats(data) {
|
||||
console.log('[upload-stats]', data.state, 'active=' + data.activeJobs);
|
||||
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
|
||||
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
|
||||
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
|
||||
@ -726,6 +743,7 @@ async function saveSettings() {
|
||||
config = await window.api.getConfig();
|
||||
hosterSettings = config.hosterSettings || {};
|
||||
renderHosterChips();
|
||||
renderSettings();
|
||||
renderHealthCheckResults([]);
|
||||
|
||||
const feedback = document.getElementById('saveFeedback');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user