Plugin
CrashHandler
Utility plugin for handling and possibly recovering from crashes without a restart
1
import { DataStore } from "@api/index";2
import { showNotification } from "@api/Notifications";3
import { definePluginSettings } from "@api/Settings";4
import { Devs } from "@utils/constants";5
import { Logger } from "@utils/Logger";6
import definePlugin, { OptionType } from "@utils/types";7
import { maybePromptToUpdate } from "@utils/updater";8
import { filters, findBulk, proxyLazyWebpack } from "@webpack";9
import { closeAllModals, DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";10
11
const CrashHandlerLogger = new Logger("CrashHandler");12
13
const { 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
DraftManager22
};23
});24
25
const settings = definePluginSettings({26
attemptToPreventCrashes: {27
type: OptionType.BOOLEAN,28
description: "Whether to attempt to prevent Discord crashes.",29
default: true30
},31
attemptToNavigateToHome: {32
type: OptionType.BOOLEAN,33
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",34
default: false35
}36
});37
38
let hasCrashedOnce = false;39
let isRecovering = false;40
let shouldAttemptRecover = true;41
42
export 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 recover73
if (isRecovering) return;74
isRecovering = true;75
76
// 1 ms timeout to avoid react breaking when re-rendering77
setTimeout(() => {78
try {79
// Prevent a crash loop with an error that could not be handled80
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: true87
});88
} catch { }89
90
return;91
}92
93
shouldAttemptRecover = false;94
// This is enough to avoid a crash loop95
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: true122
});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 happens185
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