UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

374 lines (373 loc) 13.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { createHash } from 'crypto'; import { isEqual } from '@sussudio/base/common/extpath.mjs'; import { Schemas } from '@sussudio/base/common/network.mjs'; import { join } from '@sussudio/base/common/path.mjs'; import { isLinux } from '@sussudio/base/common/platform.mjs'; import { extUriBiasedIgnorePathCase } from '@sussudio/base/common/resources.mjs'; import { Promises, RimRafMode } from '@sussudio/base/node/pfs.mjs'; import { isEmptyWindowBackupInfo, deserializeWorkspaceInfos, deserializeFolderInfos } from '../node/backup.mjs'; import { IConfigurationService } from '../../configuration/common/configuration.mjs'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.mjs'; import { IStateMainService } from '../../state/electron-main/state.mjs'; import { HotExitConfiguration } from '../../files/common/files.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { isFolderBackupInfo } from '../common/backup.mjs'; import { isWorkspaceIdentifier } from '../../workspace/common/workspace.mjs'; import { createEmptyWorkspaceIdentifier } from '../../workspaces/node/workspaces.mjs'; let BackupMainService = class BackupMainService { environmentMainService; configurationService; logService; stateMainService; static backupWorkspacesMetadataStorageKey = 'backupWorkspaces'; backupHome = this.environmentMainService.backupHome; workspaces = []; folders = []; emptyWindows = []; // Comparers for paths and resources that will // - ignore path casing on Windows/macOS // - respect path casing on Linux backupUriComparer = extUriBiasedIgnorePathCase; backupPathComparer = { isEqual: (pathA, pathB) => isEqual(pathA, pathB, !isLinux) }; constructor(environmentMainService, configurationService, logService, stateMainService) { this.environmentMainService = environmentMainService; this.configurationService = configurationService; this.logService = logService; this.stateMainService = stateMainService; } async initialize() { // read backup workspaces const serializedBackupWorkspaces = this.stateMainService.getItem( BackupMainService.backupWorkspacesMetadataStorageKey, ) ?? { workspaces: [], folders: [], emptyWindows: [] }; // validate empty workspaces backups first this.emptyWindows = await this.validateEmptyWorkspaces(serializedBackupWorkspaces.emptyWindows); // validate workspace backups this.workspaces = await this.validateWorkspaces(deserializeWorkspaceInfos(serializedBackupWorkspaces)); // validate folder backups this.folders = await this.validateFolders(deserializeFolderInfos(serializedBackupWorkspaces)); // store metadata in case some workspaces or folders have been removed this.storeWorkspacesMetadata(); } getWorkspaceBackups() { if (this.isHotExitOnExitAndWindowClose()) { // Only non-folder windows are restored on main process launch when // hot exit is configured as onExitAndWindowClose. return []; } return this.workspaces.slice(0); // return a copy } getFolderBackups() { if (this.isHotExitOnExitAndWindowClose()) { // Only non-folder windows are restored on main process launch when // hot exit is configured as onExitAndWindowClose. return []; } return this.folders.slice(0); // return a copy } isHotExitEnabled() { return this.getHotExitConfig() !== HotExitConfiguration.OFF; } isHotExitOnExitAndWindowClose() { return this.getHotExitConfig() === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE; } getHotExitConfig() { const config = this.configurationService.getValue(); return config?.files?.hotExit || HotExitConfiguration.ON_EXIT; } getEmptyWindowBackups() { return this.emptyWindows.slice(0); // return a copy } registerWorkspaceBackup(workspaceInfo, migrateFrom) { if (!this.workspaces.some((workspace) => workspaceInfo.workspace.id === workspace.workspace.id)) { this.workspaces.push(workspaceInfo); this.storeWorkspacesMetadata(); } const backupPath = join(this.backupHome, workspaceInfo.workspace.id); if (migrateFrom) { return this.moveBackupFolder(backupPath, migrateFrom).then(() => backupPath); } return backupPath; } async moveBackupFolder(backupPath, moveFromPath) { // Target exists: make sure to convert existing backups to empty window backups if (await Promises.exists(backupPath)) { await this.convertToEmptyWindowBackup(backupPath); } // When we have data to migrate from, move it over to the target location if (await Promises.exists(moveFromPath)) { try { await Promises.rename(moveFromPath, backupPath); } catch (error) { this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`); } } } registerFolderBackup(folderInfo) { if (!this.folders.some((folder) => this.backupUriComparer.isEqual(folderInfo.folderUri, folder.folderUri))) { this.folders.push(folderInfo); this.storeWorkspacesMetadata(); } return join(this.backupHome, this.getFolderHash(folderInfo)); } registerEmptyWindowBackup(emptyWindowInfo) { if ( !this.emptyWindows.some( (emptyWindow) => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, emptyWindowInfo.backupFolder), ) ) { this.emptyWindows.push(emptyWindowInfo); this.storeWorkspacesMetadata(); } return join(this.backupHome, emptyWindowInfo.backupFolder); } async validateWorkspaces(rootWorkspaces) { if (!Array.isArray(rootWorkspaces)) { return []; } const seenIds = new Set(); const result = []; // Validate Workspaces for (const workspaceInfo of rootWorkspaces) { const workspace = workspaceInfo.workspace; if (!isWorkspaceIdentifier(workspace)) { return []; // wrong format, skip all entries } if (!seenIds.has(workspace.id)) { seenIds.add(workspace.id); const backupPath = join(this.backupHome, workspace.id); const hasBackups = await this.doHasBackups(backupPath); // If the workspace has no backups, ignore it if (hasBackups) { if (workspace.configPath.scheme !== Schemas.file || (await Promises.exists(workspace.configPath.fsPath))) { result.push(workspaceInfo); } else { // If the workspace has backups, but the target workspace is missing, convert backups to empty ones await this.convertToEmptyWindowBackup(backupPath); } } else { await this.deleteStaleBackup(backupPath); } } } return result; } async validateFolders(folderWorkspaces) { if (!Array.isArray(folderWorkspaces)) { return []; } const result = []; const seenIds = new Set(); for (const folderInfo of folderWorkspaces) { const folderURI = folderInfo.folderUri; const key = this.backupUriComparer.getComparisonKey(folderURI); if (!seenIds.has(key)) { seenIds.add(key); const backupPath = join(this.backupHome, this.getFolderHash(folderInfo)); const hasBackups = await this.doHasBackups(backupPath); // If the folder has no backups, ignore it if (hasBackups) { if (folderURI.scheme !== Schemas.file || (await Promises.exists(folderURI.fsPath))) { result.push(folderInfo); } else { // If the folder has backups, but the target workspace is missing, convert backups to empty ones await this.convertToEmptyWindowBackup(backupPath); } } else { await this.deleteStaleBackup(backupPath); } } } return result; } async validateEmptyWorkspaces(emptyWorkspaces) { if (!Array.isArray(emptyWorkspaces)) { return []; } const result = []; const seenIds = new Set(); // Validate Empty Windows for (const backupInfo of emptyWorkspaces) { const backupFolder = backupInfo.backupFolder; if (typeof backupFolder !== 'string') { return []; } if (!seenIds.has(backupFolder)) { seenIds.add(backupFolder); const backupPath = join(this.backupHome, backupFolder); if (await this.doHasBackups(backupPath)) { result.push(backupInfo); } else { await this.deleteStaleBackup(backupPath); } } } return result; } async deleteStaleBackup(backupPath) { try { await Promises.rm(backupPath, RimRafMode.MOVE); } catch (error) { this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`); } } prepareNewEmptyWindowBackup() { // We are asked to prepare a new empty window backup folder. // Empty windows backup folders are derived from a workspace // identifier, so we generate a new empty workspace identifier // until we found a unique one. let emptyWorkspaceIdentifier = createEmptyWorkspaceIdentifier(); while ( this.emptyWindows.some( (emptyWindow) => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, emptyWorkspaceIdentifier.id), ) ) { emptyWorkspaceIdentifier = createEmptyWorkspaceIdentifier(); } return { backupFolder: emptyWorkspaceIdentifier.id }; } async convertToEmptyWindowBackup(backupPath) { const newEmptyWindowBackupInfo = this.prepareNewEmptyWindowBackup(); // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = join(this.backupHome, newEmptyWindowBackupInfo.backupFolder); try { await Promises.rename(backupPath, newEmptyWindowBackupPath); } catch (error) { this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; } this.emptyWindows.push(newEmptyWindowBackupInfo); return true; } async getDirtyWorkspaces() { const dirtyWorkspaces = []; // Workspaces with backups for (const workspace of this.workspaces) { if (await this.hasBackups(workspace)) { dirtyWorkspaces.push(workspace); } } // Folders with backups for (const folder of this.folders) { if (await this.hasBackups(folder)) { dirtyWorkspaces.push(folder); } } return dirtyWorkspaces; } hasBackups(backupLocation) { let backupPath; // Empty if (isEmptyWindowBackupInfo(backupLocation)) { backupPath = join(this.backupHome, backupLocation.backupFolder); } // Folder else if (isFolderBackupInfo(backupLocation)) { backupPath = join(this.backupHome, this.getFolderHash(backupLocation)); } // Workspace else { backupPath = join(this.backupHome, backupLocation.workspace.id); } return this.doHasBackups(backupPath); } async doHasBackups(backupPath) { try { const backupSchemas = await Promises.readdir(backupPath); for (const backupSchema of backupSchemas) { try { const backupSchemaChildren = await Promises.readdir(join(backupPath, backupSchema)); if (backupSchemaChildren.length > 0) { return true; } } catch (error) { // invalid folder } } } catch (error) { // backup path does not exist } return false; } storeWorkspacesMetadata() { const serializedBackupWorkspaces = { workspaces: this.workspaces.map(({ workspace, remoteAuthority }) => { const serializedWorkspaceBackupInfo = { id: workspace.id, configURIPath: workspace.configPath.toString(), }; if (remoteAuthority) { serializedWorkspaceBackupInfo.remoteAuthority = remoteAuthority; } return serializedWorkspaceBackupInfo; }), folders: this.folders.map(({ folderUri, remoteAuthority }) => { const serializedFolderBackupInfo = { folderUri: folderUri.toString(), }; if (remoteAuthority) { serializedFolderBackupInfo.remoteAuthority = remoteAuthority; } return serializedFolderBackupInfo; }), emptyWindows: this.emptyWindows.map(({ backupFolder, remoteAuthority }) => { const serializedEmptyWindowBackupInfo = { backupFolder, }; if (remoteAuthority) { serializedEmptyWindowBackupInfo.remoteAuthority = remoteAuthority; } return serializedEmptyWindowBackupInfo; }), }; this.stateMainService.setItem(BackupMainService.backupWorkspacesMetadataStorageKey, serializedBackupWorkspaces); } getFolderHash(folder) { const folderUri = folder.folderUri; let key; if (folderUri.scheme === Schemas.file) { key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); // for backward compatibility, use the fspath as key } else { key = folderUri.toString().toLowerCase(); } return createHash('md5').update(key).digest('hex'); } }; BackupMainService = __decorate( [ __param(0, IEnvironmentMainService), __param(1, IConfigurationService), __param(2, ILogService), __param(3, IStateMainService), ], BackupMainService, ); export { BackupMainService };