From 60041884e6adbe7d1d0c4db70916e204a083c5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Fri, 24 Nov 2023 17:22:52 +0100 Subject: [PATCH] Add a command to list installed plugins. Fixes #12298 (#12818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder --- .../src/generator/backend-generator.ts | 35 ++++++---- .../src/node/sample-mock-open-vsx-server.ts | 38 ++++++++--- .../electron-main-application.ts | 4 +- packages/core/src/node/backend-application.ts | 25 ++++---- packages/core/src/node/cli.spec.ts | 6 +- packages/core/src/node/cli.ts | 14 ++-- .../core/src/node/messaging/ipc-protocol.ts | 2 +- .../plugin-ext/src/common/plugin-protocol.ts | 2 +- .../node/hosted-plugin-deployer-handler.ts | 7 ++ .../main/node/plugin-deployer-contribution.ts | 4 +- .../src/main/node/plugin-deployer-impl.ts | 4 +- .../main/node/plugin-ext-backend-module.ts | 4 ++ .../main/node/plugin-mgmt-cli-contribution.ts | 64 +++++++++++++++++++ .../src/node/default-workspace-server.ts | 2 +- 14 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts index 9d49438d88a3d..5f457d0d9dba5 100644 --- a/dev-packages/application-manager/src/generator/backend-generator.ts +++ b/dev-packages/application-manager/src/generator/backend-generator.ts @@ -88,9 +88,11 @@ ${Array.from(electronMainModules?.values() ?? [], jsModulePath => `\ await load(require('${jsModulePath}'));`).join(EOL)} await start(); } catch (reason) { - console.error('Failed to start the electron application.'); - if (reason) { - console.error(reason); + if (typeof reason !== 'number') { + console.error('Failed to start the electron application.'); + if (reason) { + console.error(reason); + } } app.quit(); }; @@ -139,8 +141,17 @@ async function start(port, host, argv = process.argv) { if (!container.isBound(BackendApplicationServer)) { container.bind(BackendApplicationServer).toConstantValue({ configure: defaultServeStatic }); } - await container.get(CliManager).initializeCli(argv); - return container.get(BackendApplication).start(port, host); + let result = undefined; + await container.get(CliManager).initializeCli(argv.slice(2), + () => container.get(BackendApplication).configured, + async () => { + result = container.get(BackendApplication).start(port, host); + }); + if (result) { + return result; + } else { + return Promise.reject(0); + } } module.exports = async (port, host, argv) => { @@ -149,9 +160,11 @@ ${Array.from(backendModules.values(), jsModulePath => `\ await load(require('${jsModulePath}'));`).join(EOL)} return await start(port, host, argv); } catch (error) { - console.error('Failed to start the backend application:'); - console.error(error); - process.exitCode = 1; + if (typeof error !== 'number') { + console.error('Failed to start the backend application:'); + console.error(error); + process.exitCode = 1; + } throw error; } } @@ -168,9 +181,9 @@ BackendApplicationConfigProvider.set(${this.prettyStringify(this.pck.props.backe const serverModule = require('./server'); const serverAddress = main.start(serverModule()); -serverAddress.then(({ port, address, family }) => { - if (process && process.send) { - process.send({ port, address, family }); +serverAddress.then((addressInfo) => { + if (process && process.send && addressInfo) { + process.send(addressInfo); } }); diff --git a/examples/api-samples/src/node/sample-mock-open-vsx-server.ts b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts index f2f1fdc1bf465..4a60f8cbf47eb 100644 --- a/examples/api-samples/src/node/sample-mock-open-vsx-server.ts +++ b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts @@ -21,6 +21,9 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { OVSXMockClient, VSXExtensionRaw } from '@theia/ovsx-client'; import * as path from 'path'; import { SampleAppInfo } from '../common/vsx/sample-app-info'; +import * as http from 'http'; +import * as https from 'https'; +import { Deferred } from '@theia/core/lib/common/promise-util'; type VersionedId = `${string}.${string}@${string}`; @@ -35,6 +38,16 @@ export class SampleMockOpenVsxServer implements BackendApplicationContribution { @inject(SampleAppInfo) protected appInfo: SampleAppInfo; + protected mockClient: OVSXMockClient; + protected staticFileHandlers: Map>; + + private readyDeferred = new Deferred(); + private ready = this.readyDeferred.promise; + get mockServerPath(): string { return '/mock-open-vsx'; } @@ -43,23 +56,30 @@ export class SampleMockOpenVsxServer implements BackendApplicationContribution { return '../../sample-plugins'; } - async configure(app: express.Application): Promise { + async onStart?(server: http.Server | https.Server): Promise { const selfOrigin = await this.appInfo.getSelfOrigin(); const baseUrl = `${selfOrigin}${this.mockServerPath}`; const pluginsDb = await this.findMockPlugins(this.pluginsDbPath, baseUrl); - const staticFileHandlers = new Map(Array.from(pluginsDb.entries(), ([key, value]) => [key, express.static(value.path)])); - const mockClient = new OVSXMockClient(Array.from(pluginsDb.values(), value => value.data)); + this.staticFileHandlers = new Map(Array.from(pluginsDb.entries(), ([key, value]) => [key, express.static(value.path)])); + this.mockClient = new OVSXMockClient(Array.from(pluginsDb.values(), value => value.data)); + this.readyDeferred.resolve(); + } + + async configure(app: express.Application): Promise { app.use( this.mockServerPath + '/api', express.Router() .get('/-/query', async (req, res) => { - res.json(await mockClient.query(this.sanitizeQuery(req.query))); + await this.ready; + res.json(await this.mockClient.query(this.sanitizeQuery(req.query))); }) .get('/-/search', async (req, res) => { - res.json(await mockClient.search(this.sanitizeQuery(req.query))); + await this.ready; + res.json(await this.mockClient.search(this.sanitizeQuery(req.query))); }) .get('/:namespace', async (req, res) => { - const extensions = mockClient.extensions + await this.ready; + const extensions = this.mockClient.extensions .filter(ext => req.params.namespace === ext.namespace) .map(ext => `${ext.namespaceUrl}/${ext.name}`); if (extensions.length === 0) { @@ -72,15 +92,17 @@ export class SampleMockOpenVsxServer implements BackendApplicationContribution { } }) .get('/:namespace/:name', async (req, res) => { - res.json(mockClient.extensions.find(ext => req.params.namespace === ext.namespace && req.params.name === ext.name)); + await this.ready; + res.json(this.mockClient.extensions.find(ext => req.params.namespace === ext.namespace && req.params.name === ext.name)); }) .get('/:namespace/:name/reviews', async (req, res) => { res.json([]); }) // implicitly GET/HEAD because of the express.static handlers .use('/:namespace/:name/:version/file', async (req, res, next) => { + await this.ready; const versionedId = this.getVersionedId(req.params.namespace, req.params.name, req.params.version); - const staticFileHandler = staticFileHandlers.get(versionedId); + const staticFileHandler = this.staticFileHandlers.get(versionedId); if (!staticFileHandler) { return next(); } diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index a6b6ce5a9d7e0..98e3e207909d5 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -564,8 +564,8 @@ export class ElectronMainApplication { backendProcess.on('error', error => { reject(error); }); - backendProcess.on('exit', () => { - reject(new Error('backend process exited')); + backendProcess.on('exit', code => { + reject(code); }); app.on('quit', () => { // Only issue a kill signal if the backend process is running. diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index b043b3f1ed83d..a93376955750b 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -170,6 +170,8 @@ export class BackendApplication { @inject(Stopwatch) protected readonly stopwatch: Stopwatch; + private _configured: Promise; + constructor( @inject(ContributionProvider) @named(BackendApplicationContribution) protected readonly contributionsProvider: ContributionProvider, @@ -198,7 +200,7 @@ export class BackendApplication { } protected async initialize(): Promise { - for (const contribution of this.contributionsProvider.getContributions()) { + await Promise.all(this.contributionsProvider.getContributions().map(async contribution => { if (contribution.initialize) { try { await this.measure(contribution.constructor.name + '.initialize', @@ -208,18 +210,20 @@ export class BackendApplication { console.error('Could not initialize contribution', error); } } - } + })); + } + + get configured(): Promise { + return this._configured; } @postConstruct() protected init(): void { - this.configure(); + this._configured = this.configure(); } protected async configure(): Promise { - // Do not await the initialization because contributions are expected to handle - // concurrent initialize/configure in undefined order if they provide both - this.initialize(); + await this.initialize(); this.app.get('*.js', this.serveGzipped.bind(this, 'text/javascript')); this.app.get('*.js.map', this.serveGzipped.bind(this, 'application/json')); @@ -233,17 +237,16 @@ export class BackendApplication { this.app.get('*.woff', this.serveGzipped.bind(this, 'font/woff')); this.app.get('*.woff2', this.serveGzipped.bind(this, 'font/woff2')); - for (const contribution of this.contributionsProvider.getContributions()) { + await Promise.all(this.contributionsProvider.getContributions().map(async contribution => { if (contribution.configure) { try { - await this.measure(contribution.constructor.name + '.configure', - () => contribution.configure!(this.app) - ); + await contribution.configure!(this.app); } catch (error) { console.error('Could not configure contribution', error); } } - } + })); + console.info('configured all backend app contributions'); } use(...handlers: express.Handler[]): void { diff --git a/packages/core/src/node/cli.spec.ts b/packages/core/src/node/cli.spec.ts index c24926c23f113..015b675fc590c 100644 --- a/packages/core/src/node/cli.spec.ts +++ b/packages/core/src/node/cli.spec.ts @@ -44,7 +44,7 @@ describe('CliManager', () => { value.resolve(args['foo'] as string); } }); - await manager.initializeCli(['-f', 'bla']); + await manager.initializeCli(['-f', 'bla'], () => Promise.resolve(), () => Promise.resolve()); chai.assert.equal(await value.promise, 'bla'); }); @@ -59,14 +59,14 @@ describe('CliManager', () => { value.resolve(args['bar'] as string); } }); - await manager.initializeCli(['--foo']); + await manager.initializeCli(['--foo'], () => Promise.resolve(), () => Promise.resolve()); chai.assert.equal(await value.promise, 'my-default'); }); it('prints help and exits', async () => assertExits(async () => { const manager = new TestCliManager(); - await manager.initializeCli(['--help']); + await manager.initializeCli(['--help'], () => Promise.resolve(), () => Promise.resolve()); }) ); }); diff --git a/packages/core/src/node/cli.ts b/packages/core/src/node/cli.ts index 7985594f66fdc..949d233dfaf2f 100644 --- a/packages/core/src/node/cli.ts +++ b/packages/core/src/node/cli.ts @@ -35,7 +35,7 @@ export class CliManager { constructor(@inject(ContributionProvider) @named(CliContribution) protected readonly contributionsProvider: ContributionProvider) { } - async initializeCli(argv: string[]): Promise { + async initializeCli(argv: string[], postSetArguments: () => Promise, defaultCommand: () => Promise): Promise { const pack = require('../../package.json'); const version = pack.version; const command = yargs.version(version); @@ -43,14 +43,18 @@ export class CliManager { for (const contrib of this.contributionsProvider.getContributions()) { contrib.configure(command); } - const args = command + await command .detectLocale(false) .showHelpOnFail(false, 'Specify --help for available options') .help('help') + .middleware(async args => { + for (const contrib of this.contributionsProvider.getContributions()) { + await contrib.setArguments(args); + } + await postSetArguments(); + }) + .command('$0', false, () => { }, defaultCommand) .parse(argv); - for (const contrib of this.contributionsProvider.getContributions()) { - await contrib.setArguments(args); - } } protected isExit(): boolean { diff --git a/packages/core/src/node/messaging/ipc-protocol.ts b/packages/core/src/node/messaging/ipc-protocol.ts index 4fb65c3248ad6..3bcd3e2813b1b 100644 --- a/packages/core/src/node/messaging/ipc-protocol.ts +++ b/packages/core/src/node/messaging/ipc-protocol.ts @@ -51,7 +51,7 @@ export function checkParentAlive(): void { } catch { process.exit(); } - }, 5000); + }, 5000).unref(); // we don't want this timeout to keep the process alive } } } diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 1540eb6a4e329..19f6e0d7920f4 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -429,7 +429,7 @@ export interface PluginDeployerStartContext { export const PluginDeployer = Symbol('PluginDeployer'); export interface PluginDeployer { - start(): void; + start(): Promise; } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts index f351afc73c772..a08e29c763e81 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts @@ -76,6 +76,13 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { return Array.from(this.deployedBackendPlugins.keys()); } + async getDeployedBackendPlugins(): Promise { + // await first deploy + await this.backendPluginsMetadataDeferred.promise; + // fetch the last deployed state + return Array.from(this.deployedBackendPlugins.values()); + } + getDeployedPluginsById(pluginId: string): DeployedPlugin[] { const matches: DeployedPlugin[] = []; const handle = (plugins: Iterable): void => { diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts b/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts index 6ed7a21eabf88..053ce001bb9df 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts @@ -28,7 +28,7 @@ export class PluginDeployerContribution implements BackendApplicationContributio @inject(PluginDeployer) protected pluginDeployer: PluginDeployer; - initialize(): void { - this.pluginDeployer.start(); + initialize(): Promise { + return this.pluginDeployer.start(); } } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 18e7783efcade..0fe63e7b5f24f 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -75,9 +75,9 @@ export class PluginDeployerImpl implements PluginDeployer { @inject(ContributionProvider) @named(PluginDeployerParticipant) protected readonly participants: ContributionProvider; - public start(): void { + public start(): Promise { this.logger.debug('Starting the deployer with the list of resolvers', this.pluginResolvers); - this.doStart(); + return this.doStart(); } public async initResolvers(): Promise> { diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index de29f8d417ab4..fc3b6996f458d 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -41,6 +41,7 @@ import { WebviewBackendSecurityWarnings } from './webview-backend-security-warni import { PluginUninstallationManager } from './plugin-uninstallation-manager'; import { LocalizationServerImpl } from '@theia/core/lib/node/i18n/localization-server'; import { PluginLocalizationServer } from './plugin-localization-server'; +import { PluginMgmtCliContribution } from './plugin-mgmt-cli-contribution'; export function bindMainBackend(bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind): void { bind(PluginApiContribution).toSelf().inSingletonScope(); @@ -85,6 +86,9 @@ export function bindMainBackend(bind: interfaces.Bind, unbind: interfaces.Unbind bind(PluginCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(PluginCliContribution); + bind(PluginMgmtCliContribution).toSelf().inSingletonScope(); + bind(CliContribution).toService(PluginMgmtCliContribution); + bind(WebviewBackendSecurityWarnings).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(WebviewBackendSecurityWarnings); diff --git a/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts b/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts new file mode 100644 index 0000000000000..098da7600b9eb --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts @@ -0,0 +1,64 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Argv, Arguments } from '@theia/core/shared/yargs'; +import { CliContribution } from '@theia/core/lib/node/cli'; +import { HostedPluginDeployerHandler } from '../../hosted/node/hosted-plugin-deployer-handler'; +import { PluginType } from '../../common'; + +@injectable() +export class PluginMgmtCliContribution implements CliContribution { + + static LIST_PLUGINS = 'list-plugins'; + static SHOW_VERSIONS = '--show-versions'; + static SHOW_BUILTINS = '--show-builtins'; + + @inject(HostedPluginDeployerHandler) + protected deployerHandler: HostedPluginDeployerHandler; + + configure(conf: Argv): void { + conf.command([PluginMgmtCliContribution.LIST_PLUGINS, 'list-extensions'], + 'List the installed plugins', + yargs => yargs.option(PluginMgmtCliContribution.SHOW_VERSIONS, { + description: 'List the versions of the installed plugins', + type: 'boolean', + default: false, + }).option(PluginMgmtCliContribution.SHOW_BUILTINS, { + description: 'List the built-in plugins', + type: 'boolean', + default: false, + }), + + async yargs => { + const showVersions = yargs[PluginMgmtCliContribution.SHOW_VERSIONS]; + const deployedIds = await this.deployerHandler.getDeployedBackendPlugins(); + const pluginType = yargs[PluginMgmtCliContribution.SHOW_BUILTINS] ? PluginType.System : PluginType.User; + process.stdout.write('installed plugins:\n'); + deployedIds.filter(plugin => plugin.type === pluginType).forEach(plugin => { + if (showVersions) { + process.stdout.write(`${plugin.metadata.model.id}@${plugin.metadata.model.version}\n`); + } else { + process.stdout.write(`${plugin.metadata.model.id}\n`); + } + }); + } + ); + } + + setArguments(args: Arguments): void { + } +} diff --git a/packages/workspace/src/node/default-workspace-server.ts b/packages/workspace/src/node/default-workspace-server.ts index ff03c57657ea8..10a7490fc41a5 100644 --- a/packages/workspace/src/node/default-workspace-server.ts +++ b/packages/workspace/src/node/default-workspace-server.ts @@ -43,7 +43,7 @@ export class WorkspaceCliContribution implements CliContribution { } async setArguments(args: yargs.Arguments): Promise { - const workspaceArguments = args._.slice(2).map(probablyAlreadyString => String(probablyAlreadyString)); + const workspaceArguments = args._.map(probablyAlreadyString => String(probablyAlreadyString)); if (workspaceArguments.length === 0 && args['root-dir']) { workspaceArguments.push(String(args['root-dir'])); }