From 0cca83ff07f9f8a160c375982b8b4e351b5151ea Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Fri, 9 Jan 2026 21:28:58 +0100 Subject: [PATCH] add more robust logging --- plugins/multi-scrobbler/src/index.tsx | 236 +++++++++++++++++++++---- plugins/multi-scrobbler/src/manager.ts | 38 +++- 2 files changed, 237 insertions(+), 37 deletions(-) diff --git a/plugins/multi-scrobbler/src/index.tsx b/plugins/multi-scrobbler/src/index.tsx index c1c4381..651563e 100644 --- a/plugins/multi-scrobbler/src/index.tsx +++ b/plugins/multi-scrobbler/src/index.tsx @@ -10,6 +10,108 @@ import { serviceFactory } from "./services/ServiceFactory"; import Settings from "./ui/pages/Settings"; import patchSidebar from "./sidebar"; +(function installUnhandledRejectionHandler() { + try { + const g = typeof globalThis !== "undefined" ? (globalThis as any) : {}; + + // Avoid installing multiple times + if (g && !g.__scrobble_unhandled_rejection_installed) { + const handler = (ev: any) => { + try { + // ev may be an event or a reason object depending on the runtime + const reason = ev && (ev.reason ?? ev) ? (ev.reason ?? ev) : ev; + console.error("[Scrobble Plugin] Unhandled rejection:", reason); + } catch (inner) { + // Defensive: if formatting the event fails, still log something + console.error("[Scrobble Plugin] Unhandled rejection (unknown format)", ev); + } + }; + + // Prefer globalThis.addEventListener + if (g && typeof g.addEventListener === "function") { + try { + g.addEventListener("unhandledrejection", handler); + g.__scrobble_unhandled_rejection_installed = true; + return; + } catch (e) { + // continue to other fallbacks + console.error("[Scrobble Plugin] Failed to register via globalThis.addEventListener:", e); + } + } + + // If window exists and supports addEventListener, use it + if (typeof (globalThis as any).window !== "undefined") { + const w = (globalThis as any).window; + if (w && typeof w.addEventListener === "function") { + try { + w.addEventListener("unhandledrejection", handler); + g.__scrobble_unhandled_rejection_installed = true; + return; + } catch (e) { + console.error("[Scrobble Plugin] Failed to register via window.addEventListener:", e); + } + } else if (w) { + // fallback to assigning onunhandledrejection if available and not a function call + try { + if (typeof w.onunhandledrejection === "undefined") { + w.onunhandledrejection = (ev: any) => handler(ev); + g.__scrobble_unhandled_rejection_installed = true; + return; + } + } catch (e) { + console.error("[Scrobble Plugin] Failed to assign window.onunhandledrejection:", e); + } + } + } + + // As a last resort, try process.on (Node-style). Guard existence and function type. + try { + const proc = (globalThis as any).process; + if (proc && typeof proc.on === "function") { + proc.on("unhandledRejection", (reason: any) => handler({ reason })); + g.__scrobble_unhandled_rejection_installed = true; + return; + } + } catch (e) { + // ignore + } + + // If none of the above worked, set a marker so we don't keep trying on repeated loads + g.__scrobble_unhandled_rejection_installed = true; + } + } catch (e) { + // Most defensive: don't let any error here block plugin loading + try { + console.error("[Scrobble Plugin] Error while installing unhandled rejection handler:", e); + } catch { + /* swallow */ + } + } +})(); + +// Heartbeat helpers +let __scrobbleHeartbeat: any; +function startHeartbeat() { + if (__scrobbleHeartbeat) return; + try { + __scrobbleHeartbeat = setInterval(() => { + console.log("[Scrobble Plugin] Heartbeat (alive)"); + }, 60_000); + } catch (e) { + console.error("[Scrobble Plugin] Failed to start heartbeat:", e); + } +} +function stopHeartbeat() { + if (!__scrobbleHeartbeat) return; + try { + clearInterval(__scrobbleHeartbeat); + } catch (e) { + console.error("[Scrobble Plugin] Failed to clear heartbeat:", e); + } finally { + __scrobbleHeartbeat = undefined; + } +} + export const pluginState = { pluginStopped: false, lastActivity: undefined, @@ -28,18 +130,29 @@ let sidebarUnpatch: (() => void) | undefined; // Set up default plugin settings const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS; for (const key of Object.keys(defaultSettings)) { - plugin.storage[key] = - plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings]; + // Defensive: plugin.storage may not accept arbitrary keys in some environments, + // but assignment should generally be fine in Vendetta. + try { + plugin.storage[key] = + plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings]; + } catch (e) { + console.error("[Scrobble Plugin] Failed to initialize default setting", key, e); + } } -plugin.storage.addToSidebar ??= false; +try { + // safe default for addToSidebar + plugin.storage.addToSidebar ??= false; +} catch (e) { + // ignore if storage access fails +} export const currentSettings = new Proxy(plugin.storage, { get(target, prop) { - return target[prop]; + return (target as any)[prop]; }, set(target, prop, value) { - target[prop] = value; + (target as any)[prop] = value; return true; }, }); @@ -50,8 +163,7 @@ const MAX_CONNECTION_ATTEMPTS = 3; const RECONNECT_DELAY = 5000; const log = (...args: any[]) => console.log("[Scrobble Plugin]", ...args); -const logError = (...args: any[]) => - console.error("[Scrobble Plugin] Error:", ...args); +const logError = (...args: any[]) => console.error("[Scrobble Plugin] Error:", ...args); async function tryInitialize() { try { @@ -63,9 +175,7 @@ async function tryInitialize() { connectionAttempts++; if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) { - log( - `Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`, - ); + log(`Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`); setTimeout(tryInitialize, RECONNECT_DELAY); } else { logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`); @@ -80,7 +190,12 @@ async function validateAndInitialize() { return; } - const serviceName = serviceFactory.getCurrentService().getServiceName(); + let serviceName = "Unknown"; + try { + serviceName = serviceFactory.getCurrentService().getServiceName(); + } catch (e) { + logError("Failed to determine current service name:", e); + } // Make sure we have credentials for whatever service is selected const service = currentSettings.service; @@ -91,9 +206,7 @@ async function validateAndInitialize() { hasCredentials = !!(currentSettings.username && currentSettings.apiKey); break; case "librefm": - hasCredentials = !!( - currentSettings.librefmUsername && currentSettings.librefmApiKey - ); + hasCredentials = !!(currentSettings.librefmUsername && currentSettings.librefmApiKey); break; case "listenbrainz": hasCredentials = !!currentSettings.listenbrainzUsername; @@ -101,9 +214,7 @@ async function validateAndInitialize() { } if (!hasCredentials) { - logError( - `Missing credentials for ${serviceName}. Please configure in plugin settings.`, - ); + logError(`Missing credentials for ${serviceName}. Please configure in plugin settings.`); return; } @@ -115,14 +226,22 @@ async function validateAndInitialize() { } else { log("Waiting for Discord connection..."); const waitForUser = () => { - if (UserStore.getCurrentUser()) { - log("Discord connection established"); - tryInitialize(); - FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser); + try { + if (UserStore.getCurrentUser()) { + log("Discord connection established"); + tryInitialize(); + FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser); + } + } catch (e) { + logError("Error while waiting for Discord connection:", e); } }; - FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser); + try { + FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser); + } catch (e) { + logError("Failed to subscribe to CONNECTION_OPEN:", e); + } } } @@ -131,15 +250,27 @@ export default { log("Plugin loading..."); pluginState.pluginStopped = false; + // Start heartbeat so we can confirm plugin process is alive + try { + startHeartbeat(); + log("Heartbeat started"); + } catch (e) { + logError("Failed to start heartbeat:", e); + } + // Check if service is configured if (!currentSettings.service) { log("No service configured. Please select a service in plugin settings."); } else { // Show what we're starting with - const serviceName = serviceFactory.getCurrentService().getServiceName(); - log( - `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`, - ); + try { + const serviceName = serviceFactory.getCurrentService().getServiceName(); + log( + `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`, + ); + } catch (e) { + logError("Failed to read service configuration:", e); + } } // Add to sidebar if user wants it @@ -147,7 +278,11 @@ export default { sidebarUnpatch = patchSidebar(); cleanupFunctions.push(() => { if (sidebarUnpatch) { - sidebarUnpatch(); + try { + sidebarUnpatch(); + } catch (e) { + // ignore individual cleanup errors + } sidebarUnpatch = undefined; } }); @@ -155,13 +290,26 @@ export default { log("Sidebar setup failed:", error); } - validateAndInitialize(); + try { + validateAndInitialize(); + log("onLoad complete - validateAndInitialize triggered"); + } catch (e) { + logError("validateAndInitialize threw an error:", e); + } }, onUnload() { log("Plugin unloading..."); pluginState.pluginStopped = true; + // Stop heartbeat first to avoid stray logs after unload + try { + stopHeartbeat(); + log("Heartbeat stopped"); + } catch (e) { + logError("Failed to stop heartbeat:", e); + } + // Run all cleanup functions cleanupFunctions.forEach((cleanup) => { try { @@ -172,7 +320,11 @@ export default { }); cleanupFunctions.length = 0; - stop(); + try { + stop(); + } catch (e) { + logError("Error while stopping plugin manager:", e); + } log("Plugin unloaded"); }, @@ -184,9 +336,13 @@ export default { const newSidebar = newSettings.addToSidebar; // Apply the new settings - Object.assign(currentSettings, newSettings); + try { + Object.assign(currentSettings, newSettings); + } catch (e) { + logError("Failed to apply new settings:", e); + } - log("Settings updated:", Object.keys(newSettings)); + log("Settings updated:", Object.keys(newSettings || {})); // Check if sidebar setting changed if (oldSidebar !== newSidebar) { @@ -199,7 +355,11 @@ export default { } } else { if (sidebarUnpatch) { - sidebarUnpatch(); + try { + sidebarUnpatch(); + } catch (e) { + logError("Failed to unpatch sidebar:", e); + } sidebarUnpatch = undefined; log("Sidebar disabled"); } @@ -209,7 +369,11 @@ export default { // Switch services if needed if (oldService !== newService && newService) { log(`Service changed from ${oldService || "none"} to ${newService}`); - await switchService(newService); + try { + await switchService(newService); + } catch (e) { + logError("Failed to switch service:", e); + } } else if (!pluginState.pluginStopped && currentSettings.service) { // Just restart with the updated settings (only if service is selected) log("Restarting with updated settings..."); @@ -217,7 +381,11 @@ export default { } else if (!currentSettings.service) { // Service was unselected, stop the plugin log("Service unselected, stopping plugin..."); - stop(); + try { + stop(); + } catch (e) { + logError("Error while stopping due to service unselected:", e); + } } }, diff --git a/plugins/multi-scrobbler/src/manager.ts b/plugins/multi-scrobbler/src/manager.ts index 5db9580..380d5d4 100644 --- a/plugins/multi-scrobbler/src/manager.ts +++ b/plugins/multi-scrobbler/src/manager.ts @@ -53,8 +53,14 @@ class PluginManager { // Check for new tracks and update Discord status when something changes private async updateActivity() { if (pluginState.pluginStopped) { + log("Plugin is stopped; skipping activity updates and clearing timers"); logVerbose("Plugin is stopped, skipping update"); - this.stopUpdates(); + try { + this.stopUpdates(); + logVerbose("Update timers cleared due to stopped plugin"); + } catch (e) { + logError("Error while stopping updates for stopped plugin:", e); + } return; } @@ -75,10 +81,16 @@ class PluginManager { }); if (ignoredActivity) { + log(`Ignored app (${ignoredActivity.name}) is currently active; clearing activity and skipping updates`); logVerbose( `Ignored app (${ignoredActivity.name}) is currently playing, clearing activity`, ); - // Remove these setDebugInfo calls that cause TypeScript errors + try { + // Provide a little debug context so we can see why updates are skipped + setDebugInfo("ignoredActivity", ignoredActivity.name); + } catch (e) { + logVerbose("Failed to set debug info for ignored activity:", e); + } clearActivity(); return; } @@ -91,13 +103,20 @@ class PluginManager { setDebugInfo("lastTrack", lastTrack); if (!lastTrack.nowPlaying) { + log("No currently playing track reported by service; clearing activity and skipping RPC update"); logVerbose("Track is not currently playing"); + try { + setDebugInfo("lastTrack_nowPlaying", false); + } catch (e) { + logVerbose("Failed to set debug info for nowPlaying:", e); + } clearActivity(); return; } // use simple URL comparison like Last.fm plugin if (pluginState.lastTrackUrl === lastTrack.url) { + log("Track unchanged; skipping Discord RPC update"); logVerbose("Track hasn't changed"); recordSuccessfulUpdate(); this.consecutiveFailures = 0; @@ -255,7 +274,20 @@ class PluginManager { ); } catch (error) { logError("Update failed:", error); - recordServiceError(currentSettings.service, (error as Error).message); + try { + recordServiceError(currentSettings.service, (error as Error).message); + } catch (e) { + logError("Failed to record service error:", e); + } + try { + setDebugInfo("lastUpdateError", { + message: (error as Error).message, + service: currentSettings.service, + lastTrackUrl: pluginState.lastTrackUrl, + }); + } catch (e) { + logVerbose("Failed to set debug info for last update error:", e); + } this.handleError(error as Error); } }