Skip to content

Change hot exit to create new files for each backup #12377

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

Merged
merged 3 commits into from
Jun 16, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/client/datascience/interactive-ipynb/nativeEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
context: CustomDocumentOpenContext, // This has info about backups. right now we use our own data.
_cancellation: CancellationToken
): Promise<CustomDocument> {
const model = await this.loadModel(uri, undefined, context.backupId ? false : true);
const model = await this.loadModel(uri, undefined, context.backupId);
return {
uri,
dispose: () => model.dispose()
Expand All @@ -116,11 +116,11 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
cancellation: CancellationToken
): Promise<CustomDocumentBackup> {
const model = await this.loadModel(document.uri);
const id = this.storage.getBackupId(model);
this.storage.backup(model, cancellation).ignoreErrors();
const id = this.storage.generateBackupId(model);
await this.storage.backup(model, cancellation, id);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to wait here

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think awaiting here screwed up the custom editor. Can you try it an make sure it still works?

return {
id,
delete: () => this.storage.deleteBackup(model).ignoreErrors() // This cleans up after save has happened.
delete: () => this.storage.deleteBackup(model, id).ignoreErrors() // This cleans up after save has happened.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the back associated with this id, hence passing the id.

};
}

Expand Down Expand Up @@ -175,12 +175,16 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
return this.open(uri);
}

public async loadModel(file: Uri, contents?: string, skipDirtyContents?: boolean) {
public async loadModel(file: Uri, contents?: string, skipDirtyContents?: boolean): Promise<INotebookModel>;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards compatibility with old code, hopefully when custom editor is used we can blow away skipDirtyContents completely..

// tslint:disable-next-line: unified-signatures
public async loadModel(file: Uri, contents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: no-any
public async loadModel(file: Uri, contents?: string, options?: any): Promise<INotebookModel> {
// Every time we load a new untitled file, up the counter past the max value for this counter
this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter);

// Load our model from our storage object.
const model = await this.storage.load(file, contents, skipDirtyContents);
const model = await this.storage.load(file, contents, options);

// Make sure to listen to events on the model
this.trackModel(model);
Expand Down
59 changes: 38 additions & 21 deletions src/client/datascience/interactive-ipynb/nativeEditorStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,13 +519,16 @@ export class NativeEditorStorage implements INotebookStorage {
return isUntitledFile(file);
}

public getBackupId(model: INotebookModel): string {
const key = this.getStorageKey(model.file);
return this.getHashedFileName(key);
public generateBackupId(model: INotebookModel): string {
return `${path.basename(model.file.fsPath)}-${uuid()}`;
}

public load(file: Uri, possibleContents?: string, skipDirtyContents?: boolean): Promise<INotebookModel> {
return this.loadFromFile(file, possibleContents, skipDirtyContents);
public load(file: Uri, possibleContents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: unified-signatures
public load(file: Uri, possibleContents?: string, skipDirtyContents?: boolean): Promise<INotebookModel>;
// tslint:disable-next-line: no-any
public load(file: Uri, possibleContents?: string, options?: any): Promise<INotebookModel> {
return this.loadFromFile(file, possibleContents, options);
}
public async save(model: INotebookModel, _cancellation: CancellationToken): Promise<void> {
const contents = model.getContent();
Expand All @@ -552,15 +555,15 @@ export class NativeEditorStorage implements INotebookStorage {
});
this.savedAs.fire({ new: file, old });
}
public async backup(model: INotebookModel, cancellation: CancellationToken): Promise<void> {
public async backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string): Promise<void> {
// If we are already backing up, save this request replacing any other previous requests
if (this.backingUp) {
this.backupRequested = { model, cancellation };
return;
}
this.backingUp = true;
// Should send to extension context storage path
return this.storeContentsInHotExitFile(model, cancellation).finally(() => {
return this.storeContentsInHotExitFile(model, cancellation, backupId).finally(() => {
this.backingUp = false;

// If there is a backup request waiting, then clear and start it
Expand All @@ -579,17 +582,21 @@ export class NativeEditorStorage implements INotebookStorage {
await this.loadFromFile(model.file);
}

public async deleteBackup(model: INotebookModel): Promise<void> {
return this.clearHotExit(model.file);
public async deleteBackup(model: INotebookModel, backupId: string): Promise<void> {
return this.clearHotExit(model.file, backupId);
}
/**
* Stores the uncommitted notebook changes into a temporary location.
* Also keep track of the current time. This way we can check whether changes were
* made to the file since the last time uncommitted changes were stored.
*/
private async storeContentsInHotExitFile(model: INotebookModel, cancelToken?: CancellationToken): Promise<void> {
private async storeContentsInHotExitFile(
model: INotebookModel,
cancelToken?: CancellationToken,
backupId?: string
): Promise<void> {
const contents = model.getContent();
const key = this.getStorageKey(model.file);
const key = backupId || this.getStaticStorageKey(model.file);
const filePath = this.getHashedFileName(key);

// Keep track of the time when this data was saved.
Expand All @@ -599,8 +606,8 @@ export class NativeEditorStorage implements INotebookStorage {
return this.writeToStorage(filePath, specialContents, cancelToken);
}

private async clearHotExit(file: Uri): Promise<void> {
const key = this.getStorageKey(file);
private async clearHotExit(file: Uri, backupId?: string): Promise<void> {
const key = backupId || this.getStaticStorageKey(file);
const filePath = this.getHashedFileName(key);
await this.writeToStorage(filePath, undefined);
}
Expand All @@ -613,8 +620,10 @@ export class NativeEditorStorage implements INotebookStorage {
if (!cancelToken?.isCancellationRequested) {
await this.fileSystem.writeFile(filePath, contents);
}
} else if (await this.fileSystem.fileExists(filePath)) {
await this.fileSystem.deleteFile(filePath);
} else {
await this.fileSystem
.deleteFile(filePath)
.catch((ex) => traceError('Failed to delete hotExit file. Possible it does not exist', ex));
}
}
} catch (exc) {
Expand Down Expand Up @@ -658,24 +667,32 @@ export class NativeEditorStorage implements INotebookStorage {
noop();
}
}
private loadFromFile(file: Uri, possibleContents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: unified-signatures
private loadFromFile(file: Uri, possibleContents?: string, skipDirtyContents?: boolean): Promise<INotebookModel>;
private async loadFromFile(
file: Uri,
possibleContents?: string,
skipDirtyContents?: boolean
options?: boolean | string
): Promise<INotebookModel> {
try {
// Attempt to read the contents if a viable file
const contents = NativeEditorStorage.isUntitledFile(file)
? possibleContents
: await this.fileSystem.readFile(file.fsPath);

const skipDirtyContents = typeof options === 'boolean' ? options : !!options;
// Use backupId provided, else use static storage key.
const backupId =
typeof options === 'string' ? options : skipDirtyContents ? undefined : this.getStaticStorageKey(file);

// If skipping dirty contents, delete the dirty hot exit file now
if (skipDirtyContents) {
await this.clearHotExit(file);
await this.clearHotExit(file, backupId);
}

// See if this file was stored in storage prior to shutdown
const dirtyContents = skipDirtyContents ? undefined : await this.getStoredContents(file);
const dirtyContents = skipDirtyContents ? undefined : await this.getStoredContents(file, backupId);
if (dirtyContents) {
// This means we're dirty. Indicate dirty and load from this content
return this.loadContents(file, dirtyContents, true);
Expand Down Expand Up @@ -748,7 +765,7 @@ export class NativeEditorStorage implements INotebookStorage {
);
}

private getStorageKey(file: Uri): string {
private getStaticStorageKey(file: Uri): string {
return `${KeyPrefix}${file.toString()}`;
}

Expand All @@ -760,8 +777,8 @@ export class NativeEditorStorage implements INotebookStorage {
* @returns {(Promise<string | undefined>)}
* @memberof NativeEditor
*/
private async getStoredContents(file: Uri): Promise<string | undefined> {
const key = this.getStorageKey(file);
private async getStoredContents(file: Uri, backupId?: string): Promise<string | undefined> {
const key = backupId || this.getStaticStorageKey(file);

// First look in the global storage file location
let result = await this.getStoredContentsFromFile(file, key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,32 @@ export class NotebookStorageProvider implements INotebookStorageProvider {
this.storageAndModels.delete(oldUri.toString());
this.storageAndModels.set(targetResource.toString(), Promise.resolve(model));
}
public getBackupId(model: INotebookModel): string {
return this.storage.getBackupId(model);
public generateBackupId(model: INotebookModel): string {
return this.storage.generateBackupId(model);
}
public backup(model: INotebookModel, cancellation: CancellationToken) {
return this.storage.backup(model, cancellation);
public backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string) {
return this.storage.backup(model, cancellation, backupId);
}
public revert(model: INotebookModel, cancellation: CancellationToken) {
return this.storage.revert(model, cancellation);
}
public deleteBackup(model: INotebookModel) {
return this.storage.deleteBackup(model);
public deleteBackup(model: INotebookModel, backupId?: string) {
return this.storage.deleteBackup(model, backupId);
}
public load(file: Uri, contents?: string | undefined, skipDirtyContents?: boolean): Promise<INotebookModel> {
public load(file: Uri, contents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: unified-signatures
public load(file: Uri, contents?: string, skipDirtyContents?: boolean): Promise<INotebookModel>;

// tslint:disable-next-line: no-any
public load(file: Uri, contents?: string, options?: any): Promise<INotebookModel> {
const key = file.toString();
if (!this.storageAndModels.has(key)) {
// Every time we load a new untitled file, up the counter past the max value for this counter
NotebookStorageProvider.untitledCounter = getNextUntitledCounter(
file,
NotebookStorageProvider.untitledCounter
);
const promise = this.storage.load(file, contents, skipDirtyContents);
const promise = this.storage.load(file, contents, options);
this.storageAndModels.set(key, promise.then(this.trackModel.bind(this)));
}
return this.storageAndModels.get(key)!;
Expand Down
6 changes: 3 additions & 3 deletions src/client/datascience/notebook/contentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ export class NotebookContentProvider implements VSCodeNotebookContentProvider {
cancellation: CancellationToken
): Promise<NotebookDocumentBackup> {
const model = await this.notebookStorage.load(document.uri);
const id = this.notebookStorage.getBackupId(model);
this.notebookStorage.backup(model, cancellation).ignoreErrors();
const id = this.notebookStorage.generateBackupId(model);
await this.notebookStorage.backup(model, cancellation, id);
return {
id,
delete: () => this.notebookStorage.deleteBackup(model).ignoreErrors() // This cleans up after save has happened.
delete: () => this.notebookStorage.deleteBackup(model, id).ignoreErrors()
};
}
}
8 changes: 5 additions & 3 deletions src/client/datascience/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,13 +1031,15 @@ export const INotebookStorage = Symbol('INotebookStorage');

export interface INotebookStorage {
readonly onSavedAs: Event<{ new: Uri; old: Uri }>;
getBackupId(model: INotebookModel): string;
generateBackupId(model: INotebookModel): string;
save(model: INotebookModel, cancellation: CancellationToken): Promise<void>;
saveAs(model: INotebookModel, targetResource: Uri): Promise<void>;
backup(model: INotebookModel, cancellation: CancellationToken): Promise<void>;
backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string): Promise<void>;
load(file: Uri, contents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: unified-signatures
load(file: Uri, contents?: string, skipDirtyContents?: boolean): Promise<INotebookModel>;
revert(model: INotebookModel, cancellation: CancellationToken): Promise<void>;
deleteBackup(model: INotebookModel): Promise<void>;
deleteBackup(model: INotebookModel, backupId?: string): Promise<void>;
}
type WebViewViewState = {
readonly visible: boolean;
Expand Down