From faa90eccd363b635b84be2162376645d6a83ad55 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 1 Mar 2023 01:26:13 -0300 Subject: [PATCH] feat: Crash Handler (#531) Co-authored-by: Ven --- src/api/settings.ts | 2 +- src/components/handleComponentFailed.ts | 31 ++---- src/plugins/crashHandler.ts | 134 ++++++++++++++++++++++++ src/plugins/noF1.ts | 4 +- src/plugins/noRPC.ts | 4 +- src/utils/modal.tsx | 8 ++ src/utils/types.ts | 4 + src/utils/updater.ts | 22 ++++ 8 files changed, 182 insertions(+), 27 deletions(-) create mode 100644 src/plugins/crashHandler.ts diff --git a/src/api/settings.ts b/src/api/settings.ts index d05afcd93..c551df03b 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -92,7 +92,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings { // Return empty for plugins with no settings if (path === "plugins" && p in plugins) return target[p] = makeProxy({ - enabled: plugins[p].required ?? false + enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false }, root, `plugins.${p}`); // Since the property is not set, check if this is a plugin's setting and if so, try to resolve diff --git a/src/components/handleComponentFailed.ts b/src/components/handleComponentFailed.ts index 020e8ef21..43a3ad8b3 100644 --- a/src/components/handleComponentFailed.ts +++ b/src/components/handleComponentFailed.ts @@ -16,29 +16,12 @@ * along with this program. If not, see . */ -import { isOutdated, rebuild, update } from "@utils/updater"; +import { maybePromptToUpdate } from "@utils/updater"; -export async function handleComponentFailed() { - if (isOutdated) { - setImmediate(async () => { - const wantsUpdate = confirm( - "Uh Oh! Failed to render this Page." + - " However, there is an update available that might fix it." + - " Would you like to update and restart now?" - ); - if (wantsUpdate) { - try { - await update(); - await rebuild(); - if (IS_WEB) - location.reload(); - else - DiscordNative.app.relaunch(); - } catch (e) { - console.error(e); - alert("That also failed :( Try updating or reinstalling with the installer!"); - } - } - }); - } +export function handleComponentFailed() { + maybePromptToUpdate( + "Uh Oh! Failed to render this Page." + + " However, there is an update available that might fix it." + + " Would you like to update and restart now?" + ); } diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts new file mode 100644 index 000000000..6f030b12f --- /dev/null +++ b/src/plugins/crashHandler.ts @@ -0,0 +1,134 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { showNotification } from "@api/Notifications"; +import { definePluginSettings } from "@api/settings"; +import { Devs } from "@utils/constants"; +import Logger from "@utils/Logger"; +import { closeAllModals } from "@utils/modal"; +import definePlugin, { OptionType } from "@utils/types"; +import { maybePromptToUpdate } from "@utils/updater"; +import { FluxDispatcher, NavigationRouter } from "@webpack/common"; +import type { ReactElement } from "react"; + +const CrashHandlerLogger = new Logger("CrashHandler"); + +const settings = definePluginSettings({ + attemptToPreventCrashes: { + type: OptionType.BOOLEAN, + description: "Whether to attempt to prevent Discord crashes.", + default: true + }, + attemptToNavigateToHome: { + type: OptionType.BOOLEAN, + description: "Whether to attempt to navigate to the home when preventing Discord crashes.", + default: false + } +}); + +export default definePlugin({ + name: "CrashHandler", + description: "Utility plugin for handling and possibly recovering from Crashes without a restart", + authors: [Devs.Nuckyz], + enabledByDefault: true, + + popAllModals: undefined as (() => void) | undefined, + + settings, + + patches: [ + { + find: ".Messages.ERRORS_UNEXPECTED_CRASH", + replacement: { + match: /(?=this\.setState\()/, + replace: "$self.handleCrash(this)||" + } + }, + { + find: 'dispatch({type:"MODAL_POP_ALL"})', + replacement: { + match: /(?<=(?\i)=function\(\){\(0,\i\.\i\)\(\);\i\.\i\.dispatch\({type:"MODAL_POP_ALL"}\).+};)/, + replace: "$self.popAllModals=$;" + } + } + ], + + handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { + try { + maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); + + if (settings.store.attemptToPreventCrashes) { + this.handlePreventCrash(_this); + return true; + } + + return false; + } catch (err) { + CrashHandlerLogger.error("Failed to handle crash", err); + } + }, + + handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Attempting to recover...", + }); + } catch { } + + try { + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to close open context menu.", err); + } + try { + this.popAllModals?.(); + } catch (err) { + CrashHandlerLogger.debug("Failed to close old modals.", err); + } + try { + closeAllModals(); + } catch (err) { + CrashHandlerLogger.debug("Failed to close all open modals.", err); + } + try { + FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to close user popout.", err); + } + try { + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to pop all layers.", err); + } + if (settings.store.attemptToNavigateToHome) { + try { + NavigationRouter.transitionTo("/channels/@me"); + } catch (err) { + CrashHandlerLogger.debug("Failed to navigate to home", err); + } + } + + try { + _this.forceUpdate(); + } catch (err) { + CrashHandlerLogger.debug("Failed to update crash handler component.", err); + } + } +}); diff --git a/src/plugins/noF1.ts b/src/plugins/noF1.ts index 5c23b73ca..c951149e4 100644 --- a/src/plugins/noF1.ts +++ b/src/plugins/noF1.ts @@ -16,11 +16,13 @@ * along with this program. If not, see . */ +import { migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; +migratePluginSettings("NoF1", "No F1"); export default definePlugin({ - name: "No F1", + name: "NoF1", description: "Disables F1 help bind.", authors: [Devs.Cyn], patches: [ diff --git a/src/plugins/noRPC.ts b/src/plugins/noRPC.ts index e56c7af5e..a78cc27ed 100644 --- a/src/plugins/noRPC.ts +++ b/src/plugins/noRPC.ts @@ -16,11 +16,13 @@ * along with this program. If not, see . */ +import { migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; +migratePluginSettings("NoRPC", "No RPC"); export default definePlugin({ - name: "No RPC", + name: "NoRPC", description: "Disables Discord's RPC server.", authors: [Devs.Cyn], target: "DESKTOP", diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index 3174cace0..35aaaf889 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -117,6 +117,7 @@ const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", { openModal: filters.byCode("onCloseRequest:null!="), closeModal: filters.byCode("onCloseCallback&&"), openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m), + closeAllModals: filters.byCode(".value.key,") }); /** @@ -142,3 +143,10 @@ export function openModal(render: RenderFunction, options?: ModalOptions, contex export function closeModal(modalKey: string, contextKey?: string): void { return ModalAPI.closeModal(modalKey, contextKey); } + +/** + * Close all open modals + */ +export function closeAllModals(): void { + return ModalAPI.closeAllModals(); +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 24915a6e0..96aa4ab6f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -75,6 +75,10 @@ export interface PluginDef { * Whether this plugin is required and forcefully enabled */ required?: boolean; + /** + * Whether this plugin should be enabled by default, but can be disabled + */ + enabledByDefault?: boolean; /** * Set this if your plugin only works on Browser or Desktop, not both */ diff --git a/src/utils/updater.ts b/src/utils/updater.ts index 9fdec471e..e13f5cf7e 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -77,3 +77,25 @@ export async function rebuild() { return oldHashes["patcher.js"] !== newHashes["patcher.js"] || oldHashes["preload.js"] !== newHashes["preload.js"]; } + +export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) { + if (IS_WEB) return; + if (checkForDev && IS_DEV) return; + + try { + const isOutdated = await checkForUpdates(); + if (isOutdated) { + const wantsUpdate = confirm(confirmMessage); + if (wantsUpdate && isNewer) return alert("Your local copy has more recent commits. Please stash or reset them."); + if (wantsUpdate) { + await update(); + const needFullRestart = await rebuild(); + if (needFullRestart) DiscordNative.app.relaunch(); + else location.reload(); + } + } + } catch (err) { + UpdateLogger.error(err); + alert("That also failed :( Try updating or re-installing with the installer!"); + } +}