Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backups - encapsulate empty window workspace id generation #160378

Merged
merged 2 commits into from Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -9,19 +9,20 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { join } from 'vs/base/common/path';
import { Promises } from 'vs/base/node/pfs';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { StorageClient } from 'vs/platform/storage/common/storageIpc';
import { EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace';
import { NON_EMPTY_WORKSPACE_ID_LENGTH } from 'vs/platform/workspaces/node/workspaces';

export class UnusedWorkspaceStorageDataCleaner extends Disposable {

// Workspace/Folder storage names are MD5 hashes (128bits / 4 due to hex presentation)
private static readonly NON_EMPTY_WORKSPACE_ID_LENGTH = 128 / 4;

constructor(
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService,
@INativeHostService private readonly nativeHostService: INativeHostService
@INativeHostService private readonly nativeHostService: INativeHostService,
@IMainProcessService private readonly mainProcessService: IMainProcessService
) {
super();

Expand All @@ -36,9 +37,12 @@ export class UnusedWorkspaceStorageDataCleaner extends Disposable {

try {
const workspaceStorageFolders = await Promises.readdir(this.environmentService.workspaceStorageHome.fsPath);
const storageClient = new StorageClient(this.mainProcessService.getChannel('storage'));

await Promise.all(workspaceStorageFolders.map(async workspaceStorageFolder => {
if (workspaceStorageFolder.length === UnusedWorkspaceStorageDataCleaner.NON_EMPTY_WORKSPACE_ID_LENGTH) {
const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspaceStorageFolder);

if (workspaceStorageFolder.length === NON_EMPTY_WORKSPACE_ID_LENGTH) {
return; // keep workspace storage for folders/workspaces that can be accessed still
}

Expand All @@ -51,9 +55,14 @@ export class UnusedWorkspaceStorageDataCleaner extends Disposable {
return; // keep workspace storage for empty workspaces opened as window
}

const isStorageUsed = await storageClient.isUsed(workspaceStoragePath);
if (isStorageUsed) {
return; // keep workspace storage for empty workspaces that are in use
}

this.logService.trace(`[storage cleanup]: Deleting workspace storage folder ${workspaceStorageFolder} as it seems to be an unused empty workspace.`);

await Promises.rm(join(this.environmentService.workspaceStorageHome.fsPath, workspaceStorageFolder));
await Promises.rm(workspaceStoragePath);
}));
} catch (error) {
onUnexpectedError(error);
Expand Down
12 changes: 3 additions & 9 deletions src/vs/platform/backup/electron-main/backup.ts
Expand Up @@ -3,11 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from 'vs/base/common/uri';
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup';
import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';

export const IBackupMainService = createDecorator<IBackupMainService>('backupMainService');

Expand All @@ -19,13 +17,9 @@ export interface IBackupMainService {

getEmptyWindowBackups(): IEmptyWindowBackupInfo[];

registerWorkspaceBackup(workspace: IWorkspaceBackupInfo, migrateFrom?: string): string;
registerFolderBackup(folderUri: IFolderBackupInfo): string;
registerEmptyWindowBackup(backupFolder?: string, remoteAuthority?: string): string;

unregisterWorkspaceBackup(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackup(folderUri: URI): void;
unregisterEmptyWindowBackup(backupFolder: string): void;
registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string;
registerFolderBackup(folderInfo: IFolderBackupInfo): string;
registerEmptyWindowBackup(emptyWindowInfo: IEmptyWindowBackupInfo): string;

/**
* All folders or workspaces that are known to have
Expand Down
102 changes: 35 additions & 67 deletions src/vs/platform/backup/electron-main/backupMainService.ts
Expand Up @@ -10,7 +10,6 @@ import { Schemas } from 'vs/base/common/network';
import { join } from 'vs/base/common/path';
import { isLinux } from 'vs/base/common/platform';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Promises, RimRafMode } from 'vs/base/node/pfs';
import { TaskSequentializer } from 'vs/base/common/async';
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
Expand All @@ -21,7 +20,8 @@ import { ILifecycleMainService, ShutdownEvent } from 'vs/platform/lifecycle/elec
import { HotExitConfiguration, IFilesConfiguration } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IFolderBackupInfo, isFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { createEmptyWorkspaceIdentifier } from 'vs/platform/workspaces/node/workspaces';

export class BackupMainService implements IBackupMainService {

Expand Down Expand Up @@ -137,7 +137,7 @@ export class BackupMainService implements IBackupMainService {
this.writeWorkspacesMetadata();
}

const backupPath = this.getBackupPath(workspaceInfo.workspace.id);
const backupPath = join(this.backupHome, workspaceInfo.workspace.id);

if (migrateFrom) {
this.moveBackupFolderSync(backupPath, migrateFrom);
Expand All @@ -163,54 +163,22 @@ export class BackupMainService implements IBackupMainService {
}
}

unregisterWorkspaceBackup(workspace: IWorkspaceIdentifier): void {
const id = workspace.id;
const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id);
if (index !== -1) {
this.workspaces.splice(index, 1);
this.writeWorkspacesMetadata();
}
}

registerFolderBackup(folderInfo: IFolderBackupInfo): string {
if (!this.folders.some(folder => this.backupUriComparer.isEqual(folderInfo.folderUri, folder.folderUri))) {
this.folders.push(folderInfo);
this.writeWorkspacesMetadata();
}

return this.getBackupPath(this.getFolderHash(folderInfo));
}

unregisterFolderBackup(folderUri: URI): void {
const index = this.folders.findIndex(folder => this.backupUriComparer.isEqual(folderUri, folder.folderUri));
if (index !== -1) {
this.folders.splice(index, 1);
this.writeWorkspacesMetadata();
}
return join(this.backupHome, this.getFolderHash(folderInfo));
}

registerEmptyWindowBackup(backupFolderCandidate?: string, remoteAuthority?: string): string {

// Generate a new folder if this is a new empty workspace
const backupFolder = backupFolderCandidate || this.getRandomEmptyWindowId();
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, backupFolder))) {
this.emptyWindows.push({ backupFolder, remoteAuthority });
registerEmptyWindowBackup(emptyWindowInfo: IEmptyWindowBackupInfo): string {
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, emptyWindowInfo.backupFolder))) {
this.emptyWindows.push(emptyWindowInfo);
this.writeWorkspacesMetadata();
}

return this.getBackupPath(backupFolder);
}

unregisterEmptyWindowBackup(backupFolder: string): void {
const index = this.emptyWindows.findIndex(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, backupFolder));
if (index !== -1) {
this.emptyWindows.splice(index, 1);
this.writeWorkspacesMetadata();
}
}

private getBackupPath(oldFolderHash: string): string {
return join(this.backupHome, oldFolderHash);
return join(this.backupHome, emptyWindowInfo.backupFolder);
}

private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise<IWorkspaceBackupInfo[]> {
Expand All @@ -231,7 +199,7 @@ export class BackupMainService implements IBackupMainService {
if (!seenIds.has(workspace.id)) {
seenIds.add(workspace.id);

const backupPath = this.getBackupPath(workspace.id);
const backupPath = join(this.backupHome, workspace.id);
const hasBackups = await this.doHasBackups(backupPath);

// If the workspace has no backups, ignore it
Expand Down Expand Up @@ -264,7 +232,7 @@ export class BackupMainService implements IBackupMainService {
if (!seenIds.has(key)) {
seenIds.add(key);

const backupPath = this.getBackupPath(this.getFolderHash(folderInfo));
const backupPath = join(this.backupHome, this.getFolderHash(folderInfo));
const hasBackups = await this.doHasBackups(backupPath);

// If the folder has no backups, ignore it
Expand Down Expand Up @@ -302,7 +270,7 @@ export class BackupMainService implements IBackupMainService {
if (!seenIds.has(backupFolder)) {
seenIds.add(backupFolder);

const backupPath = this.getBackupPath(backupFolder);
const backupPath = join(this.backupHome, backupFolder);
if (await this.doHasBackups(backupPath)) {
result.push(backupInfo);
} else {
Expand All @@ -322,44 +290,49 @@ export class BackupMainService implements IBackupMainService {
}
}

private async convertToEmptyWindowBackup(backupPath: string): Promise<boolean> {
private prepareNewEmptyWindowBackup(): IEmptyWindowBackupInfo {

// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, newBackupFolder))) {
newBackupFolder = this.getRandomEmptyWindowId();
// 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 };
}

private async convertToEmptyWindowBackup(backupPath: string): Promise<boolean> {
const newEmptyWindowBackupInfo = this.prepareNewEmptyWindowBackup();

// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
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({ backupFolder: newBackupFolder });
this.emptyWindows.push(newEmptyWindowBackupInfo);

return true;
}

private convertToEmptyWindowBackupSync(backupPath: string): boolean {

// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, newBackupFolder))) {
newBackupFolder = this.getRandomEmptyWindowId();
}
const newEmptyWindowBackupInfo = this.prepareNewEmptyWindowBackup();

// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
const newEmptyWindowBackupPath = join(this.backupHome, newEmptyWindowBackupInfo.backupFolder);
try {
fs.renameSync(backupPath, newEmptyWindowBackupPath);
} catch (error) {
this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`);
return false;
}
this.emptyWindows.push({ backupFolder: newBackupFolder });
this.emptyWindows.push(newEmptyWindowBackupInfo);

return true;
}
Expand Down Expand Up @@ -394,12 +367,12 @@ export class BackupMainService implements IBackupMainService {

// Folder
else if (isFolderBackupInfo(backupLocation)) {
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
backupPath = join(this.backupHome, this.getFolderHash(backupLocation));
}

// Workspace
else {
backupPath = this.getBackupPath(backupLocation.workspace.id);
backupPath = join(this.backupHome, backupLocation.workspace.id);
}

return this.doHasBackups(backupPath);
Expand Down Expand Up @@ -482,17 +455,12 @@ export class BackupMainService implements IBackupMainService {
};
}

private getRandomEmptyWindowId(): string {
return (Date.now() + Math.round(Math.random() * 1000)).toString();
}

protected getFolderHash(folder: IFolderBackupInfo): string {
const folderUri = folder.folderUri;
let key: string;

let key: string;
if (folderUri.scheme === Schemas.file) {
// for backward compatibility, use the fspath as key
key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); // for backward compatibility, use the fspath as key
} else {
key = folderUri.toString().toLowerCase();
}
Expand Down
Expand Up @@ -568,63 +568,6 @@ flakySuite('BackupMainService', () => {
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});

suite('removeBackupPathSync', () => {
test('should remove folder workspaces from workspaces.json (folder workspace)', async () => {
service.registerFolderBackup(toFolderBackupInfo(fooFile));
service.registerFolderBackup(toFolderBackupInfo(barFile));
service.unregisterFolderBackup(fooFile);

const json = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: barFile.toString() }]);
service.unregisterFolderBackup(barFile);

const json2 = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json2.folderWorkspaceInfos, []);
});

test('should remove folder workspaces from workspaces.json (root workspace)', async () => {
const ws1 = toWorkspaceBackupInfo(fooFile.fsPath);
service.registerWorkspaceBackup(ws1);
const ws2 = toWorkspaceBackupInfo(barFile.fsPath);
service.registerWorkspaceBackup(ws2);
service.unregisterWorkspaceBackup(ws1.workspace);

const json = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackup(ws2.workspace);

const json2 = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json2.rootURIWorkspaces, []);
});

test('should remove empty workspaces from workspaces.json', async () => {
service.registerEmptyWindowBackup('foo');
service.registerEmptyWindowBackup('bar');
service.unregisterEmptyWindowBackup('foo');

const json = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
service.unregisterEmptyWindowBackup('bar');

const json2 = await readWorkspacesMetadata(backupWorkspacesPath);
assert.deepStrictEqual(json2.emptyWorkspaceInfos, []);
});

test('should fail gracefully when removing a path that doesn\'t exist', async () => {

await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync

const workspacesJson: ISerializedBackupWorkspaces = { rootURIWorkspaces: [], folderWorkspaceInfos: [{ folderUri: existingTestFolder1.toString() }], emptyWorkspaceInfos: [] };
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
service.unregisterFolderBackup(barFile);
service.unregisterEmptyWindowBackup('test');
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<ISerializedBackupWorkspaces>JSON.parse(content));
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: existingTestFolder1.toString() }]);
});
});

suite('getWorkspaceHash', () => {
(platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => {
const assertFolderHash = (uri1: URI, uri2: URI) => {
Expand Down Expand Up @@ -663,24 +606,6 @@ flakySuite('BackupMainService', () => {
assert.strictEqual(service.getWorkspaceBackups().length, 1);
}
});

test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => {

// same case
service.registerFolderBackup(toFolderBackupInfo(fooFile));
service.unregisterFolderBackup(fooFile);
assert.strictEqual(service.getFolderBackups().length, 0);

// mixed case
service.registerFolderBackup(toFolderBackupInfo(fooFile));
service.unregisterFolderBackup(URI.file(fooFile.fsPath.toUpperCase()));

if (platform.isLinux) {
assert.strictEqual(service.getFolderBackups().length, 1);
} else {
assert.strictEqual(service.getFolderBackups().length, 0);
}
});
});

suite('getDirtyWorkspaces', () => {
Expand Down