Skip to content

Commit c00bf9a

Browse files
author
Benjamin Pasero
committed
Validate backup workspaces on startup
1 parent 1df2964 commit c00bf9a

7 files changed

Lines changed: 116 additions & 93 deletions

File tree

src/vs/code/electron-main/lifecycle.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ export class LifecycleService implements ILifecycleMainService {
2727
private oneTimeListenerTokenGenerator: number;
2828
private _wasUpdated: boolean;
2929

30-
private _onBeforeUnload = new Emitter<IVSCodeWindow>();
31-
onBeforeUnload: Event<IVSCodeWindow> = this._onBeforeUnload.event;
32-
3330
private _onBeforeQuit = new Emitter<void>();
3431
onBeforeQuit: Event<void> = this._onBeforeQuit.event;
3532

@@ -133,8 +130,6 @@ export class LifecycleService implements ILifecycleMainService {
133130
const oneTimeCancelEvent = 'vscode:cancel' + oneTimeEventToken;
134131

135132
ipc.once(oneTimeOkEvent, () => {
136-
this._onBeforeUnload.fire(vscodeWindow);
137-
138133
c(false); // no veto
139134
});
140135

src/vs/code/electron-main/windows.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,6 @@ export class WindowsManager implements IWindowsMainService {
357357
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath) {
358358
const workspacesWithBackups = this.backupService.getWorkspaceBackupPaths();
359359
workspacesWithBackups.forEach(workspacePath => {
360-
if (!fs.existsSync(workspacePath)) {
361-
this.backupService.removeWorkspaceBackupPathSync(Uri.file(workspacePath));
362-
return;
363-
}
364-
365360
const configuration = this.toConfiguration(openConfig, workspacePath);
366361
const browserWindow = this.openInBrowserWindow(configuration, true /* new window */);
367362
usedWindows.push(browserWindow);

src/vs/code/test/electron-main/servicesTestUtils.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import Event, { Emitter } from 'vs/base/common/event';
1111
export class TestLifecycleService implements ILifecycleMainService {
1212
public _serviceBrand: any;
1313

14-
private _onBeforeUnload = new Emitter<IVSCodeWindow>();
15-
onBeforeUnload: Event<IVSCodeWindow> = this._onBeforeUnload.event;
16-
1714
private _onBeforeQuit = new Emitter<void>();
1815
onBeforeQuit: Event<void> = this._onBeforeQuit.event;
1916

src/vs/platform/backup/common/backup.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,4 @@ export interface IBackupMainService {
2828
* @param workspaces The workspaces to add.
2929
*/
3030
pushWorkspaceBackupPathsSync(workspaces: Uri[]): void;
31-
32-
/**
33-
* Removes a workspace backup path being tracked for restoration.
34-
*
35-
* @param workspace The workspace to remove.
36-
*/
37-
removeWorkspaceBackupPathSync(workspace: Uri): void;
38-
39-
/**
40-
* Gets whether the workspace has backup(s) associated with it (ie. if the workspace backup
41-
* directory exists).
42-
*
43-
* @param workspace The workspace to evaluate.
44-
* @return Whether the workspace has backups.
45-
*/
46-
hasWorkspaceBackup(workspace: Uri): boolean;
4731
}

src/vs/platform/backup/electron-main/backupMainService.ts

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as crypto from 'crypto';
76
import * as fs from 'fs';
87
import * as path from 'path';
8+
import * as crypto from 'crypto';
9+
import * as extfs from 'vs/base/node/extfs';
910
import Uri from 'vs/base/common/uri';
1011
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
1112
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
12-
import { ILifecycleMainService } from 'vs/platform/lifecycle/common/mainLifecycle';
13-
import { VSCodeWindow } from 'vs/code/electron-main/window';
1413

1514
export class BackupMainService implements IBackupMainService {
1615

@@ -19,86 +18,107 @@ export class BackupMainService implements IBackupMainService {
1918
protected backupHome: string;
2019
protected workspacesJsonPath: string;
2120

22-
private workspacesJsonContent: IBackupWorkspacesFormat;
21+
private backups: IBackupWorkspacesFormat;
2322

2423
constructor(
25-
@IEnvironmentService environmentService: IEnvironmentService,
26-
@ILifecycleMainService lifecycleService: ILifecycleMainService
24+
@IEnvironmentService environmentService: IEnvironmentService
2725
) {
2826
this.backupHome = environmentService.backupHome;
2927
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
3028

31-
lifecycleService.onBeforeUnload(this.onBeforeUnloadWindow.bind(this));
32-
3329
this.loadSync();
3430
}
3531

36-
private onBeforeUnloadWindow(vscodeWindow: VSCodeWindow) {
37-
if (vscodeWindow.openedWorkspacePath) {
38-
// Clear out workspace from workspaces.json if it doesn't have any backups
39-
const workspaceResource = Uri.file(vscodeWindow.openedWorkspacePath);
40-
if (!this.hasWorkspaceBackup(workspaceResource)) {
41-
this.removeWorkspaceBackupPathSync(workspaceResource);
42-
}
43-
}
44-
}
45-
4632
public getWorkspaceBackupPaths(): string[] {
47-
return this.workspacesJsonContent.folderWorkspaces;
33+
return this.backups.folderWorkspaces;
4834
}
4935

5036
public pushWorkspaceBackupPathsSync(workspaces: Uri[]): void {
37+
let needsSaving = false;
5138
workspaces.forEach(workspace => {
52-
// Hot exit is disabled for empty workspaces
53-
if (!workspace) {
54-
return;
55-
}
56-
57-
if (this.workspacesJsonContent.folderWorkspaces.indexOf(workspace.fsPath) === -1) {
58-
this.workspacesJsonContent.folderWorkspaces.push(workspace.fsPath);
39+
if (this.backups.folderWorkspaces.indexOf(workspace.fsPath) === -1) {
40+
this.backups.folderWorkspaces.push(workspace.fsPath);
41+
needsSaving = true;
5942
}
6043
});
61-
this.saveSync();
44+
45+
if (needsSaving) {
46+
this.saveSync();
47+
}
6248
}
6349

64-
public removeWorkspaceBackupPathSync(workspace: Uri): void {
65-
if (!this.workspacesJsonContent.folderWorkspaces) {
50+
protected removeWorkspaceBackupPathSync(workspace: Uri): void {
51+
if (!this.backups.folderWorkspaces) {
6652
return;
6753
}
68-
const index = this.workspacesJsonContent.folderWorkspaces.indexOf(workspace.fsPath);
54+
const index = this.backups.folderWorkspaces.indexOf(workspace.fsPath);
6955
if (index === -1) {
7056
return;
7157
}
72-
this.workspacesJsonContent.folderWorkspaces.splice(index, 1);
58+
this.backups.folderWorkspaces.splice(index, 1);
7359
this.saveSync();
7460
}
7561

76-
public hasWorkspaceBackup(workspace: Uri): boolean {
77-
return fs.existsSync(this.getWorkspaceBackupDirectory(workspace));
78-
}
79-
80-
private getWorkspaceBackupDirectory(workspace: Uri): string {
81-
const workspaceHash = crypto.createHash('md5').update(workspace.fsPath).digest('hex');
82-
return path.join(this.backupHome, workspaceHash);
83-
}
84-
8562
protected loadSync(): void {
63+
let backups: IBackupWorkspacesFormat;
8664
try {
87-
this.workspacesJsonContent = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
65+
backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
8866
} catch (error) {
89-
this.workspacesJsonContent = Object.create(null);
67+
backups = Object.create(null);
9068
}
9169

9270
// Ensure folderWorkspaces is a string[]
93-
if (this.workspacesJsonContent.folderWorkspaces) {
94-
const fws = this.workspacesJsonContent.folderWorkspaces;
71+
if (backups.folderWorkspaces) {
72+
const fws = backups.folderWorkspaces;
9573
if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
96-
this.workspacesJsonContent = Object.create(null);
74+
backups = Object.create(null);
9775
}
9876
}
9977

100-
if (!this.workspacesJsonContent.folderWorkspaces) {
101-
this.workspacesJsonContent.folderWorkspaces = [];
78+
if (!backups.folderWorkspaces) {
79+
backups.folderWorkspaces = [];
80+
}
81+
82+
this.backups = backups;
83+
84+
// Validate backup workspaces
85+
this.validateBackupWorkspaces(backups);
86+
}
87+
88+
private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
89+
const staleBackupWorkspaces: { workspacePath: string; backupPath: string; }[] = [];
90+
91+
const backupWorkspaces = backups.folderWorkspaces;
92+
backupWorkspaces.forEach(workspacePath => {
93+
const backupPath = this.toBackupPath(workspacePath);
94+
if (!this.hasBackupsSync(backupPath)) {
95+
staleBackupWorkspaces.push({ workspacePath, backupPath });
96+
}
97+
});
98+
99+
staleBackupWorkspaces.forEach(staleBackupWorkspace => {
100+
const {backupPath, workspacePath} = staleBackupWorkspace;
101+
extfs.delSync(backupPath);
102+
this.removeWorkspaceBackupPathSync(Uri.file(workspacePath));
103+
});
104+
}
105+
106+
private hasBackupsSync(backupPath): boolean {
107+
try {
108+
const backupSchemas = extfs.readdirSync(backupPath);
109+
if (backupSchemas.length === 0) {
110+
return false; // empty backups
111+
}
112+
113+
return backupSchemas.some(backupSchema => {
114+
try {
115+
return extfs.readdirSync(path.join(backupPath, backupSchema)).length > 0;
116+
} catch (error) {
117+
return false; // invalid folder
118+
}
119+
});
120+
} catch (error) {
121+
return false; // backup path does not exist
102122
}
103123
}
104124

@@ -108,9 +128,15 @@ export class BackupMainService implements IBackupMainService {
108128
if (!fs.existsSync(this.backupHome)) {
109129
fs.mkdirSync(this.backupHome);
110130
}
111-
fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.workspacesJsonContent));
131+
fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups));
112132
} catch (ex) {
113133
console.error('Could not save workspaces.json', ex);
114134
}
115135
}
136+
137+
protected toBackupPath(workspacePath: string): string {
138+
const workspaceHash = crypto.createHash('md5').update(workspacePath).digest('hex');
139+
140+
return path.join(this.backupHome, workspaceHash);
141+
}
116142
}

src/vs/platform/backup/test/electron-main/backupMainService.test.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,38 @@
77

88
import * as assert from 'assert';
99
import * as platform from 'vs/base/common/platform';
10-
import crypto = require('crypto');
1110
import fs = require('fs');
1211
import os = require('os');
1312
import path = require('path');
1413
import extfs = require('vs/base/node/extfs');
1514
import pfs = require('vs/base/node/pfs');
1615
import Uri from 'vs/base/common/uri';
1716
import { TestEnvironmentService } from 'vs/test/utils/servicesTestUtils';
18-
import { TestLifecycleService } from 'vs/code/test/electron-main/servicesTestUtils';
1917
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
2018
import { IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup';
2119

2220
class TestBackupMainService extends BackupMainService {
2321
constructor(backupHome: string, backupWorkspacesPath: string) {
24-
super(TestEnvironmentService, new TestLifecycleService());
22+
super(TestEnvironmentService);
2523

2624
this.backupHome = backupHome;
2725
this.workspacesJsonPath = backupWorkspacesPath;
2826

2927
// Force a reload with the new paths
3028
this.loadSync();
3129
}
30+
31+
public removeWorkspaceBackupPathSync(workspace: Uri): void {
32+
return super.removeWorkspaceBackupPathSync(workspace);
33+
}
34+
35+
public loadSync(): void {
36+
super.loadSync();
37+
}
38+
39+
public toBackupPath(workspacePath: string): string {
40+
return super.toBackupPath(workspacePath);
41+
}
3242
}
3343

3444
suite('BackupMainService', () => {
@@ -39,9 +49,7 @@ suite('BackupMainService', () => {
3949
const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo');
4050
const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar');
4151

42-
const fooWorkspaceBackupDir = path.join(backupHome, crypto.createHash('md5').update(fooFile.fsPath).digest('hex'));
43-
44-
let service: BackupMainService;
52+
let service: TestBackupMainService;
4553

4654
setup(done => {
4755
service = new TestBackupMainService(backupHome, backupWorkspacesPath);
@@ -123,9 +131,33 @@ suite('BackupMainService', () => {
123131
});
124132
});
125133

126-
test('doesWorkspaceHaveBackups should return whether the workspace\'s backup exists', () => {
127-
assert.equal(service.hasWorkspaceBackup(fooFile), false);
128-
fs.mkdirSync(fooWorkspaceBackupDir);
129-
assert.equal(service.hasWorkspaceBackup(fooFile), true);
134+
test('service validates backup workspaces on startup and cleans up', done => {
135+
136+
// 1) backup workspace path does not exist
137+
service.pushWorkspaceBackupPathsSync([fooFile, barFile]);
138+
service.loadSync();
139+
assert.equal(service.getWorkspaceBackupPaths().length, 0);
140+
141+
// 2) backup workspace path exists with empty contents within
142+
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
143+
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
144+
service.pushWorkspaceBackupPathsSync([fooFile, barFile]);
145+
service.loadSync();
146+
assert.equal(service.getWorkspaceBackupPaths().length, 0);
147+
assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath)));
148+
assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath)));
149+
150+
// 3) backup workspace path exists with empty folders within
151+
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
152+
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
153+
fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), 'file'));
154+
fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), 'untitled'));
155+
service.pushWorkspaceBackupPathsSync([fooFile, barFile]);
156+
service.loadSync();
157+
assert.equal(service.getWorkspaceBackupPaths().length, 0);
158+
assert.ok(!fs.exists(service.toBackupPath(fooFile.fsPath)));
159+
assert.ok(!fs.exists(service.toBackupPath(barFile.fsPath)));
160+
161+
done();
130162
});
131163
});

src/vs/platform/lifecycle/common/mainLifecycle.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@ export interface ILifecycleMainService {
1919
*/
2020
wasUpdated: boolean;
2121

22-
/**
23-
* Fired before the window unloads. This can either happen as a matter of closing the
24-
* window or when the window is being reloaded.
25-
*/
26-
onBeforeUnload: Event<IVSCodeWindow>;
27-
2822
/**
2923
* Due to the way we handle lifecycle with eventing, the general app.on('before-quit')
3024
* event cannot be used because it can be called twice on shutdown. Instead the onBeforeQuit

0 commit comments

Comments
 (0)