add more robust logging
This commit is contained in:
parent
7cb2d1aa9c
commit
0cca83ff07
2 changed files with 237 additions and 37 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue