summaryrefslogtreecommitdiff
path: root/frontend/src/plugin-loader.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/plugin-loader.tsx')
-rw-r--r--frontend/src/plugin-loader.tsx104
1 files changed, 89 insertions, 15 deletions
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 2bdfcec1..fd4dc1c0 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -19,6 +19,7 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from '
import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
+import PluginDisableModal from './components/modals/PluginDisableModal';
import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
@@ -30,7 +31,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
-import { InstallType, Plugin, PluginLoadType } from './plugin';
+import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store';
@@ -91,6 +92,7 @@ class PluginLoader extends Logger {
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
+ DeckyBackend.addEventListener('loader/disable_plugin', this.doDisablePlugin.bind(this));
DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
DeckyBackend.addEventListener(
'loader/add_multiple_plugins_install_prompt',
@@ -175,7 +177,7 @@ class PluginLoader extends Logger {
private getPluginsFromBackend = DeckyBackend.callable<
[],
- { name: string; version: string; load_type: PluginLoadType }[]
+ { name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
>('loader/get_plugins');
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
@@ -198,10 +200,16 @@ class PluginLoader extends Logger {
this.runCrashChecker();
const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = [];
+ const disabledPlugins: DisabledPlugin[] = [];
const loadStart = performance.now();
for (const plugin of plugins) {
- if (!this.hasPlugin(plugin.name))
- pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
+ if (plugin.disabled) {
+ disabledPlugins.push({ name: plugin.name, version: plugin.version });
+ this.deckyState.setDisabledPlugins(disabledPlugins);
+ } else {
+ if (!this.hasPlugin(plugin.name))
+ pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
+ }
}
await Promise.all(pluginLoadPromises);
const loadEnd = performance.now();
@@ -252,7 +260,9 @@ class PluginLoader extends Logger {
public async checkPluginUpdates() {
const frozenPlugins = this.deckyState.publicState().frozenPlugins;
- const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
+ const updates = await checkForPluginUpdates(
+ this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)),
+ );
this.deckyState.setUpdates(updates);
return updates;
}
@@ -290,6 +300,7 @@ class PluginLoader extends Logger {
version={version}
hash={hash}
installType={install_type}
+ disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>,
@@ -303,6 +314,7 @@ class PluginLoader extends Logger {
showModal(
<MultiplePluginsInstallModal
requests={requests}
+ disabledPlugins={this.deckyState.publicState().disabledPlugins}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>,
@@ -310,7 +322,19 @@ class PluginLoader extends Logger {
}
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
- showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
+ showModal(
+ <PluginUninstallModal
+ name={name}
+ title={title}
+ buttonText={buttonText}
+ description={description}
+ deckyState={this.deckyState}
+ />,
+ );
+ }
+
+ public disablePlugin(name: string, title: string, buttonText: string, description: string) {
+ showModal(<PluginDisableModal name={name} title={title} buttonText={buttonText} description={description} />);
}
public hasPlugin(name: string) {
@@ -351,6 +375,19 @@ class PluginLoader extends Logger {
this.errorBoundaryHook.deinit();
}
+ public doDisablePlugin(name: string) {
+ const plugin = this.plugins.find((plugin) => plugin.name === name);
+ if (plugin == undefined) return;
+
+ plugin?.onDismount?.();
+ this.plugins = this.plugins.filter((p) => p !== plugin);
+ this.deckyState.setDisabledPlugins([
+ ...this.deckyState.publicState().disabledPlugins,
+ { name: plugin.name, version: plugin.version },
+ ]);
+ this.deckyState.setPlugins(this.plugins);
+ }
+
public unloadPlugin(name: string, skipStateUpdate: boolean = false) {
const plugin = this.plugins.find((plugin) => plugin.name === name);
plugin?.onDismount?.();
@@ -363,6 +400,7 @@ class PluginLoader extends Logger {
version?: string | undefined,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
useQueue: boolean = true,
+ timeoutMS?: number,
) {
if (useQueue && this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
@@ -376,9 +414,11 @@ class PluginLoader extends Logger {
this.unloadPlugin(name, true);
const startTime = performance.now();
- await this.importReactPlugin(name, version, loadType);
+
+ await this.importReactPlugin(name, version, loadType, timeoutMS);
const endTime = performance.now();
+ this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
} catch (e) {
@@ -388,7 +428,7 @@ class PluginLoader extends Logger {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) {
- this.importPlugin(nextPlugin.name, nextPlugin.version, loadType);
+ this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS);
}
}
}
@@ -398,12 +438,28 @@ class PluginLoader extends Logger {
name: string,
version?: string,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
+ timeoutMS?: number,
) {
let spExists = this.checkForSP();
+ const timeoutException = new Error(
+ `${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`,
+ );
+ let timeout: number | undefined;
+
try {
switch (loadType) {
case PluginLoadType.ESMODULE_V1:
- const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
+ const importJS = () => import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
+
+ const promise =
+ timeoutMS === undefined
+ ? importJS()
+ : Promise.race([
+ importJS(),
+ new Promise((_, reject) => (timeout = setTimeout(() => reject(timeoutException), timeoutMS))),
+ ]);
+
+ const plugin_exports = await promise;
let plugin = plugin_exports.default();
this.plugins.push({
@@ -415,12 +471,26 @@ class PluginLoader extends Logger {
break;
case PluginLoadType.LEGACY_EVAL_IIFE:
- let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
- credentials: 'include',
- headers: {
- 'X-Decky-Auth': deckyAuthToken,
- },
- });
+ const fetchJS = async () => {
+ const controller = new AbortController();
+ const { signal } = controller;
+
+ if (timeoutMS !== undefined) timeout = setTimeout(() => controller.abort(), timeoutMS);
+
+ try {
+ return await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
+ credentials: 'include',
+ headers: {
+ 'X-Decky-Auth': deckyAuthToken,
+ },
+ signal,
+ });
+ } catch (e: any) {
+ throw 'name' in e && e.name === 'AbortError' ? timeoutException : e;
+ }
+ };
+
+ let res = await fetchJS();
if (res.ok) {
let plugin_export: (serverAPI: any) => Plugin = await eval(
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
@@ -439,6 +509,8 @@ class PluginLoader extends Logger {
throw new Error(`${name} has no defined loadType.`);
}
} catch (e) {
+ if (e === timeoutException) throw timeoutException;
+
this.error('Error loading plugin ' + name, e);
const TheError: FC<{}> = () => (
<PanelSection>
@@ -481,6 +553,8 @@ class PluginLoader extends Logger {
body: '' + e,
icon: <FaExclamationCircle />,
});
+ } finally {
+ if (timeout !== undefined) clearTimeout(timeout);
}
if (spExists && !this.checkForSP()) {