Skip to content

Commit

Permalink
fix: add maximum activation time for extensions (#3446)
Browse files Browse the repository at this point in the history
* fix: add maximum activation time for extensions

after reaching that time (default is 10s), 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 22, 2023
1 parent eaf4c08 commit 5a1f5e1
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: 10,
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 5a1f5e1

Please sign in to comment.