diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index 5b26a55a25dcb..e50c8efd42e7b 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -789,29 +789,6 @@ declare module '@podman-desktop/api' { connection: ContainerProviderConnection; } - export interface ProviderConnectionShellAccess { - onData: Event; - onError: Event; - onEnd: Event; - write(data: string): void; - startConnection(): void; - stopConnection(): void; - setWindow(dimensions: ShellDimensions): void; - } - - export interface ShellDimensions { - rows: number; - cols: number; - } - - export interface ProviderConnectionShellAccessError { - error: string; - } - - export interface ProviderConnectionShellAccessData { - data: string; - } - /** * Callback for openning shell session */ diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index c82df06fd9677..a0b087a4e9b55 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -1144,6 +1144,67 @@ export class PluginSystem { }, ); + const providerRegistryShellInProviderConnectionSendCallback = new Map< + number, + { + write: (param: string) => void; + resize: (dimensions: containerDesktopAPI.ProviderConnectionShellDimensions) => void; + } + >(); + this.ipcHandle( + 'provider-registry:shellInProviderConnection', + async ( + _listener, + internalProviderId: string, + connectionInfo: ProviderContainerConnectionInfo | ProviderKubernetesConnectionInfo, + onDataId: number, + ): Promise => { + // provide the data content to the remote side + const shellInProviderConnectionInvocation = await providerRegistry.shellInProviderConnection( + internalProviderId, + connectionInfo, + (content: string) => { + this.getWebContentsSender().send('provider-registry:shellInProviderConnection-onData', onDataId, content); + }, + (error: string) => { + this.getWebContentsSender().send('provider-registry:shellInProviderConnection-onError', onDataId, error); + }, + () => { + this.getWebContentsSender().send('provider-registry:shellInProviderConnection-onEnd', onDataId); + // delete the callback + providerRegistryShellInProviderConnectionSendCallback.delete(onDataId); + }, + ); + // store the callback + providerRegistryShellInProviderConnectionSendCallback.set(onDataId, shellInProviderConnectionInvocation); + return onDataId; + }, + ); + + this.ipcHandle( + 'provider-registry:shellInProviderConnectionSend', + async (_listener, onDataId: number, content: string): Promise => { + const callback = providerRegistryShellInProviderConnectionSendCallback.get(onDataId); + if (callback) { + callback.write(content); + } + }, + ); + + this.ipcHandle( + 'provider-registry:shellInProviderConnectionResize', + async ( + _listener, + onDataId: number, + dimensions: containerDesktopAPI.ProviderConnectionShellDimensions, + ): Promise => { + const callback = providerRegistryShellInProviderConnectionSendCallback.get(onDataId); + if (callback) { + callback.resize(dimensions); + } + }, + ); + const containerProviderRegistryAttachContainerSendCallback = new Map void>(); this.ipcHandle( 'container-provider-registry:attachContainer', diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index 84bd4185e9058..8180409df728e 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -28,6 +28,9 @@ import type { ProviderCleanup, ProviderCleanupAction, ProviderCleanupExecuteOptions, + ProviderConnectionShellAccess, + ProviderConnectionShellAccessSession, + ProviderConnectionShellDimensions, ProviderConnectionStatus, ProviderContainerConnection, ProviderDetectionCheck, @@ -1264,4 +1267,45 @@ export class ProviderRegistry { this.apiSender.send('provider-change', {}); }); } + + async shellInProviderConnection( + internalProviderId: string, + providerConnectionInfo: ProviderContainerConnectionInfo | ProviderKubernetesConnectionInfo, + onData: (data: string) => void, + onError: (error: string) => void, + onEnd: () => void, + ): Promise<{ write: (param: string) => void; resize: (dimensions: ProviderConnectionShellDimensions) => void }> { + try { + const containerConnection = this.getMatchingConnectionFromProvider(internalProviderId, providerConnectionInfo); + let shellAccess: ProviderConnectionShellAccess | undefined; + let connection: ProviderConnectionShellAccessSession | undefined; + if (this.isContainerConnection(containerConnection) && providerConnectionInfo.status === 'started') { + shellAccess = containerConnection.shellAccess; + connection = shellAccess?.open(); + connection?.onData(data => { + onData(data.data); + }); + connection?.onError(error => { + onError(error.error); + }); + connection?.onEnd(onEnd); + } + + return { + write: (data: string): void => { + if (connection) { + connection.write(data); + } + }, + resize: (dimension: ProviderConnectionShellDimensions): void => { + if (connection) { + connection.resize(dimension); + } + }, + }; + } catch (error) { + this.telemetryService.track('shellInProviderConnection.error', error); + throw error; + } + } } diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index 957aa7f31ba09..9d0cbe9dbdbee 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -615,6 +615,84 @@ export function initExposure(): void { }, ); + // callbacks for shellInProviderConnection + let onDataCallbacksShellInProviderConnectionId = 0; + const onDataCallbacksShellInProviderConnection = new Map< + number, + { onData: (data: string) => void; onError: (error: string) => void; onEnd: () => void } + >(); + contextBridge.exposeInMainWorld( + 'shellInProviderConnection', + async ( + internalProviderId: string, + connectionInfo: ProviderContainerConnectionInfo | ProviderKubernetesConnectionInfo, + onData: (data: string) => void, + onError: (error: string) => void, + onEnd: () => void, + ): Promise => { + onDataCallbacksShellInProviderConnectionId++; + onDataCallbacksShellInProviderConnection.set(onDataCallbacksShellInProviderConnectionId, { + onData, + onError, + onEnd, + }); + return ipcInvoke( + 'provider-registry:shellInProviderConnection', + internalProviderId, + connectionInfo, + onDataCallbacksShellInProviderConnectionId, + ); + }, + ); + + contextBridge.exposeInMainWorld( + 'shellInProviderConnectionSend', + async (dataId: number, content: string): Promise => { + return ipcInvoke('provider-registry:shellInProviderConnectionSend', dataId, content); + }, + ); + + contextBridge.exposeInMainWorld( + 'shellInProviderConnectionResize', + async (dataId: number, dimensions: containerDesktopAPI.ProviderConnectionShellDimensions) => { + return ipcInvoke('provider-registry:shellInProviderConnectionResize', dataId, dimensions); + }, + ); + + ipcRenderer.on( + 'provider-registry:shellInProviderConnection-onData', + (_, onDataCallbacksShellInProviderConnectionId: number, data: string) => { + // grab callback from the map + const callback = onDataCallbacksShellInProviderConnection.get(onDataCallbacksShellInProviderConnectionId); + if (callback) { + callback.onData(data); + } + }, + ); + ipcRenderer.on( + 'provider-registry:shellInProviderConnection-onError', + (_, onDataCallbacksShellInProviderConnectionId: number, error: string) => { + // grab callback from the map + const callback = onDataCallbacksShellInProviderConnection.get(onDataCallbacksShellInProviderConnectionId); + if (callback) { + callback.onError(error); + } + }, + ); + + ipcRenderer.on( + 'provider-registry:shellInProviderConnection-onEnd', + (_, onDataCallbacksShellInProviderConnectionId: number) => { + // grab callback from the map + const callback = onDataCallbacksShellInProviderConnection.get(onDataCallbacksShellInProviderConnectionId); + if (callback) { + callback.onEnd(); + // remove callback from the map + onDataCallbacksShellInProviderConnection.delete(onDataCallbacksShellInProviderConnectionId); + } + }, + ); + // callbacks for attachContainer let onDataCallbacksAttachContainerId = 0; const onDataCallbacksAttachContainer = new Map<