Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin Catalog prerequisites #1907

Merged
merged 7 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ frontend-test:
plugins-test:
cd plugins/headlamp-plugin && npm install && ./test-headlamp-plugin.js
cd plugins/headlamp-plugin && ./test-plugins-examples.sh
cd plugins/headlamp-plugin && node ./headlamp-plugin-management.test.js
cd plugins/headlamp-plugin && npx jest ./plugin-management-utils.test.js

# IMAGE_BASE can be used to specify a base final image.
# IMAGE_BASE=debian:latest make image
Expand Down
286 changes: 286 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import open from 'open';
import path from 'path';
import url from 'url';
import yargs from 'yargs';
import PluginManager from '../../plugins/headlamp-plugin/plugin-management-utils';
import i18n from './i18next.config';
import windowSize from './windowSize';

Expand Down Expand Up @@ -98,6 +99,289 @@ const buildManifest = fs.existsSync(manifestFile) ? require(manifestFile) : {};
// make it global so that it doesn't get garbage collected
let mainWindow: BrowserWindow | null;

/**
* `Action` is an interface for an action to be performed by the plugin manager.
*
* @interface
* @property {string} identifier - The unique identifier for the action.
* @property {'INSTALL' | 'UNINSTALL' | 'UPDATE' | 'LIST' | 'CANCEL' | 'GET'} action - The type of the action.
* @property {string} [URL] - The URL for the action. Optional.
* @property {string} [destinationFolder] - The destination folder for the action. Optional.
* @property {string} [headlampVersion] - The version of Headlamp for the action. Optional.
* @property {string} [pluginName] - The name of the plugin for the action. Optional.
*/
interface Action {
identifier: string;
yolossn marked this conversation as resolved.
Show resolved Hide resolved
action: 'INSTALL' | 'UNINSTALL' | 'UPDATE' | 'LIST' | 'CANCEL' | 'GET';
URL?: string;
destinationFolder?: string;
headlampVersion?: string;
pluginName?: string;
}

/**
* `ProgressResp` is an interface for progress response.
*
* @interface
* @property {string} type - The type of the progress response.
* @property {string} message - The message of the progress response.
* @property {Record<string, any>} data - Additional data for the progress response. Optional.
*/
interface ProgressResp {
type: string;
yolossn marked this conversation as resolved.
Show resolved Hide resolved
message: string;
data?: Record<string, any>;
}

/**
* `PluginManagerEventListeners` is a class that manages event listeners for plugins-manager.
*
* @class
*/
class PluginManagerEventListeners {
private cache: {
[key: string]: {
action: 'INSTALL' | 'UNINSTALL' | 'UPDATE' | 'LIST' | 'CANCEL';
progress: ProgressResp;
controller?: AbortController;
percentage?: number;
};
} = {};

constructor() {
this.cache = {};
}

/**
* Converts the progress response to a percentage.
*
* @param {ProgressResp} progress - The progress response object.
* @returns {number} The progress as a percentage.
*/
private convertProgressToPercentage(progress: ProgressResp): number {
switch (progress.message) {
case 'Fetching Plugin Metadata':
return 20;
case 'Plugin Metadata Fetched':
return 30;
case 'Downloading Plugin':
return 50;
case 'Plugin Downloaded':
return 100;
default:
return 0;
}
}

/**
* Sets up event handlers for plugin-manager.
*
* @method
* @name setupEventHandlers
*/
setupEventHandlers() {
ipcMain.on('plugin-manager', (event, data) => {
const eventData = JSON.parse(data) as Action;
const { identifier, action } = eventData;

const updateCache = (progress: ProgressResp) => {
const percentage = this.convertProgressToPercentage(progress);
this.cache[identifier].progress = progress;
this.cache[identifier].percentage = percentage;
};

switch (action) {
case 'INSTALL':
this.handleInstall(eventData, updateCache);
break;
case 'UPDATE':
this.handleUpdate(eventData, updateCache);
break;
case 'UNINSTALL':
this.handleUninstall(eventData, updateCache);
break;
case 'LIST':
this.handleList(event, eventData);
break;
case 'CANCEL':
this.handleCancel(event, identifier);
break;
case 'GET':
this.handleGet(event, identifier);
break;
default:
console.error(`Unknown action: ${action}`);
}
});
}

/**
* Handles the installation process.
*
* @method
* @name handleInstall
* @private
*/
private handleInstall(eventData: Action, updateCache: (progress: ProgressResp) => void) {
const { identifier, URL, destinationFolder, headlampVersion } = eventData;
if (!URL) {
this.cache[identifier] = {
action: 'INSTALL',
progress: { type: 'error', message: 'URL is required' },
};
return;
}

const controller = new AbortController();
this.cache[identifier] = {
action: 'INSTALL',
progress: { type: 'info', message: 'installing plugin' },
percentage: 10,
controller,
};

PluginManager.install(
URL,
destinationFolder,
headlampVersion,
progress => {
updateCache(progress);
},
controller.signal
);
}

/**
* Handles the update process.
*
* @method
illume marked this conversation as resolved.
Show resolved Hide resolved
* @name handleUpdate
* @private
*/
private handleUpdate(eventData: Action, updateCache: (progress: ProgressResp) => void) {
const { identifier, pluginName, destinationFolder, headlampVersion } = eventData;
if (!pluginName) {
this.cache[identifier] = {
action: 'UPDATE',
progress: { type: 'error', message: 'Plugin Name is required' },
};
return;
}

const controller = new AbortController();
this.cache[identifier] = {
action: 'UPDATE',
percentage: 10,
progress: { type: 'info', message: 'updating plugin' },
controller,
};

PluginManager.update(
pluginName,
destinationFolder,
headlampVersion,
progress => {
updateCache(progress);
},
controller.signal
);
}

/**
* Handles the uninstallation process.
*
* @method
* @name handleUninstall
* @private
*/
private handleUninstall(eventData: Action, updateCache: (progress: ProgressResp) => void) {
const { identifier, pluginName, destinationFolder } = eventData;
if (!pluginName) {
this.cache[identifier] = {
action: 'UNINSTALL',
progress: { type: 'error', message: 'Plugin Name is required' },
};
return;
}

this.cache[identifier] = {
action: 'UNINSTALL',
progress: { type: 'info', message: 'uninstalling plugin' },
};

PluginManager.uninstall(pluginName, destinationFolder, progress => {
updateCache(progress);
});
}

/**
* Handles the list event.
*
* @method
* @name handleList
* @param {Electron.IpcMainEvent} event - The IPC Main Event.
* @param {Action} eventData - The event data.
* @private
*/
private handleList(event: Electron.IpcMainEvent, eventData: Action) {
const { identifier, destinationFolder } = eventData;
PluginManager.list(destinationFolder, progress => {
event.sender.send('plugin-manager', JSON.stringify({ identifier: identifier, ...progress }));
});
}

/**
* Handles the cancel event.
*
* @method
* @name handleCancel
* @param {Electron.IpcMainEvent} event - The IPC Main Event.
* @param {string} identifier - The identifier of the event to cancel.
* @private
*/
private handleCancel(event: Electron.IpcMainEvent, identifier: string) {
const cacheEntry = this.cache[identifier];
if (cacheEntry?.controller) {
cacheEntry.controller.abort();
event.sender.send(
'plugin-manager',
JSON.stringify({ type: 'success', message: 'cancelled' })
);
}
}

/**
* Handles the get event.
*
* @method
* @name handleGet
* @param {Electron.IpcMainEvent} event - The IPC Main Event.
* @param {string} identifier - The identifier of the event to get.
* @private
*/
private handleGet(event: Electron.IpcMainEvent, identifier: string) {
const cacheEntry = this.cache[identifier];
if (cacheEntry) {
event.sender.send(
'plugin-manager',
JSON.stringify({
identifier: identifier,
...cacheEntry.progress,
percentage: cacheEntry.percentage,
})
);
} else {
event.sender.send(
'plugin-manager',
JSON.stringify({
type: 'error',
message: 'No such operation in progress',
})
);
}
}
}

function startServer(flags: string[] = []): ChildProcessWithoutNullStreams {
const serverFilePath = isDev
? path.resolve('../backend/headlamp-server')
Expand Down Expand Up @@ -772,6 +1056,8 @@ function startElecron() {

ipcMain.on('run-command', handleRunCommand);

new PluginManagerEventListeners().setupEventHandlers();

if (!useExternalServer) {
const runningHeadlamp = await getRunningHeadlampPIDs();
let shouldWaitForKill = true;
Expand Down
14 changes: 13 additions & 1 deletion app/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktopApi', {
send: (channel, data) => {
// allowed channels
const validChannels = ['setMenu', 'locale', 'appConfig', 'pluginsLoaded', 'run-command'];
const validChannels = [
'setMenu',
'locale',
'appConfig',
'pluginsLoaded',
'run-command',
'plugin-manager',
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
Expand All @@ -19,10 +26,15 @@ contextBridge.exposeInMainWorld('desktopApi', {
'command-stdout',
'command-stderr',
'command-exit',
'plugin-manager',
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},

removeListener: (channel, func) => {
ipcRenderer.removeListener(channel, func);
},
});
Loading
Loading