feat: app freeze detection and recovery mode
This requires loader support and can only be triggered from a native dialog. Recovery mode toggle won't be a functionality for now. If there's a use, such as binary searching problematic plugins, then it'll be added (similar to VSCode extension bisect).
This commit is contained in:
parent
457df3ebb5
commit
1497e8ef8d
5 changed files with 146 additions and 4 deletions
|
|
@ -13,11 +13,18 @@ export const PluginStatus = {
|
|||
Stopping: 1 << 6,
|
||||
}
|
||||
|
||||
const { storageRootPath } = callNativeMethodSync(
|
||||
const { storageRootPath, defaultsOnly } = callNativeMethodSync(
|
||||
'revenge.plugins.getConstants',
|
||||
[],
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether this boot ignores saved plugin states.
|
||||
*
|
||||
* Only essential and enabled-by-default plugins run. Saved states will come back on the next reload.
|
||||
*/
|
||||
export const isDefaultsOnlyBoot = defaultsOnly
|
||||
|
||||
/**
|
||||
* Absolute path to per-plugin storage directory.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -321,6 +321,9 @@ export interface PluginSettingsComponent<
|
|||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface NativeMethods {
|
||||
'revenge.plugins.getConstants': [[], { storageRootPath: string }]
|
||||
'revenge.plugins.getConstants': [
|
||||
[],
|
||||
{ storageRootPath: string; defaultsOnly: boolean },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
90
src/plugins/start/recovery/index.tsx
Normal file
90
src/plugins/start/recovery/index.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { onFluxEventDispatched } from '@revenge-mod/discord/flux'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import { callNativeMethod } from '@revenge-mod/modules/native'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import { asap } from '@revenge-mod/utils/callback'
|
||||
import { FullVersion } from '~constants'
|
||||
import { RouteNames } from '../settings.plugins/constants'
|
||||
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.recovery',
|
||||
name: 'Recovery',
|
||||
description: 'Provides troubleshooting options.',
|
||||
author: 'Revenge',
|
||||
icon: 'WrenchIcon',
|
||||
},
|
||||
{
|
||||
start() {
|
||||
// Freeze detection
|
||||
const id = setTimeout(() => {
|
||||
try {
|
||||
callNativeMethod('revenge.alertError', [
|
||||
'Application seems frozen. This is likely caused by a plugin.\n\nYou can launch with default plugins in the Recovery menu.',
|
||||
FullVersion,
|
||||
])
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Failed to call native method "revenge.alertError":',
|
||||
e,
|
||||
)
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
const unsub = onFluxEventDispatched('APP_STATE_UPDATE', e => {
|
||||
clearTimeout(id)
|
||||
unsub()
|
||||
return e
|
||||
})
|
||||
|
||||
if (isDefaultsOnlyBoot) {
|
||||
asap(() => {
|
||||
AlertActionCreators.openAlert(
|
||||
'revenge-recovery',
|
||||
<RecoveryModal />,
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
|
||||
)
|
||||
|
||||
function RecoveryModal() {
|
||||
const navigation = RootNavigationRef.getRootNavigationRef()
|
||||
|
||||
return (
|
||||
<Design.AlertModal
|
||||
title="Recovery Mode"
|
||||
content={
|
||||
'Running with default plugins. Your enabled plugins are saved and will come back when you reload.\n\nUninstall plugins that might be causing issues, then reload the app to get back on track.'
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Design.AlertActionButton
|
||||
variant="primary"
|
||||
text="View plugins"
|
||||
onPress={() => {
|
||||
// React Navigation will bubble our requests to the main navigator if we're already in settings
|
||||
navigation.navigate('settings', {
|
||||
screen: RouteNames.RevengePlugins,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Design.AlertActionButton
|
||||
variant="secondary"
|
||||
text="Got it"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { isPluginEnabled, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useEffect } from 'react'
|
||||
import { RouteNames, Setting } from '../constants'
|
||||
|
|
@ -11,7 +12,9 @@ const RevengePluginsSetting: SettingsItem = {
|
|||
type: 'route',
|
||||
IconComponent: () => <TableRowAssetIcon name="PuzzlePieceIcon" />,
|
||||
useTitle: () => 'Plugins',
|
||||
useTrailing: () => `${useEnabledPluginCount()} enabled`,
|
||||
useTrailing: () =>
|
||||
`${useEnabledPluginCount()} enabled` +
|
||||
(isDefaultsOnlyBoot ? ' (recovery)' : ''),
|
||||
screen: {
|
||||
route: RouteNames[Setting.RevengePlugins],
|
||||
getComponent: () => RevengePluginsSettingScreen.default,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Page from '@revenge-mod/components/Page'
|
|||
import SearchInput from '@revenge-mod/components/SearchInput'
|
||||
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
|
||||
import {
|
||||
getInternalPluginMeta,
|
||||
InternalPluginFlags,
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { isDefaultsOnlyBoot } from '@revenge-mod/plugins/constants'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import {
|
||||
useCallback,
|
||||
|
|
@ -38,7 +40,8 @@ import type { NavigationProp, RouteProp } from '@react-navigation/core'
|
|||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
|
||||
const { Stack, IconButton, FloatingActionButton, LayerScope } = Design
|
||||
const { Stack, IconButton, FloatingActionButton, LayerScope, Card, Text } =
|
||||
Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon')!
|
||||
|
|
@ -148,6 +151,41 @@ function BrowseFloatingActionButton() {
|
|||
)
|
||||
}
|
||||
|
||||
function DefaultsOnlyBanner() {
|
||||
const [hide, setHide] = useState(false)
|
||||
|
||||
if (!isDefaultsOnlyBoot || hide) return null
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<Text variant="text-sm/medium">
|
||||
Running with default plugins. Your enabled plugins are saved
|
||||
and will come back when you reload.{'\n\n'}
|
||||
Uninstall plugins that might be causing issues, then reload
|
||||
the app to get back on track.
|
||||
</Text>
|
||||
<Stack spacing={8}>
|
||||
<Design.Button
|
||||
icon={getAssetIdByName('RetryIcon')!}
|
||||
size="sm"
|
||||
text="Reload with all plugins enabled"
|
||||
onPress={() => BundleUpdaterManager.reload()}
|
||||
/>
|
||||
{/* In case it's taking too much vertical space */}
|
||||
<Design.Button
|
||||
icon={getAssetIdByName('EyeSlashIcon')!}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
text="Hide this message"
|
||||
onPress={() => setHide(true)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function snapshotPlugins() {
|
||||
return [...pList.values()].map(
|
||||
plugin => [plugin, getInternalPluginMeta(plugin)] as const,
|
||||
|
|
@ -225,6 +263,7 @@ function Screen() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<DefaultsOnlyBanner />
|
||||
<Stack direction="horizontal">
|
||||
<View style={styles.grow}>
|
||||
<SearchInput onChange={debouncedSetSearch} size="md" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue