Plugin

CrashHandler

Utility plugin for handling and possibly recovering from crashes without a restart

Utility Developers
index.ts
Download

Source

src/plugins/crashHandler/index.ts
1import { DataStore } from "@api/index";
2import { showNotification } from "@api/Notifications";
3import { definePluginSettings } from "@api/Settings";
4import { Devs } from "@utils/constants";
5import { Logger } from "@utils/Logger";
6import definePlugin, { OptionType } from "@utils/types";
7import { maybePromptToUpdate } from "@utils/updater";
8import { filters, findBulk, proxyLazyWebpack } from "@webpack";
9import { closeAllModals, DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
10
11const CrashHandlerLogger = new Logger("CrashHandler");
12
13const { ModalStack, DraftManager } = proxyLazyWebpack(() => {
14 const [ModalStack, DraftManager] = findBulk(
15 filters.byProps("pushLazy", "popAll"),
16 filters.byProps("clearDraft", "saveDraft"),
17 );
18
19 return {
20 ModalStack,
21 DraftManager
22 };
23});
24
25const settings = definePluginSettings({
26 attemptToPreventCrashes: {
27 type: OptionType.BOOLEAN,
28 description: "Whether to attempt to prevent Discord crashes.",
29 default: true
30 },
31 attemptToNavigateToHome: {
32 type: OptionType.BOOLEAN,
33 description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
34 default: false
35 }
36});
37
38let hasCrashedOnce = false;
39let isRecovering = false;
40let shouldAttemptRecover = true;
41
42export default definePlugin({
43 name: "CrashHandler",
44 description: "Utility plugin for handling and possibly recovering from crashes without a restart",
45 authors: [Devs.Nuckyz],
46 tags: ["Utility", "Developers"],
47 enabledByDefault: true,
48 settings,
49
50 patches: [
51 {
52 find: "#{intl::ERRORS_UNEXPECTED_CRASH}",
53 replacement: {
54 match: /this\.setState\((.+?)\)/,
55 replace: "$self.handleCrash(this,$1);"
56 }
57 }
58 ],
59
60 handleCrash(_this: any, errorState: any) {
61 DataStore.del("KeepCurrentChannel_previousData");
62
63 if (IS_DEV) {
64 try {
65 if (errorState?.info && "componentStack" in errorState.info) {
66 console.error("Component Stack:", errorState.info.componentStack);
67 }
68 } catch { }
69 }
70 _this.setState(errorState);
71
72 // Already recovering, prevent error which happens more than once too fast to trigger another recover
73 if (isRecovering) return;
74 isRecovering = true;
75
76 // 1 ms timeout to avoid react breaking when re-rendering
77 setTimeout(() => {
78 try {
79 // Prevent a crash loop with an error that could not be handled
80 if (!shouldAttemptRecover) {
81 try {
82 showNotification({
83 color: "#eed202",
84 title: "Discord has crashed!",
85 body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.",
86 noPersist: true
87 });
88 } catch { }
89
90 return;
91 }
92
93 shouldAttemptRecover = false;
94 // This is enough to avoid a crash loop
95 setTimeout(() => shouldAttemptRecover = true, 1000);
96 } catch { }
97
98 try {
99 if (!hasCrashedOnce) {
100 hasCrashedOnce = true;
101 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);
102 }
103 } catch { }
104
105 try {
106 if (settings.store.attemptToPreventCrashes) {
107 this.handlePreventCrash(_this);
108 }
109 } catch (err) {
110 CrashHandlerLogger.error("Failed to handle crash", err);
111 }
112 }, 1);
113 },
114
115 handlePreventCrash(_this: any) {
116 try {
117 showNotification({
118 color: "#eed202",
119 title: "Discord has crashed!",
120 body: "Attempting to recover...",
121 noPersist: true
122 });
123 } catch { }
124
125 try {
126 const channelId = SelectedChannelStore.getChannelId();
127
128 for (const key in DraftType) {
129 if (!Number.isNaN(Number(key))) continue;
130
131 DraftManager.clearDraft(channelId, DraftType[key]);
132 }
133 } catch (err) {
134 CrashHandlerLogger.debug("Failed to clear drafts.", err);
135 }
136 try {
137 ExpressionPickerStore.closeExpressionPicker();
138 }
139 catch (err) {
140 CrashHandlerLogger.debug("Failed to close expression picker.", err);
141 }
142 try {
143 FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
144 } catch (err) {
145 CrashHandlerLogger.debug("Failed to close open context menu.", err);
146 }
147 try {
148 ModalStack.popAll();
149 } catch (err) {
150 CrashHandlerLogger.debug("Failed to close old modals.", err);
151 }
152 try {
153 closeAllModals();
154 } catch (err) {
155 CrashHandlerLogger.debug("Failed to close all open modals.", err);
156 }
157 try {
158 FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
159 } catch (err) {
160 CrashHandlerLogger.debug("Failed to close user popout.", err);
161 }
162 try {
163 FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
164 } catch (err) {
165 CrashHandlerLogger.debug("Failed to pop all layers.", err);
166 }
167 try {
168 FluxDispatcher.dispatch({
169 type: "DEV_TOOLS_SETTINGS_UPDATE",
170 settings: { displayTools: false, lastOpenTabId: "analytics" }
171 });
172 } catch (err) {
173 CrashHandlerLogger.debug("Failed to close DevTools.", err);
174 }
175
176 if (settings.store.attemptToNavigateToHome) {
177 try {
178 NavigationRouter.transitionToGuild("@me");
179 } catch (err) {
180 CrashHandlerLogger.debug("Failed to navigate to home", err);
181 }
182 }
183
184 // Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
185 setImmediate(() => isRecovering = false);
186
187 try {
188 _this.setState({ error: null, info: null });
189 } catch (err) {
190 CrashHandlerLogger.debug("Failed to update crash handler component.", err);
191 }
192 }
193});
194