Merge VS Code 1.31.1 (#4283)

This commit is contained in:
Matt Irvine
2019-03-15 13:09:45 -07:00
committed by GitHub
parent 7d31575149
commit 86bac90001
1716 changed files with 53308 additions and 48375 deletions

View File

@@ -7,43 +7,90 @@ import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as platform from 'vs/base/common/platform';
import * as extfs from 'vs/base/node/extfs';
import { writeFileAndFlushSync } from 'vs/base/node/extfs';
import * as arrays from 'vs/base/common/arrays';
import { IBackupMainService, IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/common/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/paths';
import { Schemas } from 'vs/base/common/network';
import { writeFile, readFile, readdir, exists, del, rename } from 'vs/base/node/pfs';
export class BackupMainService implements IBackupMainService {
public _serviceBrand: any;
_serviceBrand: any;
protected backupHome: string;
protected workspacesJsonPath: string;
protected rootWorkspaces: IWorkspaceIdentifier[];
protected folderWorkspaces: URI[];
protected emptyWorkspaces: IEmptyWindowBackupInfo[];
private rootWorkspaces: IWorkspaceIdentifier[];
private folderWorkspaces: URI[];
private emptyWorkspaces: IEmptyWindowBackupInfo[];
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService private configurationService: IConfigurationService,
@ILogService private logService: ILogService
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
this.loadSync();
}
public getWorkspaceBackups(): IWorkspaceIdentifier[] {
async initialize(): Promise<void> {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(await readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
} catch (error) {
backups = Object.create(null);
}
// read empty workspaces backups first
if (backups.emptyWorkspaceInfos) {
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
} else if (Array.isArray(backups.emptyWorkspaces)) {
// read legacy entries
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(backupFolder => ({ backupFolder })));
} else {
this.emptyWorkspaces = [];
}
// read workspace backups
this.rootWorkspaces = await this.validateWorkspaces(backups.rootWorkspaces);
// read folder backups
let workspaceFolders: URI[] = [];
try {
if (Array.isArray(backups.folderURIWorkspaces)) {
workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f));
} else if (Array.isArray(backups.folderWorkspaces)) {
// migrate legacy folder paths
workspaceFolders = [];
for (const folderPath of backups.folderWorkspaces) {
const oldFolderHash = this.getLegacyFolderHash(folderPath);
const folderUri = URI.file(folderPath);
const newFolderHash = this.getFolderHash(folderUri);
if (newFolderHash !== oldFolderHash) {
await this.moveBackupFolder(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash));
}
workspaceFolders.push(folderUri);
}
}
} catch (e) {
// ignore URI parsing exceptions
}
this.folderWorkspaces = await this.validateFolders(workspaceFolders);
// save again in case some workspaces or folders have been removed
await this.save();
}
getWorkspaceBackups(): IWorkspaceIdentifier[] {
if (this.isHotExitOnExitAndWindowClose()) {
// Only non-folder windows are restored on main process launch when
// hot exit is configured as onExitAndWindowClose.
@@ -53,16 +100,17 @@ export class BackupMainService implements IBackupMainService {
return this.rootWorkspaces.slice(0); // return a copy
}
public getFolderBackupPaths(): URI[] {
getFolderBackupPaths(): URI[] {
if (this.isHotExitOnExitAndWindowClose()) {
// Only non-folder windows are restored on main process launch when
// hot exit is configured as onExitAndWindowClose.
return [];
}
return this.folderWorkspaces.slice(0); // return a copy
}
public isHotExitEnabled(): boolean {
isHotExitEnabled(): boolean {
return this.getHotExitConfig() !== HotExitConfiguration.OFF;
}
@@ -76,11 +124,11 @@ export class BackupMainService implements IBackupMainService {
return (config && config.files && config.files.hotExit) || HotExitConfiguration.ON_EXIT;
}
public getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
return this.emptyWorkspaces.slice(0); // return a copy
}
public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string {
registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string {
if (!this.rootWorkspaces.some(w => w.id === workspace.id)) {
this.rootWorkspaces.push(workspace);
this.saveSync();
@@ -99,7 +147,7 @@ export class BackupMainService implements IBackupMainService {
// Target exists: make sure to convert existing backups to empty window backups
if (fs.existsSync(backupPath)) {
this.convertToEmptyWindowBackup(backupPath);
this.convertToEmptyWindowBackupSync(backupPath);
}
// When we have data to migrate from, move it over to the target location
@@ -112,7 +160,24 @@ export class BackupMainService implements IBackupMainService {
}
}
public unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
private async moveBackupFolder(backupPath: string, moveFromPath: string): Promise<void> {
// Target exists: make sure to convert existing backups to empty window backups
if (await exists(backupPath)) {
await this.convertToEmptyWindowBackup(backupPath);
}
// When we have data to migrate from, move it over to the target location
if (await exists(moveFromPath)) {
try {
await rename(moveFromPath, backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
}
}
}
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
let index = arrays.firstIndex(this.rootWorkspaces, w => w.id === workspace.id);
if (index !== -1) {
this.rootWorkspaces.splice(index, 1);
@@ -120,15 +185,16 @@ export class BackupMainService implements IBackupMainService {
}
}
public registerFolderBackupSync(folderUri: URI): string {
registerFolderBackupSync(folderUri: URI): string {
if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri))) {
this.folderWorkspaces.push(folderUri);
this.saveSync();
}
return this.getBackupPath(this.getFolderHash(folderUri));
}
public unregisterFolderBackupSync(folderUri: URI): void {
unregisterFolderBackupSync(folderUri: URI): void {
let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri));
if (index !== -1) {
this.folderWorkspaces.splice(index, 1);
@@ -136,22 +202,24 @@ export class BackupMainService implements IBackupMainService {
}
}
public registerEmptyWindowBackupSync(backupInfo: IEmptyWindowBackupInfo): string {
registerEmptyWindowBackupSync(backupInfo: IEmptyWindowBackupInfo): string {
let backupFolder = backupInfo.backupFolder;
let remoteAuthority = backupInfo.remoteAuthority;
// Generate a new folder if this is a new empty workspace
if (!backupFolder) {
backupFolder = this.getRandomEmptyWindowId();
}
if (!this.emptyWorkspaces.some(w => isEqual(w.backupFolder, backupFolder, !platform.isLinux))) {
this.emptyWorkspaces.push({ backupFolder, remoteAuthority });
this.saveSync();
}
return this.getBackupPath(backupFolder);
}
public unregisterEmptyWindowBackupSync(backupFolder: string): void {
unregisterEmptyWindowBackupSync(backupFolder: string): void {
let index = arrays.firstIndex(this.emptyWorkspaces, w => isEqual(w.backupFolder, backupFolder, !platform.isLinux));
if (index !== -1) {
this.emptyWorkspaces.splice(index, 1);
@@ -159,61 +227,11 @@ export class BackupMainService implements IBackupMainService {
}
}
protected loadSync(): void {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
} catch (error) {
backups = Object.create(null);
}
// read empty workspaces backups first
if (backups.emptyWorkspaceInfos) {
this.emptyWorkspaces = this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
} else if (Array.isArray(backups.emptyWorkspaces)) {
// read legacy entries
this.emptyWorkspaces = this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(backupFolder => ({ backupFolder })));
} else {
this.emptyWorkspaces = [];
}
// read workspace backups
this.rootWorkspaces = this.validateWorkspaces(backups.rootWorkspaces);
// read folder backups
let workspaceFolders: URI[] = [];
try {
if (Array.isArray(backups.folderURIWorkspaces)) {
workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f));
} else if (Array.isArray(backups.folderWorkspaces)) {
// migrate legacy folder paths
workspaceFolders = [];
for (const folderPath of backups.folderWorkspaces) {
const oldFolderHash = this.getLegacyFolderHash(folderPath);
const folderUri = URI.file(folderPath);
const newFolderHash = this.getFolderHash(folderUri);
if (newFolderHash !== oldFolderHash) {
this.moveBackupFolderSync(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash));
}
workspaceFolders.push(folderUri);
}
}
} catch (e) {
// ignore URI parsing exceptions
}
this.folderWorkspaces = this.validateFolders(workspaceFolders);
// save again in case some workspaces or folders have been removed
this.saveSync();
}
private getBackupPath(oldFolderHash: string): string {
return path.join(this.backupHome, oldFolderHash);
}
private validateWorkspaces(rootWorkspaces: IWorkspaceIdentifier[]): IWorkspaceIdentifier[] {
private async validateWorkspaces(rootWorkspaces: IWorkspaceIdentifier[]): Promise<IWorkspaceIdentifier[]> {
if (!Array.isArray(rootWorkspaces)) {
return [];
}
@@ -231,57 +249,58 @@ export class BackupMainService implements IBackupMainService {
seenIds[workspace.id] = true;
const backupPath = this.getBackupPath(workspace.id);
const hasBackups = this.hasBackupsSync(backupPath);
const hasBackups = await this.hasBackups(backupPath);
// If the workspace has no backups, ignore it
if (hasBackups) {
if (fs.existsSync(workspace.configPath)) {
if (await exists(workspace.configPath)) {
result.push(workspace);
} else {
// If the workspace has backups, but the target workspace is missing, convert backups to empty ones
this.convertToEmptyWindowBackup(backupPath);
await this.convertToEmptyWindowBackup(backupPath);
}
} else {
this.deleteStaleBackup(backupPath);
await this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private validateFolders(folderWorkspaces: URI[]): URI[] {
private async validateFolders(folderWorkspaces: URI[]): Promise<URI[]> {
if (!Array.isArray(folderWorkspaces)) {
return [];
}
const result: URI[] = [];
const seen: { [id: string]: boolean } = Object.create(null);
for (let folderURI of folderWorkspaces) {
const key = getComparisonKey(folderURI);
if (!seen[key]) {
seen[key] = true;
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
const hasBackups = this.hasBackupsSync(backupPath);
const hasBackups = await this.hasBackups(backupPath);
// If the folder has no backups, ignore it
if (hasBackups) {
if (folderURI.scheme !== Schemas.file || fs.existsSync(folderURI.fsPath)) {
if (folderURI.scheme !== Schemas.file || await exists(folderURI.fsPath)) {
result.push(folderURI);
} else {
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
this.convertToEmptyWindowBackup(backupPath);
await this.convertToEmptyWindowBackup(backupPath);
}
} else {
this.deleteStaleBackup(backupPath);
await this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private validateEmptyWorkspaces(emptyWorkspaces: IEmptyWindowBackupInfo[]): IEmptyWindowBackupInfo[] {
private async validateEmptyWorkspaces(emptyWorkspaces: IEmptyWindowBackupInfo[]): Promise<IEmptyWindowBackupInfo[]> {
if (!Array.isArray(emptyWorkspaces)) {
return [];
}
@@ -300,10 +319,10 @@ export class BackupMainService implements IBackupMainService {
seen[backupFolder] = true;
const backupPath = this.getBackupPath(backupFolder);
if (this.hasBackupsSync(backupPath)) {
if (await this.hasBackups(backupPath)) {
result.push(backupInfo);
} else {
this.deleteStaleBackup(backupPath);
await this.deleteStaleBackup(backupPath);
}
}
}
@@ -311,17 +330,38 @@ export class BackupMainService implements IBackupMainService {
return result;
}
private deleteStaleBackup(backupPath: string) {
private async deleteStaleBackup(backupPath: string): Promise<void> {
try {
if (fs.existsSync(backupPath)) {
extfs.delSync(backupPath);
if (await exists(backupPath)) {
await del(backupPath);
}
} catch (ex) {
this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
}
}
private convertToEmptyWindowBackup(backupPath: string): boolean {
private async convertToEmptyWindowBackup(backupPath: string): Promise<boolean> {
// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWorkspaces.some(w => isEqual(w.backupFolder, newBackupFolder, platform.isLinux))) {
newBackupFolder = this.getRandomEmptyWindowId();
}
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
await rename(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
return false;
}
this.emptyWorkspaces.push({ backupFolder: newBackupFolder });
return true;
}
private convertToEmptyWindowBackupSync(backupPath: string): boolean {
// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
@@ -342,56 +382,66 @@ export class BackupMainService implements IBackupMainService {
return true;
}
private hasBackupsSync(backupPath: string): boolean {
private async hasBackups(backupPath: string): Promise<boolean> {
try {
const backupSchemas = extfs.readdirSync(backupPath);
if (backupSchemas.length === 0) {
return false; // empty backups
}
const backupSchemas = await readdir(backupPath);
return backupSchemas.some(backupSchema => {
for (const backupSchema of backupSchemas) {
try {
return extfs.readdirSync(path.join(backupPath, backupSchema)).length > 0;
const backupSchemaChildren = await readdir(path.join(backupPath, backupSchema));
if (backupSchemaChildren.length > 0) {
return true;
}
} catch (error) {
return false; // invalid folder
// invalid folder
}
});
}
} catch (error) {
return false; // backup path does not exist
// backup path does not exist
}
return false;
}
private saveSync(): void {
try {
// The user data directory must exist so only the Backup directory needs to be checked.
if (!fs.existsSync(this.backupHome)) {
fs.mkdirSync(this.backupHome);
}
const backups: IBackupWorkspacesFormat = {
rootWorkspaces: this.rootWorkspaces,
folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()),
emptyWorkspaceInfos: this.emptyWorkspaces,
emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder)
};
extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(backups));
writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
}
}
private async save(): Promise<void> {
try {
await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
}
}
private serializeBackups(): IBackupWorkspacesFormat {
return {
rootWorkspaces: this.rootWorkspaces,
folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()),
emptyWorkspaceInfos: this.emptyWorkspaces,
emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder)
} as IBackupWorkspacesFormat;
}
private getRandomEmptyWindowId(): string {
return (Date.now() + Math.round(Math.random() * 1000)).toString();
}
protected getFolderHash(folderUri: URI): string {
let key;
let key: string;
if (folderUri.scheme === Schemas.file) {
// for backward compatibility, use the fspath as key
key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
} else {
key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString();
}
return crypto.createHash('md5').update(key).digest('hex');
}