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: {
|
globalSettings: {
|
||||||
alwaysOnTop: false,
|
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: []
|
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 path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const ConfigStore = require('./lib/config-store');
|
const ConfigStore = require('./lib/config-store');
|
||||||
@ -12,6 +12,26 @@ const configStore = new ConfigStore(app);
|
|||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
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) {
|
function withTimeout(promise, timeoutMs, label) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -210,6 +230,7 @@ function createWindow() {
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
// Auto-check for updates after 3 seconds
|
// Auto-check for updates after 3 seconds
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
@ -227,6 +248,12 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
// --- IPC Handlers ---
|
// --- IPC Handlers ---
|
||||||
|
|
||||||
|
// Debug log from renderer
|
||||||
|
ipcMain.handle('debug-log', (_event, msg) => {
|
||||||
|
debugLog(`[RENDERER] ${msg}`);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-config', () => {
|
ipcMain.handle('get-config', () => {
|
||||||
return configStore.load();
|
return configStore.load();
|
||||||
});
|
});
|
||||||
@ -257,35 +284,82 @@ ipcMain.handle('select-files', async () => {
|
|||||||
return result.canceled ? null : result.filePaths;
|
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) => {
|
ipcMain.handle('start-upload', (_event, payload) => {
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
const { files, hosters } = payload;
|
const { files, hosters } = payload;
|
||||||
|
|
||||||
|
debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}`);
|
||||||
|
|
||||||
// Build tasks with credentials
|
// Build tasks with credentials
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const hoster of hosters) {
|
for (const hoster of hosters) {
|
||||||
const hosterConfig = config.hosters[hoster];
|
const hosterConfig = config.hosters[hoster];
|
||||||
if (!hosterConfig) continue;
|
if (!hosterConfig) {
|
||||||
|
debugLog(` skip ${hoster}: no config`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (hoster === 'vidmoly.me') {
|
if (hoster === 'vidmoly.me') {
|
||||||
// Vidmoly uses username/password login
|
// 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 });
|
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
||||||
} else {
|
} else {
|
||||||
// Other hosters use API key
|
// 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 });
|
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.' };
|
if (tasks.length === 0) return { error: 'Keine gueltigen Zugangsdaten fuer die gewaehlten Hoster.' };
|
||||||
|
|
||||||
// Pass hoster settings to the upload manager
|
// Pass hoster settings to the upload manager
|
||||||
uploadManager = new UploadManager(config.hosterSettings || {});
|
uploadManager = new UploadManager(config.hosterSettings || {});
|
||||||
|
|
||||||
uploadManager.on('progress', (data) => {
|
uploadManager.on('progress', (data) => {
|
||||||
|
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('upload-progress', data);
|
mainWindow.webContents.send('upload-progress', data);
|
||||||
}
|
}
|
||||||
@ -298,6 +372,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('batch-done', (summary) => {
|
uploadManager.on('batch-done', (summary) => {
|
||||||
|
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||||
configStore.appendHistory(summary);
|
configStore.appendHistory(summary);
|
||||||
// Write successful uploads to fileuploader.log
|
// Write successful uploads to fileuploader.log
|
||||||
for (const file of summary.files || []) {
|
for (const file of summary.files || []) {
|
||||||
@ -319,7 +394,29 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
handleShutdownAfterFinish();
|
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 };
|
return { started: true, taskCount: tasks.length };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
|
|
||||||
// File selection
|
// File selection
|
||||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||||
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
|
|
||||||
// Upload control
|
// Upload control
|
||||||
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
||||||
@ -47,6 +48,10 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.on('app:update-progress', (_event, data) => callback(data));
|
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)
|
// Events (main -> renderer)
|
||||||
onUploadProgress: (callback) => {
|
onUploadProgress: (callback) => {
|
||||||
ipcRenderer.on('upload-progress', (_event, data) => callback(data));
|
ipcRenderer.on('upload-progress', (_event, data) => callback(data));
|
||||||
|
|||||||
@ -40,12 +40,23 @@ async function init() {
|
|||||||
window.api.onUpdateAvailable(showUpdateBanner);
|
window.api.onUpdateAvailable(showUpdateBanner);
|
||||||
window.api.onUpdateProgress(handleUpdateProgress);
|
window.api.onUpdateProgress(handleUpdateProgress);
|
||||||
|
|
||||||
// Upload event listeners
|
// Upload event listeners — with debug logging to file
|
||||||
window.api.onUploadProgress(handleProgress);
|
window.api.onUploadProgress((data) => {
|
||||||
window.api.onUploadBatchDone(handleBatchDone);
|
window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || ''));
|
||||||
window.api.onUploadStats(handleStats);
|
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.onShutdownCountdown(handleShutdownCountdown);
|
||||||
|
|
||||||
|
window.api.debugLog('init complete, all listeners registered');
|
||||||
|
|
||||||
// Restore always-on-top state
|
// Restore always-on-top state
|
||||||
try {
|
try {
|
||||||
const onTop = await window.api.getAlwaysOnTop();
|
const onTop = await window.api.getAlwaysOnTop();
|
||||||
@ -453,10 +464,13 @@ async function startUpload() {
|
|||||||
document.getElementById('startUploadBtn').style.display = 'none';
|
document.getElementById('startUploadBtn').style.display = 'none';
|
||||||
document.getElementById('cancelUploadBtn').style.display = 'inline-block';
|
document.getElementById('cancelUploadBtn').style.display = 'inline-block';
|
||||||
|
|
||||||
const result = await window.api.startUpload({
|
const uploadPayload = {
|
||||||
files: selectedFiles.map(f => f.path),
|
files: selectedFiles.map(f => f.path),
|
||||||
hosters
|
hosters
|
||||||
});
|
};
|
||||||
|
console.log('[startUpload] sending payload:', uploadPayload);
|
||||||
|
const result = await window.api.startUpload(uploadPayload);
|
||||||
|
console.log('[startUpload] response:', result);
|
||||||
|
|
||||||
if (result && result.error) {
|
if (result && result.error) {
|
||||||
alert(result.error);
|
alert(result.error);
|
||||||
@ -476,6 +490,7 @@ async function cancelUpload() {
|
|||||||
|
|
||||||
// --- Progress handling ---
|
// --- Progress handling ---
|
||||||
function handleProgress(data) {
|
function handleProgress(data) {
|
||||||
|
console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || '');
|
||||||
// Find matching job by fileName + hoster, or by uploadId
|
// Find matching job by fileName + hoster, or by uploadId
|
||||||
let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null;
|
let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null;
|
||||||
if (!job) {
|
if (!job) {
|
||||||
@ -517,6 +532,7 @@ function handleProgress(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBatchDone(summary) {
|
function handleBatchDone(summary) {
|
||||||
|
console.log('[batch-done]', summary);
|
||||||
uploading = false;
|
uploading = false;
|
||||||
selectedFiles = []; // Clear selected files after batch
|
selectedFiles = []; // Clear selected files after batch
|
||||||
document.getElementById('startUploadBtn').style.display = 'inline-block';
|
document.getElementById('startUploadBtn').style.display = 'inline-block';
|
||||||
@ -529,6 +545,7 @@ function handleBatchDone(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleStats(data) {
|
function handleStats(data) {
|
||||||
|
console.log('[upload-stats]', data.state, 'active=' + data.activeJobs);
|
||||||
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
|
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
|
||||||
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
|
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
|
||||||
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
|
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
|
||||||
@ -726,6 +743,7 @@ async function saveSettings() {
|
|||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
hosterSettings = config.hosterSettings || {};
|
hosterSettings = config.hosterSettings || {};
|
||||||
renderHosterChips();
|
renderHosterChips();
|
||||||
|
renderSettings();
|
||||||
renderHealthCheckResults([]);
|
renderHealthCheckResults([]);
|
||||||
|
|
||||||
const feedback = document.getElementById('saveFeedback');
|
const feedback = document.getElementById('saveFeedback');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user