diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 39040b04bf7..f5f1304f3d9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2840,7 +2840,7 @@ declare module 'vscode' { /* * Replaces the existing contents of the channel with the given value. */ - replaceAll(value: string): void; + replace(value: string): void; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 47ed29594b4..5078c34fce2 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; import { MainThreadOutputServiceShape, MainContext, IExtHostContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; @@ -12,6 +12,7 @@ import { UriComponents, URI } from 'vs/base/common/uri'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IViewsService } from 'vs/workbench/common/views'; +import { isNumber } from 'vs/base/common/types'; @extHostNamedCustomer(MainContext.MainThreadOutputService) export class MainThreadOutputService extends Disposable implements MainThreadOutputServiceShape { @@ -65,26 +66,30 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut return undefined; } - public $update(channelId: string): Promise | undefined { + public $update(channelId: string, mode: OutputChannelUpdateMode, till?: number): Promise | undefined { const channel = this._getChannel(channelId); if (channel) { - channel.update(); + if (mode === OutputChannelUpdateMode.Append) { + channel.update(mode); + } else if (isNumber(till)) { + channel.update(mode, till); + } } return undefined; } - public $clear(channelId: string, till: number): Promise | undefined { + public $clear(channelId: string): Promise | undefined { const channel = this._getChannel(channelId); if (channel) { - channel.clear(till); + channel.clear(); } return undefined; } - public $replaceAll(channelId: string, till: number, value: string): Promise | undefined { + public $replace(channelId: string, value: string): Promise | undefined { const channel = this._getChannel(channelId); if (channel) { - channel.replaceAll(till, value); + channel.replace(value); } return undefined; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 03d33fe3739..17738393365 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -56,6 +56,7 @@ import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -445,9 +446,10 @@ export interface MainThreadMessageServiceShape extends IDisposable { export interface MainThreadOutputServiceShape extends IDisposable { $register(label: string, log: boolean, file?: UriComponents, extensionId?: string): Promise; $append(channelId: string, value: string): Promise | undefined; - $update(channelId: string): Promise | undefined; - $clear(channelId: string, till: number): Promise | undefined; - $replaceAll(channelId: string, till: number, value?: string): Promise | undefined; + $clear(channelId: string): Promise | undefined; + $replace(channelId: string, value: string): Promise | undefined; + $update(channelId: string, mode: OutputChannelUpdateMode.Append): Promise | undefined; + $update(channelId: string, mode: OutputChannelUpdateMode, till: number): Promise | undefined; $reveal(channelId: string, preserveFocus: boolean): Promise | undefined; $close(channelId: string): Promise | undefined; $dispose(channelId: string): Promise | undefined; diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 3845c68ff02..facadbcd405 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -7,7 +7,6 @@ import { MainContext, MainThreadOutputServiceShape, ExtHostOutputServiceShape } import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; -import { VSBuffer } from 'vs/base/common/buffer'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -18,73 +17,37 @@ export abstract class AbstractExtHostOutputChannel extends Disposable implements readonly _id: Promise; private readonly _name: string; protected readonly _proxy: MainThreadOutputServiceShape; + private _disposed: boolean; - private _offset: number; - private readonly _extension: IExtensionDescription; + get disposed(): boolean { return this._disposed; } public visible: boolean = false; - constructor(name: string, log: boolean, file: URI | undefined, extension: IExtensionDescription, proxy: MainThreadOutputServiceShape) { + constructor(name: string, log: boolean, file: URI | undefined, extensionId: string, proxy: MainThreadOutputServiceShape) { super(); this._name = name; this._proxy = proxy; - this._id = proxy.$register(this.name, log, file, extension.identifier.value); + this._id = proxy.$register(this.name, log, file, extensionId); this._disposed = false; - this._offset = 0; - this._extension = extension; } get name(): string { return this._name; } - append(value: string): void { - this.validate(); - this.incrementOffset(value); - } - appendLine(value: string): void { - this.validate(); this.append(value + '\n'); } - clear(): void { - this.validate(); - const till = this._offset; - this._id.then(id => this._proxy.$clear(id, till)); - } - - replaceAll(value: string, donotSendValue?: boolean): void { - this.validate(true); - const till = this._offset; - this.incrementOffset(value); - this._id.then(id => this._proxy.$replaceAll(id, till, donotSendValue ? undefined : value)); - } - show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { - this.validate(); this._id.then(id => this._proxy.$reveal(id, !!(typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus))); } hide(): void { - this.validate(); this._id.then(id => this._proxy.$close(id)); } - protected validate(checkProposedApi?: boolean): void { - if (checkProposedApi) { - checkProposedApiEnabled(this._extension); - } - if (this._disposed) { - throw new Error('Channel has been closed'); - } - } - - private incrementOffset(value: string) { - this._offset += value ? VSBuffer.fromString(value).byteLength : 0; - } - override dispose(): void { super.dispose(); @@ -94,50 +57,30 @@ export abstract class AbstractExtHostOutputChannel extends Disposable implements .then(() => this._disposed = true); } } + + abstract append(value: string): void; + abstract clear(): void; + abstract replace(value: string): void; } export class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { - constructor(name: string, extension: IExtensionDescription, proxy: MainThreadOutputServiceShape) { - super(name, false, undefined, extension, proxy); + constructor(name: string, extensionId: string, proxy: MainThreadOutputServiceShape) { + super(name, false, undefined, extensionId, proxy); } - override append(value: string): void { - super.append(value); + append(value: string): void { this._id.then(id => this._proxy.$append(id, value)); } - -} - -export class LazyOutputChannel implements vscode.OutputChannel { - - constructor( - readonly name: string, - private readonly _channel: Promise - ) { } - - append(value: string): void { - this._channel.then(channel => channel.append(value)); - } - appendLine(value: string): void { - this._channel.then(channel => channel.appendLine(value)); - } clear(): void { - this._channel.then(channel => channel.clear()); + this._id.then(id => this._proxy.$clear(id)); } - replaceAll(value: string): void { - this._channel.then(channel => channel.replaceAll(value)); - } - show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { - this._channel.then(channel => channel.show(columnOrPreserveFocus, preserveFocus)); - } - hide(): void { - this._channel.then(channel => channel.hide()); - } - dispose(): void { - this._channel.then(channel => channel.dispose()); + + replace(value: string): void { + this._id.then(id => this._proxy.$replace(id, value)); } + } export class ExtHostOutputService implements ExtHostOutputServiceShape { @@ -145,12 +88,18 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { readonly _serviceBrand: undefined; protected readonly _proxy: MainThreadOutputServiceShape; + private readonly _channels: Map = new Map(); + private visibleChannelId: string | null = null; constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadOutputService); } - $setVisibleChannel(channelId: string): void { + $setVisibleChannel(visibleChannelId: string | null): void { + this.visibleChannelId = visibleChannelId; + for (const [id, channel] of this._channels) { + channel.visible = id === this.visibleChannelId; + } } createOutputChannel(name: string, extension: IExtensionDescription): vscode.OutputChannel { @@ -158,9 +107,73 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { if (!name) { throw new Error('illegal argument `name`. must not be falsy'); } - return new ExtHostPushOutputChannel(name, extension, this._proxy); + const extHostOutputChannel = this.doCreateOutChannel(name, extension); + extHostOutputChannel.then(channel => channel._id.then(id => { + this._channels.set(id, channel); + channel.visible = id === this.visibleChannelId; + })); + return this.createExtHostOutputChannel(name, extHostOutputChannel, extension); } + protected async doCreateOutChannel(name: string, extension: IExtensionDescription): Promise { + return new ExtHostPushOutputChannel(name, extension.identifier.value, this._proxy); + } + + private createExtHostOutputChannel(name: string, channelPromise: Promise, extensionDescription: IExtensionDescription): vscode.OutputChannel { + const validate = (channel: AbstractExtHostOutputChannel, checkProposedApi?: boolean) => { + if (checkProposedApi) { + checkProposedApiEnabled(extensionDescription); + } + if (channel.disposed) { + throw new Error('Channel has been closed'); + } + }; + return { + get name(): string { return name; }, + append(value: string): void { + channelPromise.then(channel => { + validate(channel); + channel.append(value); + }); + }, + appendLine(value: string): void { + channelPromise.then(channel => { + validate(channel); + channel.appendLine(value); + }); + }, + clear(): void { + channelPromise.then(channel => { + validate(channel); + channel.clear(); + }); + }, + replace(value: string): void { + channelPromise.then(channel => { + validate(channel, true); + channel.replace(value); + }); + }, + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + channelPromise.then(channel => { + validate(channel); + channel.show(columnOrPreserveFocus, preserveFocus); + }); + }, + hide(): void { + channelPromise.then(channel => { + validate(channel); + channel.hide(); + }); + }, + dispose(): void { + channelPromise.then(channel => { + validate(channel); + channel.dispose(); + }); + } + }; + } } export interface IExtHostOutputService extends ExtHostOutputService { } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 540563fad58..6016baa59b8 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtHostOutputService2 } from 'vs/workbench/api/node/extHostOutputService'; +import { ExtHostOutputService } from 'vs/workbench/api/node/extHostOutputService'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService'; @@ -34,7 +34,7 @@ registerSingleton(ILogService, ExtHostLogService); registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostDebugService, ExtHostDebugService); -registerSingleton(IExtHostOutputService, ExtHostOutputService2); +registerSingleton(IExtHostOutputService, ExtHostOutputService); registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); registerSingleton(IExtHostTerminalService, ExtHostTerminalService); diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index e057eab0d37..971129b7c86 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { toLocalISOString } from 'vs/base/common/date'; import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; -import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; +import { AbstractExtHostOutputChannel, ExtHostOutputService as BaseExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,6 +17,8 @@ import { createRotatingLogger } from 'vs/platform/log/node/spdlogLog'; import { Logger } from 'spdlog'; import { ByteSize } from 'vs/platform/files/common/files'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { VSBuffer } from 'vs/base/common/buffer'; class OutputAppender { @@ -41,23 +43,34 @@ class OutputAppender { class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { - private _appender: OutputAppender; + private _offset: number; + private readonly _appender: OutputAppender; - constructor(name: string, appender: OutputAppender, extension: IExtensionDescription, proxy: MainThreadOutputServiceShape) { - super(name, false, URI.file(appender.file), extension, proxy); + constructor(name: string, appender: OutputAppender, extensionId: string, proxy: MainThreadOutputServiceShape) { + super(name, false, URI.file(appender.file), extensionId, proxy); + this._offset = 0; this._appender = appender; } - override append(value: string): void { - super.append(value); + append(value: string): void { + this.incrementOffset(value); this._appender.append(value); if (this.visible) { - this.update(); + this._appender.flush(); + this._id.then(id => this._proxy.$update(id, OutputChannelUpdateMode.Append)); } } - override replaceAll(value: string): void { - super.replaceAll(value, true); + clear(): void { + const till = this._offset; + this._appender.flush(); + this._id.then(id => this._proxy.$update(id, OutputChannelUpdateMode.Clear, till)); + } + + replace(value: string): void { + const till = this._offset; + this.incrementOffset(value); + this._id.then(id => this._proxy.$update(id, OutputChannelUpdateMode.Replace, till)); this._appender.append(value); if (this.visible) { this._appender.flush(); @@ -69,24 +82,16 @@ class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { super.show(columnOrPreserveFocus, preserveFocus); } - override clear(): void { - this._appender.flush(); - super.clear(); + private incrementOffset(value: string) { + this._offset += VSBuffer.fromString(value).byteLength; } - private update(): void { - this._appender.flush(); - this._id.then(id => this._proxy.$update(id)); - } } -export class ExtHostOutputService2 extends ExtHostOutputService { +export class ExtHostOutputService extends BaseExtHostOutputService { private _logsLocation: URI; private _namePool: number = 1; - private readonly _channels: Map = new Map(); - - private visibleChannelId: string | null = null; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -97,27 +102,7 @@ export class ExtHostOutputService2 extends ExtHostOutputService { this._logsLocation = initData.logsLocation; } - override $setVisibleChannel(visibleChannelId: string | null): void { - this.visibleChannelId = visibleChannelId; - for (const [id, channel] of this._channels) { - channel.visible = id === this.visibleChannelId; - } - } - - override createOutputChannel(name: string, extension: IExtensionDescription): vscode.OutputChannel { - name = name.trim(); - if (!name) { - throw new Error('illegal argument `name`. must not be falsy'); - } - const extHostOutputChannel = this._doCreateOutChannel(name, extension); - extHostOutputChannel.then(channel => channel._id.then(id => { - this._channels.set(id, channel); - channel.visible = id === this.visibleChannelId; - })); - return new LazyOutputChannel(name, extHostOutputChannel); - } - - private async _doCreateOutChannel(name: string, extension: IExtensionDescription): Promise { + protected override async doCreateOutChannel(name: string, extension: IExtensionDescription): Promise { try { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); const exists = await SymlinkSupport.existsDirectory(outputDirPath); @@ -127,11 +112,11 @@ export class ExtHostOutputService2 extends ExtHostOutputService { const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); const appender = await OutputAppender.create(fileName, file.fsPath); - return new ExtHostOutputChannelBackedByFile(name, appender, extension, this._proxy); + return new ExtHostOutputChannelBackedByFile(name, appender, extension.identifier.value, this._proxy); } catch (error) { // Do not crash if logger cannot be created this.logService.error(error); - return new ExtHostPushOutputChannel(name, extension, this._proxy); } + return super.doCreateOutChannel(name, extension); } } diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 74840be69b9..a8dc8204d1b 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,7 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME } from 'vs/workbench/contrib/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; import { IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/common/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; @@ -46,16 +46,16 @@ class OutputChannel extends Disposable implements IOutputChannel { this.model.append(output); } - update(): void { - this.model.update(); + update(mode: OutputChannelUpdateMode, till?: number): void { + this.model.update(mode, till); } - clear(till?: number): void { - this.model.clear(till); + clear(): void { + this.model.clear(); } - replaceAll(till: number, value: string): void { - this.model.replaceAll(till, value); + replace(value: string): void { + this.model.replace(value); } } diff --git a/src/vs/workbench/contrib/output/common/output.ts b/src/vs/workbench/contrib/output/common/output.ts index 2d7c2b3ec20..902e024acc8 100644 --- a/src/vs/workbench/contrib/output/common/output.ts +++ b/src/vs/workbench/contrib/output/common/output.ts @@ -95,6 +95,12 @@ export interface IOutputService { onActiveOutputChannel: Event; } +export enum OutputChannelUpdateMode { + Append = 1, + Replace, + Clear +} + export interface IOutputChannel { /** @@ -117,20 +123,21 @@ export interface IOutputChannel { */ append(output: string): void; - /** - * Update the channel. - */ - update(): void; - /** * Clears all received output for this channel. */ - clear(till?: number): void; + clear(): void; /** - * Replaces the output of the channel. + * Replaces the content of the channel with given output */ - replaceAll(till: number, value: string): void; + replace(output: string): void; + + /** + * Update the channel. + */ + update(mode: OutputChannelUpdateMode.Append): void; + update(mode: OutputChannelUpdateMode, till: number): void; /** * Disposes the output channel. diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 4e340d24ddb..99b4a96ace1 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,69 +21,153 @@ import { Range } from 'vs/editor/common/core/range'; import { VSBuffer } from 'vs/base/common/buffer'; import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; append(output: string): void; - update(): void; + update(mode: OutputChannelUpdateMode, till?: number): void; loadModel(): Promise; - clear(till?: number): void; - replaceAll(till: number, value?: string): void; + clear(): void; + replace(value: string): void; } -const enum ModelUpdateMode { - Append = 1, - Replace, - Clear +class OutputFileListener extends Disposable { + + private readonly _onDidContentChange = new Emitter(); + readonly onDidContentChange: Event = this._onDidContentChange.event; + + private watching: boolean = false; + private syncDelayer: ThrottledDelayer; + private etag: string | undefined; + + constructor( + private readonly file: URI, + private readonly fileService: IFileService, + private readonly logService: ILogService + ) { + super(); + this.syncDelayer = new ThrottledDelayer(500); + } + + watch(eTag: string | undefined): void { + if (!this.watching) { + this.etag = eTag; + this.poll(); + this.logService.trace('Started polling', this.file.toString()); + this.watching = true; + } + } + + private poll(): void { + const loop = () => this.doWatch().then(() => this.poll()); + this.syncDelayer.trigger(loop); + } + + private async doWatch(): Promise { + const stat = await this.fileService.resolve(this.file, { resolveMetadata: true }); + if (stat.etag !== this.etag) { + this.etag = stat.etag; + this._onDidContentChange.fire(stat.size); + } + } + + unwatch(): void { + if (this.watching) { + this.syncDelayer.cancel(); + this.watching = false; + this.logService.trace('Stopped polling', this.file.toString()); + } + } + + override dispose(): void { + this.unwatch(); + super.dispose(); + } } -export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel { +export class FileOutputChannelModel extends Disposable implements IOutputChannelModel { - protected readonly _onDispose = this._register(new Emitter()); + private readonly _onDispose = this._register(new Emitter()); readonly onDispose: Event = this._onDispose.event; - protected model: ITextModel | null = null; + private readonly fileHandler: OutputFileListener; + private etag: string | undefined = ''; + + private loadModelPromise: Promise | null = null; + private model: ITextModel | null = null; private modelUpdateInProgress: boolean = false; - private modelUpdateCancellationSource = this._register(new MutableDisposable()); - private appendThrottler = this._register(new ThrottledDelayer(300)); + private readonly modelUpdateCancellationSource = this._register(new MutableDisposable()); + private readonly appendThrottler = this._register(new ThrottledDelayer(300)); private replacePromise: Promise | undefined; - protected startOffset: number = 0; - protected endOffset: number = 0; + private startOffset: number = 0; + private endOffset: number = 0; constructor( private readonly modelUri: URI, private readonly mimeType: string, - protected readonly file: URI, - protected fileService: IFileService, - protected modelService: IModelService, - protected modeService: IModeService, - protected editorWorkerService: IEditorWorkerService, + private readonly file: URI, + @IFileService private readonly fileService: IFileService, + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService, + @ILogService logService: ILogService, + @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, ) { super(); + + this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService, logService)); + this._register(this.fileHandler.onDidContentChange(size => this.onDidContentChange(size))); + this._register(toDisposable(() => this.fileHandler.unwatch())); } - clear(till?: number): void { - this.startOffset = this.endOffset = isNumber(till) ? till : this.endOffset; - this.updateModel(ModelUpdateMode.Clear); + append(message: string): void { + throw new Error('Not supported'); } - replaceAll(till: number, message: string): void { - this.startOffset = this.endOffset = till; - this.updateModel(ModelUpdateMode.Replace); + replace(message: string): void { + throw new Error('Not supported'); } - update(): void { } + clear(): void { + this.update(OutputChannelUpdateMode.Clear, this.endOffset); + } - protected createModel(content: string): ITextModel { + update(mode: OutputChannelUpdateMode, till?: number): void { + const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); + loadModelPromise.then(() => this.doUpdate(mode, till)); + } + + loadModel(): Promise { + this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { + try { + let content = ''; + if (await this.fileService.exists(this.file)) { + const fileContent = await this.fileService.readFile(this.file, { position: this.startOffset }); + this.endOffset = this.startOffset + fileContent.value.byteLength; + this.etag = fileContent.etag; + content = fileContent.value.toString(); + } else { + this.startOffset = 0; + this.endOffset = 0; + } + c(this.createModel(content)); + } catch (error) { + e(error); + } + }); + return this.loadModelPromise; + } + + private createModel(content: string): ITextModel { if (this.model) { this.model.setValue(content); } else { this.model = this.modelService.createModel(content, this.modeService.create(this.mimeType), this.modelUri); - this.onModelCreated(this.model); + this.fileHandler.watch(this.etag); const disposable = this.model.onWillDispose(() => { this.cancelModelUpdate(); - this.onModelWillDispose(this.model); + this.fileHandler.unwatch(); this.model = null; dispose(disposable); }); @@ -91,28 +175,31 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen return this.model; } - protected updateModel(mode: ModelUpdateMode): void { - if (mode !== ModelUpdateMode.Append) { + private doUpdate(mode: OutputChannelUpdateMode, till?: number): void { + if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { + this.startOffset = this.endOffset = isNumber(till) ? till : this.endOffset; this.cancelModelUpdate(); } if (!this.model) { return; } + + this.modelUpdateInProgress = true; if (!this.modelUpdateCancellationSource.value) { this.modelUpdateCancellationSource.value = new CancellationTokenSource(); } - this.modelUpdateInProgress = true; const token = this.modelUpdateCancellationSource.value.token; - switch (mode) { - case ModelUpdateMode.Clear: - this.clearContent(this.model); - break; - case ModelUpdateMode.Replace: - this.replacePromise = this.replaceContent(this.model, token).finally(() => this.replacePromise = undefined); - break; - case ModelUpdateMode.Append: - this.appendContent(this.model, token); - break; + + if (mode === OutputChannelUpdateMode.Clear) { + this.clearContent(this.model); + } + + else if (mode === OutputChannelUpdateMode.Replace) { + this.replacePromise = this.replaceContent(this.model, token).finally(() => this.replacePromise = undefined); + } + + else { + this.appendContent(this.model, token); } } @@ -187,8 +274,8 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen if (edits.length) { model.applyEdits(edits); } + this.endOffset = this.endOffset + content.byteLength; this.modelUpdateInProgress = false; - this.onDidModelUpdate(content); } protected cancelModelUpdate(): void { @@ -201,17 +288,27 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen this.modelUpdateInProgress = false; } - protected isModelUpdateInProgress(): boolean { - return this.modelUpdateInProgress; + private async getContentToUpdate(): Promise { + const content = await this.fileService.readFile(this.file, { position: this.endOffset }); + this.etag = content.etag; + return content.value; } - abstract loadModel(): Promise; - abstract append(message: string): void; + private onDidContentChange(size: number | undefined): void { + if (this.model) { + if (!this.modelUpdateInProgress) { + if (isNumber(size) && this.endOffset > size) { + // Reset - Content is removed + this.update(OutputChannelUpdateMode.Clear, 0); + } + } + this.update(OutputChannelUpdateMode.Append); + } + } - protected onModelCreated(model: ITextModel) { } - protected onModelWillDispose(model: ITextModel | null) { } - protected abstract getContentToUpdate(): Promise; - protected abstract onDidModelUpdate(content: VSBuffer): void; + protected isVisible(): boolean { + return !!this.model; + } override dispose(): void { this._onDispose.fire(); @@ -219,159 +316,10 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen } } -class OutputFileListener extends Disposable { - - private readonly _onDidContentChange = new Emitter(); - readonly onDidContentChange: Event = this._onDidContentChange.event; - - private watching: boolean = false; - private syncDelayer: ThrottledDelayer; - private etag: string | undefined; - - constructor( - private readonly file: URI, - private readonly fileService: IFileService, - private readonly logService: ILogService - ) { - super(); - this.syncDelayer = new ThrottledDelayer(500); - } - - watch(eTag: string | undefined): void { - if (!this.watching) { - this.etag = eTag; - this.poll(); - this.logService.trace('Started polling', this.file.toString()); - this.watching = true; - } - } - - private poll(): void { - const loop = () => this.doWatch().then(() => this.poll()); - this.syncDelayer.trigger(loop); - } - - private async doWatch(): Promise { - const stat = await this.fileService.resolve(this.file, { resolveMetadata: true }); - if (stat.etag !== this.etag) { - this.etag = stat.etag; - this._onDidContentChange.fire(stat.size); - } - } - - unwatch(): void { - if (this.watching) { - this.syncDelayer.cancel(); - this.watching = false; - this.logService.trace('Stopped polling', this.file.toString()); - } - } - - override dispose(): void { - this.unwatch(); - super.dispose(); - } -} - -/** - * An output channel driven by a file and does not support appending messages. - */ -export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { - - private readonly fileHandler: OutputFileListener; - - private etag: string | undefined = ''; - private loadModelPromise: Promise | null = null; - - constructor( - modelUri: URI, - mimeType: string, - file: URI, - @IFileService fileService: IFileService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILogService logService: ILogService, - @IEditorWorkerService editorWorkerService: IEditorWorkerService - ) { - super(modelUri, mimeType, file, fileService, modelService, modeService, editorWorkerService); - - this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService, logService)); - this._register(this.fileHandler.onDidContentChange(size => this.update(size))); - this._register(toDisposable(() => this.fileHandler.unwatch())); - } - - loadModel(): Promise { - this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { - try { - let content = ''; - if (await this.fileService.exists(this.file)) { - const fileContent = await this.fileService.readFile(this.file, { position: this.startOffset }); - this.endOffset = this.startOffset + fileContent.value.byteLength; - this.etag = fileContent.etag; - content = fileContent.value.toString(); - } else { - this.startOffset = 0; - this.endOffset = 0; - } - c(this.createModel(content)); - } catch (error) { - e(error); - } - }); - return this.loadModelPromise; - } - - override clear(till?: number): void { - const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); - loadModelPromise.then(() => { - super.clear(till); - this.update(); - }); - } - - append(message: string): void { - throw new Error('Not supported'); - } - - protected async getContentToUpdate(): Promise { - const content = await this.fileService.readFile(this.file, { position: this.endOffset }); - this.etag = content.etag; - return content.value; - } - - protected override onDidModelUpdate(content: VSBuffer): void { - this.endOffset = this.endOffset + content.byteLength; - } - - protected override onModelCreated(model: ITextModel): void { - this.fileHandler.watch(this.etag); - } - - protected override onModelWillDispose(model: ITextModel | null): void { - this.fileHandler.unwatch(); - } - - override update(size?: number): void { - if (this.model) { - if (!this.isModelUpdateInProgress()) { - if (isNumber(size) && this.endOffset > size) { - // Reset - Content is removed - this.startOffset = this.endOffset = 0; - this.updateModel(ModelUpdateMode.Clear); - } - } - this.updateModel(ModelUpdateMode.Append); - } - } -} - -class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel { +class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel { private logger: ILogger; - private appendedMessage: string; - private loadingFromFileInProgress: boolean; - private resettingDelayer: ThrottledDelayer; - private readonly rotatingFilePath: URI; + private _offset: number; constructor( id: string, @@ -382,99 +330,35 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement @IModelService modelService: IModelService, @IModeService modeService: IModeService, @ILoggerService loggerService: ILoggerService, + @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService ) { - super(modelUri, mimeType, file, fileService, modelService, modeService, editorWorkerService); - this.appendedMessage = ''; - this.loadingFromFileInProgress = false; + super(modelUri, mimeType, file, fileService, modelService, modeService, logService, editorWorkerService); // Donot rotate to check for the file reset - this.logger = loggerService.createLogger(this.file, { always: true, donotRotate: true, donotUseFormatters: true }); - - const rotatingFilePathDirectory = resources.dirname(this.file); - this.rotatingFilePath = resources.joinPath(rotatingFilePathDirectory, `${id}.1.log`); - - this._register(fileService.watch(rotatingFilePathDirectory)); - this._register(fileService.onDidFilesChange(e => { - if (e.contains(this.rotatingFilePath)) { - this.resettingDelayer.trigger(() => this.resetModel()); - } - })); - - this.resettingDelayer = new ThrottledDelayer(50); + this.logger = loggerService.createLogger(file, { always: true, donotRotate: true, donotUseFormatters: true }); + this._offset = 0; } - append(message: string): void { - // update end offset always as message is read - this.endOffset = this.endOffset + VSBuffer.fromString(message).byteLength; - if (this.loadingFromFileInProgress) { - this.appendedMessage += message; - } else { - this.write(message); - if (this.model) { - this.appendedMessage += message; - this.updateModel(ModelUpdateMode.Append); - } - } + override append(message: string): void { + this.write(message); + this.update(OutputChannelUpdateMode.Append); } - override replaceAll(till: number, value: string): void { - this.appendedMessage = value; - super.replaceAll(till, value); - } - - override clear(till?: number): void { - super.clear(till); - this.appendedMessage = ''; - } - - async loadModel(): Promise { - this.loadingFromFileInProgress = true; - this.cancelModelUpdate(); - this.appendedMessage = ''; - let content = await this.loadFile(); - if (this.endOffset !== this.startOffset + VSBuffer.fromString(content).byteLength) { - // Queue content is not written into the file - // Flush it and load file again - this.flush(); - content = await this.loadFile(); - } - if (this.appendedMessage) { - this.write(this.appendedMessage); - this.appendedMessage = ''; - } - this.loadingFromFileInProgress = false; - return this.createModel(content); - } - - private async resetModel(): Promise { - this.startOffset = 0; - this.endOffset = 0; - if (this.model) { - await this.loadModel(); - } - } - - private async loadFile(): Promise { - const content = await this.fileService.readFile(this.file, { position: this.startOffset }); - return this.appendedMessage ? content.value + this.appendedMessage : content.value.toString(); - } - - protected async getContentToUpdate(): Promise { - return VSBuffer.fromString(this.appendedMessage); - } - - protected override onDidModelUpdate(content: VSBuffer): void { - this.appendedMessage = ''; + override replace(message: string): void { + const till = this._offset; + this.write(message); + this.update(OutputChannelUpdateMode.Replace, till); } private write(content: string): void { + this._offset += VSBuffer.fromString(content).byteLength; this.logger.info(content); + if (this.isVisible()) { + this.logger.flush(); + } } - private flush(): void { - this.logger.flush(); - } } export class DelegatedOutputChannelModel extends Disposable implements IOutputChannelModel { @@ -509,19 +393,19 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output)); } - update(): void { - this.outputChannelModel.then(outputChannelModel => outputChannelModel.update()); + update(mode: OutputChannelUpdateMode, till?: number): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.update(mode, till)); } loadModel(): Promise { return this.outputChannelModel.then(outputChannelModel => outputChannelModel.loadModel()); } - clear(till?: number): void { - this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear(till)); + clear(): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear()); } - replaceAll(till: number, value: string): void { - this.outputChannelModel.then(outputChannelModel => outputChannelModel.replaceAll(till, value)); + replace(value: string): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value)); } }