Skip to content

Commit

Permalink
fix: add maximum activation time for extensions
Browse files Browse the repository at this point in the history
after reaching that time, loader continue without waiting for this extension
extension is flagged as a failed extension

fixes #3280
fixes #2993

Signed-off-by: Florent Benoit <fbenoit@redhat.com>
  • Loading branch information
benoitf committed Aug 21, 2023
1 parent 0fcc67f commit e952bf6
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 5 deletions.
22 changes: 22 additions & 0 deletions packages/main/src/plugin/extension-loader-settings.ts
@@ -0,0 +1,22 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

export enum ExtensionLoaderSettings {
SectionName = 'extensions',
MaxActivationTime = 'maxActivationTime',
}
44 changes: 43 additions & 1 deletion packages/main/src/plugin/extension-loader.spec.ts
Expand Up @@ -66,6 +66,10 @@ class TestExtensionLoader extends ExtensionLoader {
return this.extensionState;
}

getExtensionStateErrors() {
return this.extensionStateErrors;
}

doRequire(module: string): NodeRequire {
return super.doRequire(module);
}
Expand All @@ -79,7 +83,10 @@ const menuRegistry: MenuRegistry = {} as unknown as MenuRegistry;

const providerRegistry: ProviderRegistry = {} as unknown as ProviderRegistry;

const configurationRegistry: ConfigurationRegistry = {} as unknown as ConfigurationRegistry;
const configurationRegistryGetConfigurationMock = vi.fn();
const configurationRegistry: ConfigurationRegistry = {
getConfiguration: configurationRegistryGetConfigurationMock,
} as unknown as ConfigurationRegistry;

const imageRegistry: ImageRegistry = {} as unknown as ImageRegistry;

Expand Down Expand Up @@ -292,6 +299,41 @@ test('Verify extension error leads to failed state', async () => {
expect(extensionLoader.getExtensionState().get(id)).toBe('failed');
});

test('Verify extension activate with a long timeout is flagged as error', async () => {
const id = 'extension.id';

// mock getConfiguration
const getMock = vi.fn();
configurationRegistryGetConfigurationMock.mockReturnValue({
get: getMock,
});
getMock.mockReturnValue(1);

await extensionLoader.activateExtension(
{
id: id,
name: 'id',
path: 'dummy',
api: {} as typeof containerDesktopAPI,
mainPath: '',
removable: false,
manifest: {},
},
{
activate: () => {
// wait for 20 seconds
return new Promise(resolve => setTimeout(resolve, 20000));
},
},
);

expect(extensionLoader.getExtensionStateErrors().get(id)).toBeDefined();
expect(extensionLoader.getExtensionStateErrors().get(id)?.toString()).toContain(
'Extension extension.id activation timed out after 1 seconds',
);
expect(extensionLoader.getExtensionState().get(id)).toBe('failed');
});

test('Verify setExtensionsUpdates', async () => {
// set the private field analyzedExtensions of extensionLoader
const analyzedExtensions = new Map<string, AnalyzedExtension>();
Expand Down
51 changes: 47 additions & 4 deletions packages/main/src/plugin/extension-loader.ts
Expand Up @@ -25,7 +25,7 @@ import * as zipper from 'zip-local';
import type { TrayMenuRegistry } from './tray-menu-registry.js';
import { Disposable } from './types/disposable.js';
import type { ProviderRegistry } from './provider-registry.js';
import type { ConfigurationRegistry } from './configuration-registry.js';
import type { ConfigurationRegistry, IConfigurationNode } from './configuration-registry.js';
import type { ImageRegistry } from './image-registry.js';
import type { MessageBox } from './message-box.js';
import type { ProgressImpl } from './progress-impl.js';
Expand Down Expand Up @@ -65,6 +65,7 @@ import type { Context } from './context/context.js';
import type { OnboardingRegistry } from './onboarding-registry.js';
import { createHttpPatchedModules } from './proxy-resolver.js';
import { ModuleLoader } from './module-loader.js';
import { ExtensionLoaderSettings } from './extension-loader-settings.js';

/**
* Handle the loading of an extension
Expand Down Expand Up @@ -212,6 +213,23 @@ export class ExtensionLoader {
this.moduleLoader.addOverride({ '@podman-desktop/api': ext => ext.api }); // add podman desktop API

this.moduleLoader.overrideRequire();
// register configuration for the max activation time
const maxActivationTimeConfiguration: IConfigurationNode = {
id: 'preferences.extension',
title: 'Extensions',
type: 'object',
properties: {
[ExtensionLoaderSettings.SectionName + '.' + ExtensionLoaderSettings.MaxActivationTime]: {
description: 'Maximum activation time for an extension, in seconds.',
type: 'number',
default: 5,
minimum: 1,
maximum: 100,
},
},
};

this.configurationRegistry.registerConfigurations([maxActivationTimeConfiguration]);
}

protected async setupScanningDirectory(): Promise<void> {
Expand Down Expand Up @@ -1045,10 +1063,35 @@ export class ExtensionLoader {
const telemetryOptions = { extensionId: extension.id };
try {
if (typeof extensionMain?.['activate'] === 'function') {
// maximum time to wait for the extension to activate by reading from configuration
const delayInSeconds: number =
this.configurationRegistry
.getConfiguration(ExtensionLoaderSettings.SectionName)
.get(ExtensionLoaderSettings.MaxActivationTime) || 5;
const delayInMilliseconds = delayInSeconds * 1000;

// reject a promise after this delay
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error(`Extension ${extension.id} activation timed out after ${delayInSeconds} seconds`)),
delayInMilliseconds,
),
);

// it returns exports
console.log(`Activating extension (${extension.id})`);
await extensionMain['activate'].apply(undefined, [extensionContext]);
console.log(`Activating extension (${extension.id}) ended`);
console.log(`Activating extension (${extension.id}) with max activation time of ${delayInSeconds} seconds`);

const beforeActivateTime = performance.now();
const activatePromise = extensionMain['activate'].apply(undefined, [extensionContext]);

// if extension reach the timeout, do not wait for it to finish and flag as error
await Promise.race([activatePromise, timeoutPromise]);
const afterActivateTime = performance.now();
console.log(
`Activating extension (${extension.id}) ended in ${Math.round(
afterActivateTime - beforeActivateTime,
)} milliseconds`,
);
}
const id = extension.id;
const activatedExtension: ActivatedExtension = {
Expand Down

0 comments on commit e952bf6

Please sign in to comment.