// ***************************************************************************** // 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 // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; import * as cp from 'child_process'; import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; import { FileUri } from '@theia/core/lib/node'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin'; import { LogType } from '@theia/plugin-ext/lib/common/types'; import { ProcessUtils } from '@theia/core/lib/node/process-utils'; export const HostedPluginsManager = Symbol('HostedPluginsManager'); export interface HostedPluginsManager { /** * Runs watcher script to recompile plugin on any changes along given path. * * @param uri uri to plugin root folder. */ runWatchCompilation(uri: string): Promise; /** * Stops watcher script. * * @param uri uri to plugin root folder. */ stopWatchCompilation(uri: string): Promise; /** * Checks if watcher script to recompile plugin is running. * * @param uri uri to plugin root folder. */ isWatchCompilationRunning(uri: string): Promise; } @injectable() export class HostedPluginsManagerImpl implements HostedPluginsManager { @inject(HostedPluginSupport) protected readonly hostedPluginSupport: HostedPluginSupport; @inject(ProcessUtils) protected readonly processUtils: ProcessUtils; protected watchCompilationRegistry: Map; constructor() { this.watchCompilationRegistry = new Map(); } runWatchCompilation(uri: string): Promise { const pluginRootPath = FileUri.fsPath(uri); if (this.watchCompilationRegistry.has(pluginRootPath)) { throw new Error('Watcher is already running in ' + pluginRootPath); } if (!this.checkWatchScript(pluginRootPath)) { this.hostedPluginSupport.sendLog({ data: 'Plugin in ' + uri + ' doesn\'t have watch script', type: LogType.Error }); throw new Error('Watch script doesn\'t exist in ' + pluginRootPath + 'package.json'); } return this.runWatchScript(pluginRootPath); } private killProcessTree(parentPid: number): void { this.processUtils.terminateProcessTree(parentPid); } stopWatchCompilation(uri: string): Promise { const pluginPath = FileUri.fsPath(uri); const watchProcess = this.watchCompilationRegistry.get(pluginPath); if (!watchProcess) { throw new Error('Watcher is not running in ' + pluginPath); } this.killProcessTree(watchProcess.pid!); return Promise.resolve(); } isWatchCompilationRunning(uri: string): Promise { const pluginPath = FileUri.fsPath(uri); return new Promise(resolve => resolve(this.watchCompilationRegistry.has(pluginPath))); } protected runWatchScript(pluginRootPath: string): Promise { const watchProcess = cp.spawn('npm', ['run', 'watch'], { cwd: pluginRootPath, shell: true }); watchProcess.on('exit', () => this.unregisterWatchScript(pluginRootPath)); this.watchCompilationRegistry.set(pluginRootPath, watchProcess); this.hostedPluginSupport.sendLog({ data: 'Compilation watcher has been started in ' + pluginRootPath, type: LogType.Info }); return Promise.resolve(); } protected unregisterWatchScript(pluginRootPath: string): void { this.watchCompilationRegistry.delete(pluginRootPath); this.hostedPluginSupport.sendLog({ data: 'Compilation watcher has been stopped in ' + pluginRootPath, type: LogType.Info }); } /** * Checks whether watch script is present into package.json by given parent folder. * * @param pluginPath path to plugin's root directory */ protected async checkWatchScript(pluginPath: string): Promise { const pluginPackageJsonPath = path.join(pluginPath, 'package.json'); try { const packageJson = await fs.readJSON(pluginPackageJsonPath); const scripts = packageJson['scripts']; if (scripts && scripts['watch']) { return true; } } catch { } return false; } }