diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 2ef6c5c49a1..be75b642ab6 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -11,8 +11,9 @@ import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender import { toLocalISOString } from 'vs/base/common/date'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dirExists, mkdirp } from 'vs/base/node/pfs'; -export abstract class AbstractExtHostOutputChannel extends Disposable implements vscode.OutputChannel { +abstract class AbstractExtHostOutputChannel extends Disposable implements vscode.OutputChannel { readonly _id: Promise; private readonly _name: string; @@ -83,7 +84,7 @@ export abstract class AbstractExtHostOutputChannel extends Disposable implements } } -export class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { +class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { constructor(name: string, proxy: MainThreadOutputServiceShape) { super(name, false, undefined, proxy); @@ -96,17 +97,13 @@ export class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { } } -export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { +class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { - private static _namePool = 1; private _appender: OutputAppender; - constructor(name: string, outputDir: string, proxy: MainThreadOutputServiceShape) { - const fileName = `${ExtHostOutputChannelBackedByFile._namePool++}-${name}`; - const file = URI.file(join(outputDir, `${fileName}.log`)); - - super(name, false, file, proxy); - this._appender = new OutputAppender(fileName, file.fsPath); + constructor(name: string, appender: OutputAppender, proxy: MainThreadOutputServiceShape) { + super(name, false, URI.file(appender.file), proxy); + this._appender = appender; } append(value: string): void { @@ -131,7 +128,7 @@ export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChann } } -export class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { +class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { constructor(name: string, file: URI, proxy: MainThreadOutputServiceShape) { super(name, true, file, proxy); @@ -142,15 +139,31 @@ export class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { } } +let namePool = 1; +async function createExtHostOutputChannel(name: string, outputDirPromise: Promise, proxy: MainThreadOutputServiceShape): Promise { + try { + const outputDir = await outputDirPromise; + const fileName = `${namePool++}-${name}`; + const file = URI.file(join(outputDir, `${fileName}.log`)); + const appender = new OutputAppender(fileName, file.fsPath); + return new ExtHostOutputChannelBackedByFile(name, appender, proxy); + } catch (error) { + // Do not crash if logger cannot be created + console.log(error); + return new ExtHostPushOutputChannel(name, proxy); + } +} + export class ExtHostOutputService implements ExtHostOutputServiceShape { + private readonly _outputDir: Promise; private _proxy: MainThreadOutputServiceShape; - private _outputDir: string; private _channels: Map = new Map(); private _visibleChannelDisposable: IDisposable; constructor(logsLocation: URI, mainContext: IMainContext) { - this._outputDir = join(logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); + const outputDirPath = join(logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); + this._outputDir = dirExists(outputDirPath).then(exists => exists ? exists : mkdirp(outputDirPath)).then(() => outputDirPath); this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService); } @@ -167,23 +180,32 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { } createOutputChannel(name: string): vscode.OutputChannel { - const channel = this._createOutputChannel(name); - channel._id.then(id => this._channels.set(id, channel)); - return channel; - } - - private _createOutputChannel(name: string): AbstractExtHostOutputChannel { name = name.trim(); if (!name) { throw new Error('illegal argument `name`. must not be falsy'); } else { - // Do not crash if logger cannot be created - try { - return new ExtHostOutputChannelBackedByFile(name, this._outputDir, this._proxy); - } catch (error) { - console.log(error); - return new ExtHostPushOutputChannel(name, this._proxy); - } + const extHostOutputChannel = createExtHostOutputChannel(name, this._outputDir, this._proxy); + extHostOutputChannel.then(channel => channel._id.then(id => this._channels.set(id, channel))); + return { + append(value: string): void { + extHostOutputChannel.then(channel => channel.append(value)); + }, + appendLine(value: string): void { + extHostOutputChannel.then(channel => channel.appendLine(value)); + }, + clear(): void { + extHostOutputChannel.then(channel => channel.clear()); + }, + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + extHostOutputChannel.then(channel => channel.show(columnOrPreserveFocus, preserveFocus)); + }, + hide(): void { + extHostOutputChannel.then(channel => channel.hide()); + }, + dispose(): void { + extHostOutputChannel.then(channel => channel.dispose()); + } + }; } } diff --git a/src/vs/workbench/services/output/common/outputChannelModel.ts b/src/vs/workbench/services/output/common/outputChannelModel.ts index 81ed0ef526d..071ad9b2f2a 100644 --- a/src/vs/workbench/services/output/common/outputChannelModel.ts +++ b/src/vs/workbench/services/output/common/outputChannelModel.ts @@ -279,7 +279,7 @@ class FileOutputChannelModel extends AbstractFileOutputChannelModel implements I } } -class BufferredOutputChannel extends Disposable implements IOutputChannelModel { +export class BufferredOutputChannel extends Disposable implements IOutputChannelModel { readonly file: URI | null = null; scrollLock: boolean = false; diff --git a/src/vs/workbench/services/output/node/outputAppender.ts b/src/vs/workbench/services/output/node/outputAppender.ts index a7ffc9cd3d2..04cf07303c3 100644 --- a/src/vs/workbench/services/output/node/outputAppender.ts +++ b/src/vs/workbench/services/output/node/outputAppender.ts @@ -9,7 +9,7 @@ export class OutputAppender { private appender: RotatingLogger; - constructor(name: string, file: string) { + constructor(name: string, readonly file: string) { this.appender = createRotatingLogger(name, file, 1024 * 1024 * 30, 1); this.appender.clearFormatters(); } diff --git a/src/vs/workbench/services/output/node/outputChannelModelService.ts b/src/vs/workbench/services/output/node/outputChannelModelService.ts index 7709518de6f..3fedb28b0ed 100644 --- a/src/vs/workbench/services/output/node/outputChannelModelService.ts +++ b/src/vs/workbench/services/output/node/outputChannelModelService.ts @@ -6,21 +6,23 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as extfs from 'vs/base/node/extfs'; import { dirname, join } from 'vs/base/common/path'; +import * as resources from 'vs/base/common/resources'; import { ITextModel } from 'vs/editor/common/model'; import { URI } from 'vs/base/common/uri'; import { ThrottledDelayer } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IOutputChannelModel, AbstractFileOutputChannelModel, IOutputChannelModelService, AsbtractOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel'; +import { IOutputChannelModel, AbstractFileOutputChannelModel, IOutputChannelModelService, AsbtractOutputChannelModelService, BufferredOutputChannel } from 'vs/workbench/services/output/common/outputChannelModel'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { toLocalISOString } from 'vs/base/common/date'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; let watchingOutputDir = false; let callbacks: ((eventType: string, fileName?: string) => void)[] = []; @@ -55,15 +57,13 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement id: string, modelUri: URI, mimeType: string, - @IWindowService windowService: IWindowService, - @IEnvironmentService environmentService: IEnvironmentService, + file: URI, @IFileService fileService: IFileService, @IModelService modelService: IModelService, @IModeService modeService: IModeService, @ILogService logService: ILogService ) { - const outputDir = join(environmentService.logsPath, `output_${windowService.getCurrentWindowId()}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - super(modelUri, mimeType, URI.file(join(outputDir, `${id}.log`)), fileService, modelService, modeService); + super(modelUri, mimeType, file, fileService, modelService, modeService); this.appendedMessage = ''; this.loadingFromFileInProgress = false; @@ -159,32 +159,93 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement } } +class DelegatedOutputChannelModel extends Disposable implements IOutputChannelModel { + + private readonly _onDidAppendedContent: Emitter = this._register(new Emitter()); + readonly onDidAppendedContent: Event = this._onDidAppendedContent.event; + + private readonly _onDispose: Emitter = this._register(new Emitter()); + readonly onDispose: Event = this._onDispose.event; + + private readonly outputChannelModel: Promise; + + constructor( + id: string, + modelUri: URI, + mimeType: string, + outputDir: Promise, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { + super(); + this.outputChannelModel = this.createOutputChannelModel(id, modelUri, mimeType, outputDir); + } + + private async createOutputChannelModel(id: string, modelUri: URI, mimeType: string, outputDirPromise: Promise): Promise { + let outputChannelModel: IOutputChannelModel; + try { + const outputDir = await outputDirPromise; + const file = resources.joinPath(outputDir, `${id}.log`); + outputChannelModel = this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, mimeType, file); + } catch (e) { + // Do not crash if spdlog rotating logger cannot be loaded (workaround for https://github.com/Microsoft/vscode/issues/47883) + this.logService.error(e); + /* __GDPR__ + "output.channel.creation.error" : {} + */ + this.telemetryService.publicLog('output.channel.creation.error'); + outputChannelModel = this.instantiationService.createInstance(BufferredOutputChannel, modelUri, mimeType); + } + this._register(outputChannelModel); + outputChannelModel.onDidAppendedContent(() => this._onDidAppendedContent.fire()); + outputChannelModel.onDispose(() => this._onDispose.fire()); + return outputChannelModel; + } + + append(output: string): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output)); + } + + update(): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.update()); + } + + loadModel(): Promise { + return this.outputChannelModel.then(outputChannelModel => outputChannelModel.loadModel()); + } + + clear(till?: number): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear(till)); + } + +} + export class OutputChannelModelService extends AsbtractOutputChannelModelService implements IOutputChannelModelService { _serviceBrand: any; constructor( @IInstantiationService instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWindowService private readonly windowService: IWindowService, + @IFileService private readonly fileService: IFileService ) { super(instantiationService); } createOutputChannelModel(id: string, modelUri: URI, mimeType: string, file?: URI): IOutputChannelModel { - if (!file) { - try { - return this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, mimeType); - } catch (e) { - // Do not crash if spdlog rotating logger cannot be loaded (workaround for https://github.com/Microsoft/vscode/issues/47883) - this.logService.error(e); - /* __GDPR__ - "output.channel.creation.error" : {} - */ - this.telemetryService.publicLog('output.channel.creation.error'); - } + return file ? super.createOutputChannelModel(id, modelUri, mimeType, file) : + this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, mimeType, this.outputDir); + } + + private _outputDir: Promise | null; + private get outputDir(): Promise { + if (!this._outputDir) { + const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.windowService.getCurrentWindowId()}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`)); + this._outputDir = this.fileService.createFolder(outputDir).then(() => outputDir); } - return super.createOutputChannelModel(id, modelUri, mimeType, file); + return this._outputDir; } }