@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
374 lines (373 loc) • 13.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 };