diff --git a/src/vs/platform/output/node/outputAppender.ts b/src/vs/platform/output/node/outputAppender.ts index 85566a80a119596a3856079f65eb05922cf82985..a7ffc9cd3d245b24425a99524a08196e6c80ccd3 100644 --- a/src/vs/platform/output/node/outputAppender.ts +++ b/src/vs/platform/output/node/outputAppender.ts @@ -5,27 +5,13 @@ import { createRotatingLogger } from 'vs/platform/log/node/spdlogService'; import { RotatingLogger } from 'spdlog'; -interface Appender { - critical(content: string); - flush(); -} - export class OutputAppender { - private appender: Appender; + private appender: RotatingLogger; constructor(name: string, file: string) { - // Do not crash if logger cannot be loaded - try { - this.appender = createRotatingLogger(name, file, 1024 * 1024 * 30, 1); - (this.appender).clearFormatters(); - } catch (error) { - console.error(error); - this.appender = { - critical() { }, - flush() { }, - }; - } + this.appender = createRotatingLogger(name, file, 1024 * 1024 * 30, 1); + this.appender.clearFormatters(); } append(content: string): void { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index a1c996e7d0bfd7ec6c3b3be7c2ed7cc263d2aa97..4336fff14163d21226589f8fee1ccd631e069370 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -111,7 +111,17 @@ export class ExtHostOutputService { if (!name) { throw new Error('illegal argument `name`. must not be falsy'); } else { - return push ? new ExtHostPushOutputChannel(name, this._proxy) : new ExtHostFileOutputChannel(name, this._outputDir, this._proxy); + if (push) { + return new ExtHostPushOutputChannel(name, this._proxy); + } else { + // Do not crash if logger cannot be created + try { + return new ExtHostFileOutputChannel(name, this._outputDir, this._proxy); + } catch (error) { + console.log(error); + return new ExtHostPushOutputChannel(name, this._proxy); + } + } } } } diff --git a/src/vs/workbench/parts/output/electron-browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts index e3261d78b6c1a227d843e55ddd90e1cb593fd797..1e1abd25288db6e08f630b3c8a01b9138569c5e8 100644 --- a/src/vs/workbench/parts/output/electron-browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; +import * as strings from 'vs/base/common/strings'; import * as extfs from 'vs/base/node/extfs'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; @@ -15,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { IOutputChannelDescriptor, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; +import { IOutputChannelDescriptor, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, MAX_OUTPUT_LENGTH } from 'vs/workbench/parts/output/common/output'; import { OutputPanel } from 'vs/workbench/parts/output/browser/outputPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -34,10 +35,12 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { toLocalISOString } from 'vs/base/common/date'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILogService } from 'vs/platform/log/common/log'; +import { binarySearch } from 'vs/base/common/arrays'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { CancellationToken } from 'vs/base/common/cancellation'; import { OutputAppender } from 'vs/platform/output/node/outputAppender'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -408,6 +411,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo @IEnvironmentService environmentService: IEnvironmentService, @IWindowService windowService: IWindowService, @ILogService private logService: ILogService, + @ITelemetryService private telemetryService: ITelemetryService, @ILifecycleService private lifecycleService: ILifecycleService, @IContextKeyService private contextKeyService: IContextKeyService, ) { @@ -551,7 +555,17 @@ export class OutputService extends Disposable implements IOutputService, ITextMo if (channelData && channelData.file) { return this.instantiationService.createInstance(FileOutputChannel, channelData, uri); } - return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri); + try { + return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri); + } 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 this.instantiationService.createInstance(BufferredOutputChannel, { id, label: channelData ? channelData.label : '' }); + } } private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): Thenable { @@ -625,4 +639,144 @@ export class LogContentProvider { } return channel; } -} \ No newline at end of file +} +// Remove this channel when https://github.com/Microsoft/vscode/issues/47883 is fixed +class BufferredOutputChannel extends Disposable implements OutputChannel { + + readonly id: string; + readonly label: string; + readonly file: URI = null; + scrollLock: boolean = false; + + protected _onDidAppendedContent: Emitter = new Emitter(); + readonly onDidAppendedContent: Event = this._onDidAppendedContent.event; + + private readonly _onDispose: Emitter = new Emitter(); + readonly onDispose: Event = this._onDispose.event; + + private modelUpdater: RunOnceScheduler; + private model: ITextModel; + private readonly bufferredContent: BufferedContent; + private lastReadId: number = void 0; + + constructor( + protected readonly outputChannelIdentifier: IOutputChannelDescriptor, + @IModelService private modelService: IModelService, + @IModeService private modeService: IModeService + ) { + super(); + + this.id = outputChannelIdentifier.id; + this.label = outputChannelIdentifier.label; + + this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300); + this._register(toDisposable(() => this.modelUpdater.cancel())); + + this.bufferredContent = new BufferedContent(); + this._register(toDisposable(() => this.bufferredContent.clear())); + } + + append(output: string) { + this.bufferredContent.append(output); + if (!this.modelUpdater.isScheduled()) { + this.modelUpdater.schedule(); + } + } + + clear(): void { + if (this.modelUpdater.isScheduled()) { + this.modelUpdater.cancel(); + } + if (this.model) { + this.model.setValue(''); + } + this.bufferredContent.clear(); + this.lastReadId = void 0; + } + + loadModel(): TPromise { + const { value, id } = this.bufferredContent.getDelta(this.lastReadId); + if (this.model) { + this.model.setValue(value); + } else { + this.model = this.createModel(value); + } + this.lastReadId = id; + return TPromise.as(this.model); + } + + private createModel(content: string): ITextModel { + const model = this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), URI.from({ scheme: OUTPUT_SCHEME, path: this.id })); + const disposables: IDisposable[] = []; + disposables.push(model.onWillDispose(() => { + this.model = null; + dispose(disposables); + })); + return model; + } + + private updateModel(): void { + if (this.model) { + const { value, id } = this.bufferredContent.getDelta(this.lastReadId); + this.lastReadId = id; + const lastLine = this.model.getLineCount(); + const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine); + this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), value)]); + this._onDidAppendedContent.fire(); + } + } + + dispose(): void { + this._onDispose.fire(); + super.dispose(); + } +} + +class BufferedContent { + + private data: string[] = []; + private dataIds: number[] = []; + private idPool = 0; + private length = 0; + + public append(content: string): void { + this.data.push(content); + this.dataIds.push(++this.idPool); + this.length += content.length; + this.trim(); + } + + public clear(): void { + this.data.length = 0; + this.dataIds.length = 0; + this.length = 0; + } + + private trim(): void { + if (this.length < MAX_OUTPUT_LENGTH * 1.2) { + return; + } + + while (this.length > MAX_OUTPUT_LENGTH) { + this.dataIds.shift(); + const removed = this.data.shift(); + this.length -= removed.length; + } + } + + public getDelta(previousId?: number): { value: string, id: number } { + let idx = -1; + if (previousId !== void 0) { + idx = binarySearch(this.dataIds, previousId, (a, b) => a - b); + } + + const id = this.idPool; + if (idx >= 0) { + const value = strings.removeAnsiEscapeCodes(this.data.slice(idx + 1).join('')); + return { value, id }; + } else { + const value = strings.removeAnsiEscapeCodes(this.data.join('')); + return { value, id }; + } + } +}