Skip to content

Commit

Permalink
adding configurable timeout, #43768
Browse files Browse the repository at this point in the history
  • Loading branch information
jrieken committed Nov 19, 2019
1 parent 515a0cb commit 094fd80
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 31 deletions.
5 changes: 3 additions & 2 deletions src/vs/base/common/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { once as onceFn } from 'vs/base/common/functional';
import { Disposable, IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { CancellationToken } from 'vs/base/common/cancellation';

/**
* To an event a function with one or zero parameters
Expand Down Expand Up @@ -655,7 +656,7 @@ export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {

private _asyncDeliveryQueue?: [Listener<T>, T, Promise<any>[]][];

async fireAsync(eventFn: (thenables: Promise<any>[], listener: Function) => T): Promise<void> {
async fireAsync(eventFn: (thenables: Promise<any>[], listener: Function) => T, token: CancellationToken): Promise<void> {
if (!this._listeners) {
return;
}
Expand All @@ -672,7 +673,7 @@ export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
this._asyncDeliveryQueue.push([e.value, eventFn(thenables, typeof e.value === 'function' ? e.value : e.value[0]), thenables]);
}

while (this._asyncDeliveryQueue.length > 0) {
while (this._asyncDeliveryQueue.length > 0 && !token.isCancellationRequested) {
const [listener, event, thenables] = this._asyncDeliveryQueue.shift()!;
try {
if (typeof listener === 'function') {
Expand Down
11 changes: 6 additions & 5 deletions src/vs/base/test/common/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Event, Emitter, EventBufferer, EventMultiplexer, AsyncEmitter, IWaitUnt
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as Errors from 'vs/base/common/errors';
import { timeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';

namespace Samples {

Expand Down Expand Up @@ -276,7 +277,7 @@ suite('AsyncEmitter', function () {
foo: true,
bar: 1,
waitUntil(t: Promise<void>) { thenables.push(t); }
}));
}), CancellationToken.None);
emitter.dispose();
});

Expand Down Expand Up @@ -308,7 +309,7 @@ suite('AsyncEmitter', function () {
waitUntil(t) {
thenables.push(t);
}
}));
}), CancellationToken.None);
assert.equal(globalState, 2);
});

Expand All @@ -329,7 +330,7 @@ suite('AsyncEmitter', function () {
waitUntil(t) {
thenables.push(t);
}
}));
}), CancellationToken.None);
assert.deepEqual(events, [1, 2]);
done = true;
}
Expand All @@ -347,7 +348,7 @@ suite('AsyncEmitter', function () {
waitUntil(t) {
thenables.push(t);
}
}));
}), CancellationToken.None);
assert.ok(done);
});

Expand Down Expand Up @@ -377,7 +378,7 @@ suite('AsyncEmitter', function () {
waitUntil(t) {
thenables.push(t);
}
})).then(() => {
}), CancellationToken.None).then(() => {
assert.equal(globalState, 2);
}).catch(e => {
console.log(e);
Expand Down
46 changes: 41 additions & 5 deletions src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/ext
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { localize } from 'vs/nls';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationTokenSource } from 'vs/base/common/cancellation';

@extHostCustomer
export class MainThreadFileSystemEventService {
Expand All @@ -20,7 +25,9 @@ export class MainThreadFileSystemEventService {
extHostContext: IExtHostContext,
@IFileService fileService: IFileService,
@ITextFileService textFileService: ITextFileService,
@IProgressService progressService: IProgressService
@IProgressService progressService: IProgressService,
@IConfigurationService configService: IConfigurationService,
@ILogService logService: ILogService,
) {

const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
Expand Down Expand Up @@ -59,16 +66,33 @@ export class MainThreadFileSystemEventService {
messages.set(FileOperation.DELETE, localize('msg-delete', "Running 'File Delete' participants..."));
messages.set(FileOperation.MOVE, localize('msg-rename', "Running 'File Rename' participants..."));


this._listener.add(textFileService.onWillRunOperation(e => {

const timeout = configService.getValue<number>('files.participants.timeout');
if (timeout <= 0) {
return; // disabled
}

const p = progressService.withProgress({ location: ProgressLocation.Window }, progress => {

progress.report({ message: messages.get(e.operation) });

const p1 = proxy.$onWillRunFileOperation(e.operation, e.target, e.source);
const p2 = new Promise((_resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), 5000);
return new Promise((resolve, reject) => {

const cts = new CancellationTokenSource();

const timeoutHandle = setTimeout(() => {
logService.trace('CANCELLED file participants because of timeout', timeout, e.target, e.operation);
cts.cancel();
reject(new Error('timeout'));
}, timeout);

proxy.$onWillRunFileOperation(e.operation, e.target, e.source, cts.token)
.then(resolve, reject)
.finally(() => clearTimeout(timeoutHandle));
});
return Promise.race([p1, p2]);

});

e.waitUntil(p);
Expand All @@ -82,3 +106,15 @@ export class MainThreadFileSystemEventService {
this._listener.dispose();
}
}


Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
id: 'files',
properties: {
'files.participants.timeout': {
type: 'number',
default: 5000,
markdownDescription: localize('files.participants.timeout', "Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants."),
}
}
});
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ export interface FileSystemEvents {

export interface ExtHostFileSystemEventServiceShape {
$onFileEvent(events: FileSystemEvents): void;
$onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): Promise<any>;
$onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined, token: CancellationToken): Promise<any>;
$onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void;
}

Expand Down
33 changes: 18 additions & 15 deletions src/vs/workbench/api/common/extHostFileSystemEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Disposable, WorkspaceEdit } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { FileOperation } from 'vs/platform/files/common/files';
import { flatten } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';

class FileSystemWatcher implements vscode.FileSystemWatcher {

Expand Down Expand Up @@ -176,23 +177,23 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
};
}

async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): Promise<any> {
async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined, token: CancellationToken): Promise<any> {
switch (operation) {
case FileOperation.MOVE:
await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }], });
await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }, token);
break;
case FileOperation.DELETE:
await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] });
await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] }, token);
break;
case FileOperation.CREATE:
await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] });
await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] }, token);
break;
default:
//ignore, dont send
}
}

private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: Omit<E, 'waitUntil'>): Promise<any> {
private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: Omit<E, 'waitUntil'>, token: CancellationToken): Promise<any> {

const edits: WorkspaceEdit[] = [];
await Promise.resolve(emitter.fireAsync(bucket => {
Expand All @@ -214,19 +215,21 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
}
}
};
}));
}, token));

if (edits.length === 0) {
return undefined;
if (token.isCancellationRequested) {
return;
}

// flatten all WorkspaceEdits collected via waitUntil-call
// and apply them in one go.
const allEdits = new Array<Array<IResourceFileEditDto | IResourceTextEditDto>>();
for (let edit of edits) {
let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors);
allEdits.push(edits);
if (edits.length > 0) {
// flatten all WorkspaceEdits collected via waitUntil-call
// and apply them in one go.
const allEdits = new Array<Array<IResourceFileEditDto | IResourceTextEditDto>>();
for (let edit of edits) {
let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors);
allEdits.push(edits);
}
return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) });
}
return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) });
}
}
7 changes: 4 additions & 3 deletions src/vs/workbench/services/textfile/browser/textFileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ITextSnapshot } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { CancellationToken } from 'vs/base/common/cancellation';

/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
Expand Down Expand Up @@ -344,7 +345,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {

// before event
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.CREATE, resource));
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.CREATE, resource), CancellationToken.None);

const stat = await this.doCreate(resource, value, options);

Expand Down Expand Up @@ -374,7 +375,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {

// before event
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.DELETE, resource));
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.DELETE, resource), CancellationToken.None);

const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
await this.revertAll(dirtyFiles, { soft: true });
Expand All @@ -388,7 +389,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {

// before event
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.MOVE, target, source));
await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.MOVE, target, source), CancellationToken.None);

// find all models that related to either source or target (can be many if resource is a folder)
const sourceModels: ITextFileEditorModel[] = [];
Expand Down

0 comments on commit 094fd80

Please sign in to comment.