"use strict";
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginContributions = exports.AbstractHostedPluginSupport = exports.isConnectionScopedBackendPlugin = exports.ALL_ACTIVATION_EVENT = void 0;
const tslib_1 = require("tslib");
/* eslint-disable @typescript-eslint/no-explicit-any */
const debounce = require("@theia/core/shared/lodash.debounce");
const inversify_1 = require("@theia/core/shared/inversify");
const plugin_protocol_1 = require("../../common/plugin-protocol");
const core_1 = require("@theia/core");
const plugin_ext_api_contribution_1 = require("../../common/plugin-ext-api-contribution");
const plugin_paths_protocol_1 = require("../../main/common/plugin-paths-protocol");
const promise_util_1 = require("@theia/core/lib/common/promise-util");
const env_variables_1 = require("@theia/core/lib/common/env-variables");
const environment_1 = require("@theia/core/shared/@theia/application-package/lib/environment");
const common_1 = require("@theia/core/lib/common");
exports.ALL_ACTIVATION_EVENT = '*';
function isConnectionScopedBackendPlugin(plugin) {
    const entryPoint = plugin.metadata.model.entryPoint;
    // A plugin doesn't have to have any entry-point if it doesn't need the activation handler,
    // in which case it's assumed to be a backend plugin.
    return !entryPoint.headless || !!entryPoint.backend;
}
exports.isConnectionScopedBackendPlugin = isConnectionScopedBackendPlugin;
let AbstractHostedPluginSupport = class AbstractHostedPluginSupport {
    constructor(clientId) {
        this.clientId = clientId;
        this.managers = new Map();
        this.contributions = new Map();
        this.activationEvents = new Set();
        this.onDidChangePluginsEmitter = new core_1.Emitter();
        this.onDidChangePlugins = this.onDidChangePluginsEmitter.event;
        this.deferredWillStart = new promise_util_1.Deferred();
        this.deferredDidStart = new promise_util_1.Deferred();
        this.loadQueue = Promise.resolve(undefined);
        this.load = debounce(() => this.loadQueue = this.loadQueue.then(async () => {
            try {
                await this.runOperation(() => this.doLoad());
            }
            catch (e) {
                console.error('Failed to load plugins:', e);
            }
        }), 50, { leading: true });
    }
    /**
     * Resolves when the initial plugins are loaded and about to be started.
     */
    get willStart() {
        return this.deferredWillStart.promise;
    }
    /**
     * Resolves when the initial plugins are started.
     */
    get didStart() {
        return this.deferredDidStart.promise;
    }
    init() {
        this.theiaReadyPromise = this.createTheiaReadyPromise();
    }
    get plugins() {
        const plugins = [];
        this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata));
        return plugins;
    }
    getPlugin(id) {
        const contributions = this.contributions.get(id);
        return contributions && contributions.plugin;
    }
    /** do not call it, except from the plugin frontend contribution */
    onStart(container) {
        this.container = container;
        this.load();
        this.afterStart();
    }
    afterStart() {
        // Nothing to do in the abstract
    }
    runOperation(operation) {
        return operation();
    }
    async doLoad() {
        const toDisconnect = new core_1.DisposableCollection(core_1.Disposable.create(() => { }));
        await this.beforeSyncPlugins(toDisconnect);
        // process empty plugins as well in order to properly remove stale plugin widgets
        await this.syncPlugins();
        // it has to be resolved before awaiting layout is initialized
        // otherwise clients can hang forever in the initialization phase
        this.deferredWillStart.resolve();
        await this.beforeLoadContributions(toDisconnect);
        if (toDisconnect.disposed) {
            // if disconnected then don't try to load plugin contributions
            return;
        }
        const contributionsByHost = this.loadContributions(toDisconnect);
        await this.afterLoadContributions(toDisconnect);
        await this.theiaReadyPromise;
        if (toDisconnect.disposed) {
            // if disconnected then don't try to init plugin code and dynamic contributions
            return;
        }
        await this.startPlugins(contributionsByHost, toDisconnect);
        this.deferredDidStart.resolve();
    }
    beforeSyncPlugins(toDisconnect) {
        // Nothing to do in the abstract
        return Promise.resolve();
    }
    beforeLoadContributions(toDisconnect) {
        // Nothing to do in the abstract
        return Promise.resolve();
    }
    afterLoadContributions(toDisconnect) {
        // Nothing to do in the abstract
        return Promise.resolve();
    }
    /**
     * Sync loaded and deployed plugins:
     * - undeployed plugins are unloaded
     * - newly deployed plugins are initialized
     */
    async syncPlugins() {
        var _a;
        let initialized = 0;
        const waitPluginsMeasurement = this.measure('waitForDeployment');
        let syncPluginsMeasurement;
        const toUnload = new Set(this.contributions.keys());
        let didChangeInstallationStatus = false;
        try {
            const newPluginIds = [];
            const [deployedPluginIds, uninstalledPluginIds] = await Promise.all([this.server.getDeployedPluginIds(), this.server.getUninstalledPluginIds()]);
            waitPluginsMeasurement.log('Waiting for backend deployment');
            syncPluginsMeasurement = this.measure('syncPlugins');
            for (const versionedId of deployedPluginIds) {
                const unversionedId = plugin_protocol_1.PluginIdentifiers.unversionedFromVersioned(versionedId);
                toUnload.delete(unversionedId);
                if (!this.contributions.has(unversionedId)) {
                    newPluginIds.push(versionedId);
                }
            }
            for (const pluginId of toUnload) {
                (_a = this.contributions.get(pluginId)) === null || _a === void 0 ? void 0 : _a.dispose();
            }
            for (const versionedId of uninstalledPluginIds) {
                const plugin = this.getPlugin(plugin_protocol_1.PluginIdentifiers.unversionedFromVersioned(versionedId));
                if (plugin && plugin_protocol_1.PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) {
                    plugin.metadata.outOfSync = didChangeInstallationStatus = true;
                }
            }
            for (const contribution of this.contributions.values()) {
                if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(plugin_protocol_1.PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) {
                    contribution.plugin.metadata.outOfSync = false;
                    didChangeInstallationStatus = true;
                }
            }
            if (newPluginIds.length) {
                const deployedPlugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds });
                const plugins = [];
                for (const plugin of deployedPlugins) {
                    const accepted = this.acceptPlugin(plugin);
                    if (typeof accepted === 'object') {
                        plugins.push(accepted);
                    }
                    else if (accepted) {
                        plugins.push(plugin);
                    }
                }
                for (const plugin of plugins) {
                    const pluginId = plugin_protocol_1.PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model);
                    const contributions = new PluginContributions(plugin);
                    this.contributions.set(pluginId, contributions);
                    contributions.push(core_1.Disposable.create(() => this.contributions.delete(pluginId)));
                    initialized++;
                }
            }
        }
        finally {
            if (initialized || toUnload.size || didChangeInstallationStatus) {
                this.onDidChangePluginsEmitter.fire(undefined);
            }
            if (!syncPluginsMeasurement) {
                // await didn't complete normally
                waitPluginsMeasurement.error('Backend deployment failed.');
            }
        }
        if (initialized > 0) {
            // Only log sync measurement if there are were plugins to sync.
            syncPluginsMeasurement === null || syncPluginsMeasurement === void 0 ? void 0 : syncPluginsMeasurement.log(`Sync of ${this.getPluginCount(initialized)}`);
        }
        else {
            syncPluginsMeasurement === null || syncPluginsMeasurement === void 0 ? void 0 : syncPluginsMeasurement.stop();
        }
    }
    /**
     * Always synchronous in order to simplify handling disconnections.
     * @throws never
     */
    loadContributions(toDisconnect) {
        let loaded = 0;
        const loadPluginsMeasurement = this.measure('loadPlugins');
        const hostContributions = new Map();
        console.log(`[${this.clientId}] Loading plugin contributions`);
        for (const contributions of this.contributions.values()) {
            const plugin = contributions.plugin.metadata;
            const pluginId = plugin.model.id;
            if (contributions.state === PluginContributions.State.INITIALIZING) {
                contributions.state = PluginContributions.State.LOADING;
                contributions.push(core_1.Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`)));
                contributions.push(this.handleContributions(contributions.plugin));
                contributions.state = PluginContributions.State.LOADED;
                console.debug(`[${this.clientId}][${pluginId}]: Loaded contributions.`);
                loaded++;
            }
            if (contributions.state === PluginContributions.State.LOADED) {
                contributions.state = PluginContributions.State.STARTING;
                const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host;
                const dynamicContributions = hostContributions.get(host) || [];
                dynamicContributions.push(contributions);
                hostContributions.set(host, dynamicContributions);
                toDisconnect.push(core_1.Disposable.create(() => {
                    contributions.state = PluginContributions.State.LOADED;
                    console.debug(`[${this.clientId}][${pluginId}]: Disconnected.`);
                }));
            }
        }
        if (loaded > 0) {
            // Only log load measurement if there are were plugins to load.
            loadPluginsMeasurement === null || loadPluginsMeasurement === void 0 ? void 0 : loadPluginsMeasurement.log(`Load contributions of ${this.getPluginCount(loaded)}`);
        }
        else {
            loadPluginsMeasurement.stop();
        }
        return hostContributions;
    }
    async startPlugins(contributionsByHost, toDisconnect) {
        let started = 0;
        const startPluginsMeasurement = this.measure('startPlugins');
        const [hostLogPath, hostStoragePath, hostGlobalStoragePath] = await Promise.all([
            this.pluginPathsService.getHostLogPath(),
            this.getStoragePath(),
            this.getHostGlobalStoragePath()
        ]);
        if (toDisconnect.disposed) {
            return;
        }
        const thenable = [];
        const configStorage = {
            hostLogPath,
            hostStoragePath,
            hostGlobalStoragePath
        };
        for (const [host, hostContributions] of contributionsByHost) {
            // do not start plugins for electron browser
            if (host === 'frontend' && environment_1.environment.electron.is()) {
                continue;
            }
            const manager = await this.obtainManager(host, hostContributions, toDisconnect);
            if (!manager) {
                continue;
            }
            const plugins = hostContributions.map(contributions => contributions.plugin.metadata);
            thenable.push((async () => {
                try {
                    const activationEvents = [...this.activationEvents];
                    await manager.$start({ plugins, configStorage, activationEvents });
                    if (toDisconnect.disposed) {
                        return;
                    }
                    console.log(`[${this.clientId}] Starting plugins.`);
                    for (const contributions of hostContributions) {
                        started++;
                        const plugin = contributions.plugin;
                        const id = plugin.metadata.model.id;
                        contributions.state = PluginContributions.State.STARTED;
                        console.debug(`[${this.clientId}][${id}]: Started plugin.`);
                        toDisconnect.push(contributions.push(core_1.Disposable.create(() => {
                            console.debug(`[${this.clientId}][${id}]: Stopped plugin.`);
                            manager.$stop(id);
                        })));
                        this.handlePluginStarted(manager, plugin);
                    }
                }
                catch (e) {
                    console.error(`Failed to start plugins for '${host}' host`, e);
                }
            })());
        }
        await Promise.all(thenable);
        await this.activateByEvent('onStartupFinished');
        if (toDisconnect.disposed) {
            return;
        }
        if (started > 0) {
            startPluginsMeasurement.log(`Start of ${this.getPluginCount(started)}`);
        }
        else {
            startPluginsMeasurement.stop();
        }
    }
    async activateByEvent(activationEvent) {
        if (this.activationEvents.has(activationEvent)) {
            return;
        }
        this.activationEvents.add(activationEvent);
        await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent)));
    }
    async activatePlugin(id) {
        const activation = [];
        for (const manager of this.managers.values()) {
            activation.push(manager.$activatePlugin(id));
        }
        await Promise.all(activation);
    }
    handlePluginStarted(manager, plugin) {
        // Nothing to do in the abstract
    }
    measure(name) {
        return this.stopwatch.start(name, { context: this.clientId });
    }
    getPluginCount(plugins) {
        return `${plugins} plugin${plugins === 1 ? '' : 's'}`;
    }
};
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(core_1.ILogger),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "logger", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(plugin_protocol_1.HostedPluginServer),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "server", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(core_1.ContributionProvider),
    (0, inversify_1.named)(plugin_ext_api_contribution_1.MainPluginApiProvider),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "mainPluginApiProviders", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(plugin_protocol_1.PluginServer),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "pluginServer", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(plugin_paths_protocol_1.PluginPathsService),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "pluginPathsService", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(env_variables_1.EnvVariablesServer),
    (0, tslib_1.__metadata)("design:type", Object)
], AbstractHostedPluginSupport.prototype, "envServer", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.inject)(common_1.Stopwatch),
    (0, tslib_1.__metadata)("design:type", common_1.Stopwatch)
], AbstractHostedPluginSupport.prototype, "stopwatch", void 0);
(0, tslib_1.__decorate)([
    (0, inversify_1.postConstruct)(),
    (0, tslib_1.__metadata)("design:type", Function),
    (0, tslib_1.__metadata)("design:paramtypes", []),
    (0, tslib_1.__metadata)("design:returntype", void 0)
], AbstractHostedPluginSupport.prototype, "init", null);
AbstractHostedPluginSupport = (0, tslib_1.__decorate)([
    (0, inversify_1.injectable)(),
    (0, tslib_1.__metadata)("design:paramtypes", [String])
], AbstractHostedPluginSupport);
exports.AbstractHostedPluginSupport = AbstractHostedPluginSupport;
class PluginContributions extends core_1.DisposableCollection {
    constructor(plugin) {
        super();
        this.plugin = plugin;
        this.state = PluginContributions.State.INITIALIZING;
    }
}
exports.PluginContributions = PluginContributions;
(function (PluginContributions) {
    let State;
    (function (State) {
        State[State["INITIALIZING"] = 0] = "INITIALIZING";
        State[State["LOADING"] = 1] = "LOADING";
        State[State["LOADED"] = 2] = "LOADED";
        State[State["STARTING"] = 3] = "STARTING";
        State[State["STARTED"] = 4] = "STARTED";
    })(State = PluginContributions.State || (PluginContributions.State = {}));
})(PluginContributions = exports.PluginContributions || (exports.PluginContributions = {}));
//# sourceMappingURL=hosted-plugin.js.map