Skip to content

Commit

Permalink
Merge pull request #7602 from mook-as/rdx/shutdown-hook
Browse files Browse the repository at this point in the history
RDX: Allow hooks on shutdown
  • Loading branch information
jandubois authored Oct 9, 2024
2 parents 2d7b923 + ba17df1 commit 9c4941f
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 16 deletions.
9 changes: 2 additions & 7 deletions background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,14 +608,9 @@ Electron.app.on('before-quit', async(event) => {
httpCredentialHelperServer.closeServer();

try {
await mainEvents.tryInvoke('extensions/shutdown');
await k8smanager?.stop();
try {
await mainEvents.invoke('shutdown-integrations');
} catch (ex) {
if (!`${ ex }`.includes('No handlers registered')) {
throw ex;
}
}
await mainEvents.tryInvoke('shutdown-integrations');

console.log(`2: Child exited cleanly.`);
} catch (ex: any) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"email": "[email protected]"
},
"engines": {
"node": "^20.17"
"node": "20.16.0"
},
"packageManager": "[email protected]",
"repository": {
Expand Down
35 changes: 33 additions & 2 deletions pkg/rancher-desktop/main/extensions/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChildProcessByStdio } from 'child_process';
import { ChildProcessByStdio, spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import { Readable } from 'stream';
Expand Down Expand Up @@ -46,6 +46,11 @@ type ComposeFile = {
volumes?: Record<string, any>;
};

// ScriptType is any key in ExtensionMetadata.host that starts with `x-rd-`.
type ScriptType = keyof {
[K in keyof Required<ExtensionMetadata>['host'] as K extends `x-rd-${ infer _U }` ? K : never]: 1;
};

const console = Logging.extensions;

export class ExtensionErrorImpl extends Error implements ExtensionError {
Expand Down Expand Up @@ -224,7 +229,7 @@ export class ExtensionImpl implements Extension {
* Returns the script executable plus arguments; the executable path is always
* absolute.
*/
protected getScriptArgs(metadata: ExtensionMetadata, key: 'x-rd-install' | 'x-rd-uninstall'): string[] | undefined {
protected getScriptArgs(metadata: ExtensionMetadata, key: ScriptType): string[] | undefined {
const scriptData = metadata.host?.[key]?.[this.platform];

if (!scriptData) {
Expand All @@ -235,6 +240,7 @@ export class ExtensionImpl implements Extension {
const description = {
'x-rd-install': 'Post-install',
'x-rd-uninstall': 'Pre-uninstall',
'x-rd-shutdown': 'Shutdown',
}[key];
const binDir = path.join(this.dir, 'bin');
const scriptPath = path.normalize(path.resolve(binDir, scriptName));
Expand Down Expand Up @@ -632,4 +638,29 @@ export class ExtensionImpl implements Extension {
async readFile(sourcePath: string): Promise<string> {
return await this.client.readFile(this.image, sourcePath, { namespace: this.extensionNamespace });
}

async shutdown() {
// Don't trigger downloading the extension if it hasn't been installed.
const metadata = await this._metadata;

if (!metadata) {
return;
}
try {
const [scriptPath, ...scriptArgs] = this.getScriptArgs(metadata, 'x-rd-shutdown') ?? [];

if (scriptPath) {
console.log(`Running ${ this.id } shutdown script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);
// No need to wait for the script to finish here.
const stream = await console.fdStream;
const process = spawn(scriptPath, scriptArgs, {
detached: true, stdio: ['ignore', stream, stream], cwd: path.dirname(scriptPath), windowsHide: true,
});

process.unref();
}
} catch (ex) {
console.error(`Ignoring error running ${ this.id } post-install script: ${ ex }`);
}
}
}
11 changes: 11 additions & 0 deletions pkg/rancher-desktop/main/extensions/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ export class ExtensionManagerImpl implements ExtensionManager {
})(repo, tag));
}
await Promise.all(tasks);

// Register a listener to shut down extensions on quit
mainEvents.handle('extensions/shutdown', this.triggerExtensionShutdown);
}

/**
Expand Down Expand Up @@ -573,7 +576,15 @@ export class ExtensionManagerImpl implements ExtensionManager {
await Promise.allSettled(Object.values(this.processes).map((proc) => {
proc.deref()?.kill();
}));

mainEvents.handle('extensions/shutdown', undefined);
}

triggerExtensionShutdown = async() => {
await Promise.all((await this.getInstalledExtensions()).map((extension) => {
return extension.shutdown();
}));
};
}

function getExtensionManager(): Promise<ExtensionManager | undefined> {
Expand Down
7 changes: 7 additions & 0 deletions pkg/rancher-desktop/main/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export type ExtensionMetadata = {
* `binaries`. Errors will be ignored.
*/
'x-rd-uninstall'?: PlatformSpecific<string|string[]>,
/**
* Rancher Desktop extension: this will be executed when the application
* quits. The application may exit before the process completes. It is not
* defined what the container engine / Kubernetes cluster may be doing at
* the time this is called.
*/
'x-rd-shutdown'?: PlatformSpecific<string|string[]>,
};
};

Expand Down
45 changes: 39 additions & 6 deletions pkg/rancher-desktop/main/mainEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { TransientSettings } from '@pkg/config/transientSettings';
import { DiagnosticsCheckerResult } from '@pkg/main/diagnostics/types';
import { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';

export class NoMainEventsHandlerError extends Error {
constructor(eventName: string) {
super(`No handlers registered for mainEvents::${ eventName }`);
}
}

/**
* MainEventNames describes the events available over the MainEvents event
* emitter. All normal events are described as methods returning void, with
Expand Down Expand Up @@ -119,6 +125,11 @@ interface MainEventNames {
*/
'extensions/ui/uninstall'(id: string): void;

/**
* Emitted on application quit; this is used to shut down extensions.
*/
'extensions/shutdown'(): Promise<void>;

/**
* Emitted on application quit, used to shut down any integrations. This
* requires feedback from the handler to know when all tasks are complete.
Expand Down Expand Up @@ -223,17 +234,26 @@ interface MainEvents extends EventEmitter {
...args: HandlerParams<eventName>): Promise<HandlerReturn<eventName>>;

/**
* Register a handler that will handle invoke() callers.
* Invoke a handler that returns a promise of a result. Unlike `invoke`, this
* does not raise an exception if the event handler is not registered.
*/
tryInvoke<eventName extends keyof MainEventNames>(
event: IsHandler<eventName> extends true ? eventName : never,
...args: HandlerParams<eventName>): Promise<HandlerReturn<eventName> | undefined>;

/**
* Register a handler that will handle invoke() callers. If the given handler
* is `undefined`, unregister it instead.
*/
handle<eventName extends keyof MainEventNames>(
event: IsHandler<eventName> extends true ? eventName : never,
handler: HandlerType<eventName>
handler: HandlerType<eventName> | undefined,
): void;
}

class MainEventsImpl extends EventEmitter implements MainEvents {
handlers: {
[eventName in keyof MainEventNames]?: HandlerType<eventName> | undefined;
[eventName in keyof MainEventNames]?: IsHandler<eventName> extends true ? HandlerType<eventName> : never;
} = {};

emit<eventName extends keyof MainEventNames>(
Expand Down Expand Up @@ -281,14 +301,27 @@ class MainEventsImpl extends EventEmitter implements MainEvents {
if (handler) {
return await handler(...args);
}
throw new Error(`No handlers registered for mainEvents::${ event }`);
throw new NoMainEventsHandlerError(event);
}

async tryInvoke<eventName extends keyof MainEventNames>(
event: IsHandler<eventName> extends true ? eventName : never,
...args: HandlerParams<eventName>
): Promise<HandlerReturn<eventName> | undefined> {
const handler: HandlerType<eventName> | undefined = this.handlers[event];

return await handler?.(...args);
}

handle<eventName extends keyof MainEventNames>(
event: IsHandler<eventName> extends true ? eventName : never,
handler: HandlerType<eventName>,
handler: HandlerType<eventName> | undefined,
): void {
this.handlers[event] = handler as any;
if (handler) {
this.handlers[event] = handler as any;
} else {
delete this.handlers[event];
}
}
}
const mainEvents: MainEvents = new MainEventsImpl();
Expand Down

0 comments on commit 9c4941f

Please sign in to comment.