backupTracker.ts 5.5 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
8
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
9
import { ILogService } from 'vs/platform/log/common/log';
10
import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
11

12
export abstract class BackupTracker extends Disposable {
13

14 15 16 17 18 19
	// A map from working copy to a version ID we compute on each content
	// change. This version ID allows to e.g. ask if a backup for a specific
	// content has been made before closing.
	private readonly mapWorkingCopyToContentVersion = new Map<IWorkingCopy, number>();

	// A map of scheduled pending backups for working copies
20 21 22
	private readonly pendingBackups = new Map<IWorkingCopy, IDisposable>();

	constructor(
23 24
		protected readonly backupFileService: IBackupFileService,
		protected readonly workingCopyService: IWorkingCopyService,
25
		protected readonly logService: ILogService,
26
		protected readonly lifecycleService: ILifecycleService
27 28 29
	) {
		super();

30 31 32
		// Fill in initial dirty working copies
		this.workingCopyService.dirtyWorkingCopies.forEach(workingCopy => this.onDidRegister(workingCopy));

33 34 35 36 37 38
		this.registerListeners();
	}

	private registerListeners() {

		// Working Copy events
39 40 41 42
		this._register(this.workingCopyService.onDidRegister(workingCopy => this.onDidRegister(workingCopy)));
		this._register(this.workingCopyService.onDidUnregister(workingCopy => this.onDidUnregister(workingCopy)));
		this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.onDidChangeDirty(workingCopy)));
		this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy)));
43

44 45
		// Lifecycle (handled in subclasses)
		this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
46 47 48
	}

	private onDidRegister(workingCopy: IWorkingCopy): void {
49 50 51
		if (workingCopy.isDirty()) {
			this.scheduleBackup(workingCopy);
		}
52 53 54
	}

	private onDidUnregister(workingCopy: IWorkingCopy): void {
55 56 57 58 59

		// Remove from content version map
		this.mapWorkingCopyToContentVersion.delete(workingCopy);

		// Discard backup
60 61 62 63
		this.discardBackup(workingCopy);
	}

	private onDidChangeDirty(workingCopy: IWorkingCopy): void {
64 65 66
		if (workingCopy.isDirty()) {
			this.scheduleBackup(workingCopy);
		} else {
67 68 69 70 71
			this.discardBackup(workingCopy);
		}
	}

	private onDidChangeContent(workingCopy: IWorkingCopy): void {
72 73 74 75 76 77

		// Increment content version ID
		const contentVersionId = this.getContentVersion(workingCopy);
		this.mapWorkingCopyToContentVersion.set(workingCopy, contentVersionId + 1);

		// Schedule backup if dirty
78
		if (workingCopy.isDirty()) {
79 80 81
			// this listener will make sure that the backup is
			// pushed out for as long as the user is still changing
			// the content of the working copy.
82 83 84 85
			this.scheduleBackup(workingCopy);
		}
	}

86 87 88 89 90 91 92 93 94 95 96
	/**
	 * Allows subclasses to conditionally opt-out of doing a backup, e.g. if
	 * auto save is enabled.
	 */
	protected abstract shouldScheduleBackup(workingCopy: IWorkingCopy): boolean;

	/**
	 * Allows subclasses to control the delay before performing a backup from
	 * working copy content changes.
	 */
	protected abstract getBackupScheduleDelay(workingCopy: IWorkingCopy): number;
97 98

	private scheduleBackup(workingCopy: IWorkingCopy): void {
99 100
		if (!this.shouldScheduleBackup(workingCopy)) {
			return; // subclass prevented backup for working copy
101 102 103 104 105 106 107 108 109
		}

		// Clear any running backup operation
		dispose(this.pendingBackups.get(workingCopy));
		this.pendingBackups.delete(workingCopy);

		this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString());

		// Schedule new backup
110
		const handle = setTimeout(async () => {
111 112 113 114 115

			// Clear disposable
			this.pendingBackups.delete(workingCopy);

			// Backup if dirty
116
			if (workingCopy.isDirty()) {
117 118
				this.logService.trace(`[backup tracker] running backup`, workingCopy.resource.toString());

119 120 121
				try {
					const backup = await workingCopy.backup();
					await this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta);
S
Sandeep Somavarapu 已提交
122 123 124
				} catch (error) {
					this.logService.error(error);
				}
125
			}
126
		}, this.getBackupScheduleDelay(workingCopy));
127 128 129 130 131 132 133 134 135

		// Keep in map for disposal as needed
		this.pendingBackups.set(workingCopy, toDisposable(() => {
			this.logService.trace(`[backup tracker] clearing pending backup`, workingCopy.resource.toString());

			clearTimeout(handle);
		}));
	}

136 137 138 139
	protected getContentVersion(workingCopy: IWorkingCopy): number {
		return this.mapWorkingCopyToContentVersion.get(workingCopy) || 0;
	}

140 141 142 143 144 145 146 147
	private discardBackup(workingCopy: IWorkingCopy): void {
		this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString());

		// Clear any running backup operation
		dispose(this.pendingBackups.get(workingCopy));
		this.pendingBackups.delete(workingCopy);

		// Forward to backup file service
B
Benjamin Pasero 已提交
148
		this.backupFileService.discardBackup(workingCopy.resource);
149
	}
150 151

	protected abstract onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean>;
152
}