// ***************************************************************************** // 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 { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import * as path from 'path'; import * as fs from '@theia/core/shared/fs-extra'; import { readdir } from 'fs/promises'; import { remove } from '@theia/core/shared/fs-extra'; import * as crypto from 'crypto'; import { ILogger } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { PluginPaths } from './const'; import { PluginPathsService } from '../../common/plugin-paths-protocol'; import { UntitledWorkspaceService } from '@theia/workspace/lib/common'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginCliContribution } from '../plugin-cli-contribution'; const SESSION_TIMESTAMP_PATTERN = /^\d{8}T\d{6}$/; // Service to provide configuration paths for plugin api. @injectable() export class PluginPathsServiceImpl implements PluginPathsService { @inject(ILogger) protected readonly logger: ILogger; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; @inject(PluginCliContribution) protected readonly cliContribution: PluginCliContribution; @inject(UntitledWorkspaceService) protected readonly untitledWorkspaceService: UntitledWorkspaceService; async getHostLogPath(): Promise { const parentLogsDir = await this.getLogsDirPath(); if (!parentLogsDir) { throw new Error('Unable to get parent log directory'); } const pluginDirPath = path.join(parentLogsDir, this.generateTimeFolderName(), 'host'); await fs.mkdirs(pluginDirPath); // no `await` as We should never wait for the cleanup this.cleanupOldLogs(parentLogsDir); return pluginDirPath; } async getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise { const parentStorageDir = await this.getWorkspaceStorageDirPath(); if (!parentStorageDir) { throw new Error('Unable to get parent storage directory'); } if (!workspaceUri) { return undefined; } await fs.mkdirs(parentStorageDir); const storageDirName = await this.buildWorkspaceId(workspaceUri, rootUris); const storageDirPath = path.join(parentStorageDir, storageDirName); await fs.mkdirs(storageDirPath); return storageDirPath; } protected async buildWorkspaceId(workspaceUri: string, rootUris: string[]): Promise { const configDirUri = await this.envServer.getConfigDirUri(); const untitledWorkspace = await this.untitledWorkspaceService.getUntitledWorkspaceUri( new URI(configDirUri), async uri => !await fs.pathExists(uri.path.fsPath()) ); if (untitledWorkspace.toString() === workspaceUri) { // if workspace is temporary // then let create a storage path for each set of workspace roots const rootsStr = rootUris.sort().join(','); return this.createHash(rootsStr); } else { return this.createHash(workspaceUri); } } /** * Creates a hash digest of the given string. */ protected createHash(str: string): string { try { // md5 is not FIPS-approved but we have to continue use it as there're existing storage folders based on it return crypto.createHash('md5').update(str).digest('hex'); } catch (e) { if (e.message.indexOf('disabled for FIPS') > -1) { // SHA256 is FIPS-compliant return crypto.createHash('sha256').update(str).digest('hex'); } else { throw e; } } // see more details in the issues 8378 } /** * Generate time folder name in format: YYYYMMDDTHHMMSS, for example: 20181205T093828 */ private generateTimeFolderName(): string { const timeStamp = new Date().toISOString().replace(/[-:]|(\..*)/g, ''); // Helps ensure our timestamp generation logic is "valid". // Changes to the timestamp structure may break old logs deletion logic. if (!SESSION_TIMESTAMP_PATTERN.test(timeStamp)) { this.logger.error(`Generated log folder name: "${timeStamp}" does not match expected pattern: ${SESSION_TIMESTAMP_PATTERN}`); } return timeStamp; } private async getLogsDirPath(): Promise { const configDirUri = await this.envServer.getConfigDirUri(); return path.join(FileUri.fsPath(configDirUri), PluginPaths.PLUGINS_LOGS_DIR); } private async getWorkspaceStorageDirPath(): Promise { const configDirUri = await this.envServer.getConfigDirUri(); return path.join(FileUri.fsPath(configDirUri), PluginPaths.PLUGINS_WORKSPACE_STORAGE_DIR); } private async cleanupOldLogs(parentLogsDir: string): Promise { const dirEntries = await readdir(parentLogsDir, { withFileTypes: true }); const subDirEntries = dirEntries.filter(dirent => dirent.isDirectory()); const subDirNames = subDirEntries.map(dirent => dirent.name); // We never clean a folder that is not a Theia logs session folder. // Even if it does appears under the `parentLogsDir`... const sessionSubDirNames = subDirNames.filter((dirName: string) => SESSION_TIMESTAMP_PATTERN.test(dirName)); // [].sort is ascending order and we need descending order (newest first). const sortedSessionSubDirNames = sessionSubDirNames.sort().reverse(); const maxSessionLogsFolders = this.cliContribution.maxSessionLogsFolders(); // [5,4,3,2,1].slice(2) --> [2,1] --> only keep N latest session folders. const oldSessionSubDirNames = sortedSessionSubDirNames.slice(maxSessionLogsFolders); oldSessionSubDirNames.forEach((sessionDir: string) => { const sessionDirPath = path.resolve(parentLogsDir, sessionDir); // we are not waiting for the async `remove` to finish before returning // in order to minimize impact on Theia startup time. remove(sessionDirPath); }); } }