diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index a394116b7d9..7d9cb8af40f 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -202,7 +202,18 @@ declare module '@tmpwip/extension-api' { create(params: { [key: string]: any }): Promise; } + export interface CheckResult { + successful: boolean; + description?: string; + } + + export interface InstallCheck { + title: string; + execute(): Promise; + } + export interface ProviderInstallation { + preflightChecks?(): InstallCheck[]; // ask to install the provider install(logger: Logger): Promise; } diff --git a/packages/main/src/plugin/api/provider-info.ts b/packages/main/src/plugin/api/provider-info.ts index 5beb8cca59b..88f4fb75b9f 100644 --- a/packages/main/src/plugin/api/provider-info.ts +++ b/packages/main/src/plugin/api/provider-info.ts @@ -73,3 +73,19 @@ export interface ProviderInfo { version: string; }; } + +export interface PreflightChecksCallback { + startCheck: (status: CheckStatus) => void; + endCheck: (status: CheckStatus) => void; +} + +export interface CheckStatus { + name: string; + successful?: boolean; + description?: string; +} + +export interface PreflightCheckEvent { + type: 'start' | 'stop'; + status: CheckStatus; +} diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index bf117489eb0..202b8c29504 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -31,7 +31,12 @@ import { ConfigurationRegistry } from './configuration-registry'; import { TerminalInit } from './terminal-init'; import { ImageRegistry } from './image-registry'; import { EventEmitter } from 'node:events'; -import type { ProviderContainerConnectionInfo, ProviderInfo } from './api/provider-info'; +import type { + PreflightCheckEvent, + PreflightChecksCallback, + ProviderContainerConnectionInfo, + ProviderInfo, +} from './api/provider-info'; import type { WebContents } from 'electron'; import { ipcMain, BrowserWindow } from 'electron'; import type { ContainerCreateOptions, ContainerInfo } from './api/container-info'; @@ -331,6 +336,27 @@ export class PluginSystem { return providerRegistry.installProvider(providerInternalId); }); + this.ipcHandle( + 'provider-registry:runInstallPreflightChecks', + async (_, providerInternalId: string, callbackId: number): Promise => { + const callback: PreflightChecksCallback = { + startCheck: status => { + this.getWebContentsSender().send('provider-registry:installPreflightChecksUpdate', callbackId, { + type: 'start', + status, + } as PreflightCheckEvent); + }, + endCheck: status => { + this.getWebContentsSender().send('provider-registry:installPreflightChecksUpdate', callbackId, { + type: 'stop', + status, + } as PreflightCheckEvent); + }, + }; + return providerRegistry.runPreflightChecks(providerInternalId, callback); + }, + ); + this.ipcHandle('provider-registry:startProvider', async (_, providerInternalId: string): Promise => { return providerRegistry.startProvider(providerInternalId); }); diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index 0c3fde36dc4..c053e5400b2 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -32,6 +32,7 @@ import type { ProviderInfo, ProviderKubernetesConnectionInfo, LifecycleMethod, + PreflightChecksCallback, } from './api/provider-info'; import type { ContainerProviderRegistry } from './container-registry'; import { LifecycleContextImpl, LoggerImpl } from './lifecycle-context'; @@ -244,6 +245,44 @@ export class ProviderRegistry { return provider.detectionChecks; } + async runPreflightChecks(providerInternalId: string, statusCallback: PreflightChecksCallback): Promise { + const provider = this.getMatchingProvider(providerInternalId); + const providerInstall = this.providerInstallations.get(providerInternalId); + if (!providerInstall) { + throw new Error(`No matching installation for provider ${provider.internalId}`); + } + + if (!providerInstall.preflightChecks) { + return false; + } + + const checks = providerInstall.preflightChecks(); + for (const check of checks) { + statusCallback.startCheck({ name: check.title }); + try { + const checkResult = await check.execute(); + + statusCallback.endCheck({ + name: check.title, + successful: checkResult.successful, + description: checkResult.description, + }); + + if (!checkResult.successful) { + return false; + } + } catch (err) { + console.error(err); + statusCallback.endCheck({ + name: check.title, + successful: false, + description: err instanceof Error ? err.message : typeof err === 'object' ? err?.toString() : 'unknown error', + }); + } + } + return true; + } + async installProvider(providerInternalId: string): Promise { const provider = this.getMatchingProvider(providerInternalId); diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index e5af8d28c94..4a8b4220552 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -28,7 +28,12 @@ import type { ContributionInfo } from '../../main/src/plugin/api/contribution-in import type { ImageInfo } from '../../main/src/plugin/api/image-info'; import type { ImageInspectInfo } from '../../main/src/plugin/api/image-inspect-info'; import type { ExtensionInfo } from '../../main/src/plugin/api/extension-info'; -import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../main/src/plugin/api/provider-info'; +import type { + PreflightCheckEvent, + PreflightChecksCallback, + ProviderContainerConnectionInfo, + ProviderInfo, +} from '../../main/src/plugin/api/provider-info'; import type { IConfigurationPropertyRecordedSchema } from '../../main/src/plugin/configuration-registry'; import type { PullEvent } from '../../main/src/plugin/api/pull-event'; import { Deferred } from './util/deferred'; @@ -260,6 +265,31 @@ function initExposure(): void { }, ); + const preflightChecksCallbacks = new Map(); + let checkCallbackId = 0; + contextBridge.exposeInMainWorld( + 'runInstallPreflightChecks', + async (providerId: string, callBack: PreflightChecksCallback) => { + checkCallbackId++; + preflightChecksCallbacks.set(checkCallbackId, callBack); + return await ipcInvoke('provider-registry:runInstallPreflightChecks', providerId, checkCallbackId); + }, + ); + + ipcRenderer.on('provider-registry:installPreflightChecksUpdate', (_, callbackId, data: PreflightCheckEvent) => { + const callback = preflightChecksCallbacks.get(callbackId); + if (callback) { + switch (data.type) { + case 'start': + callback.startCheck(data.status); + break; + case 'stop': + callback.endCheck(data.status); + break; + } + } + }); + contextBridge.exposeInMainWorld( 'updateProvider', async (providerId: string): Promise => { diff --git a/packages/renderer/src/lib/welcome/ProviderInstallationButton.svelte b/packages/renderer/src/lib/welcome/ProviderInstallationButton.svelte index 5c8dc3a59ed..da3a05969e3 100644 --- a/packages/renderer/src/lib/welcome/ProviderInstallationButton.svelte +++ b/packages/renderer/src/lib/welcome/ProviderInstallationButton.svelte @@ -1,13 +1,45 @@
@@ -24,7 +25,7 @@ let detectionChecks: ProviderDetectionCheck[] = [];
- +
{#if detectionChecks.length > 0}
@@ -38,5 +39,26 @@ let detectionChecks: ProviderDetectionCheck[] = []; {/each}
{/if} + {#if preflightChecks.length > 0} +
+ {#each preflightChecks as preCheck} +
+

+ {#if preCheck.successful === undefined} + + + + {:else} + {preCheck.successful ? '✅' : '❌'} + {/if} + {preCheck.name} +

+ {#if preCheck.description} + Details:

{preCheck.description}

+ {/if} +
+ {/each} +
+ {/if}