diff --git a/package.json b/package.json index 7f683262225..58405fbb67d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "node-pty": "0.7.4", "nsfw": "1.0.16", "semver": "4.3.6", - "spdlog": "0.3.7", + "spdlog": "0.5.0", "sudo-prompt": "^8.0.0", "v8-inspect-profiler": "^0.0.7", "vscode-chokidar": "1.6.2", @@ -129,4 +129,4 @@ "windows-mutex": "^0.2.0", "windows-process-tree": "0.1.6" } -} +} \ No newline at end of file diff --git a/src/typings/spdlog.d.ts b/src/typings/spdlog.d.ts index ef93a9cf800..5417fdd4212 100644 --- a/src/typings/spdlog.d.ts +++ b/src/typings/spdlog.d.ts @@ -28,6 +28,7 @@ declare module 'spdlog' { error(message: string); critical(message: string); setLevel(level: number); + clearFormatters(); flush(): void; drop(): void; } diff --git a/src/vs/workbench/parts/output/browser/output.contribution.ts b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts similarity index 97% rename from src/vs/workbench/parts/output/browser/output.contribution.ts rename to src/vs/workbench/parts/output/electron-browser/output.contribution.ts index 2809a152fc1..705648e1f4c 100644 --- a/src/vs/workbench/parts/output/browser/output.contribution.ts +++ b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts @@ -11,7 +11,7 @@ import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/ import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { OutputService } from 'vs/workbench/parts/output/browser/outputServices'; +import { OutputService } from 'vs/workbench/parts/output/electron-browser/outputServices'; import { ToggleOutputAction, ClearOutputAction } from 'vs/workbench/parts/output/browser/outputActions'; import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_PANEL_ID, IOutputService, CONTEXT_IN_OUTPUT } from 'vs/workbench/parts/output/common/output'; import { PanelRegistry, Extensions, PanelDescriptor } from 'vs/workbench/browser/panel'; diff --git a/src/vs/workbench/parts/output/browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts similarity index 77% rename from src/vs/workbench/parts/output/browser/outputServices.ts rename to src/vs/workbench/parts/output/electron-browser/outputServices.ts index d5ddb7aee3b..e56069a3098 100644 --- a/src/vs/workbench/parts/output/browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import nls = require('vs/nls'); +import * as nls from 'vs/nls'; +import * as fs from 'fs'; +import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; -import strings = require('vs/base/common/strings'); import Event, { Emitter } from 'vs/base/common/event'; -import { binarySearch } from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, MAX_OUTPUT_LENGTH, OUTPUT_SCHEME, OUTPUT_MIME } from 'vs/workbench/parts/output/common/output'; +import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME } 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'; @@ -30,225 +30,12 @@ import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; import { IPanel } from 'vs/workbench/common/panel'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { RotatingLogger } from 'spdlog'; +import { toLocalISOString } from 'vs/base/common/date'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; -export interface IOutputDelta { - readonly value: string; - readonly id: number; - readonly append?: boolean; -} - -export 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): IOutputDelta { - 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, append: true }; - } else { - const value = strings.removeAnsiEscapeCodes(this.data.join('')); - return { value, id }; - } - } -} - -abstract class OutputChannel extends Disposable implements IOutputChannel { - - protected _onDidChange: Emitter = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - - protected _onDidClear: Emitter = new Emitter(); - readonly onDidClear: Event = this._onDidClear.event; - - protected _onDispose: Emitter = new Emitter(); - readonly onDispose: Event = this._onDispose.event; - - scrollLock: boolean = false; - - constructor(private readonly oputChannelIdentifier: IOutputChannelIdentifier) { - super(); - } - - get id(): string { - return this.oputChannelIdentifier.id; - } - - get label(): string { - return this.oputChannelIdentifier.label; - } - - show(): TPromise { return TPromise.as(null); } - hide(): void { } - append(output: string) { /** noop */ } - getOutputDelta(id?: number): TPromise { return TPromise.as(null); } - clear(): void { } - - dispose(): void { - this._onDispose.fire(); - super.dispose(); - } - -} - -class BufferredOutputChannel extends OutputChannel implements IOutputChannel { - - private bufferredContent: BufferedContent = new BufferedContent(); - - append(output: string) { - this.bufferredContent.append(output); - this._onDidChange.fire(); - } - - getOutputDelta(id?: number): TPromise { - return TPromise.as(this.bufferredContent.getDelta(id)); - } - - clear(): void { - this.bufferredContent.clear(); - this._onDidClear.fire(); - } -} - -class FileOutputChannel extends OutputChannel implements IOutputChannel { - - private readonly file: URI; - private disposables: IDisposable[] = []; - private shown: boolean = false; - - private contentResolver: TPromise; - private startOffset: number; - private endOffset: number; - - constructor( - outputChannelIdentifier: IOutputChannelIdentifier, - @IFileService private fileService: IFileService - ) { - super(outputChannelIdentifier); - this.file = outputChannelIdentifier.file; - this.startOffset = 0; - this.endOffset = 0; - } - - show(): TPromise { - if (!this.shown) { - this.shown = true; - this.watch(); - return this.resolve() - .then(content => { - if (this.endOffset !== content.length) { - this._onDidChange.fire(); - } - }); - } - return TPromise.as(null); - } - - hide(): void { - if (this.shown) { - this.shown = false; - this.unwatch(); - } - this.contentResolver = null; - } - - getOutputDelta(previousId?: number): TPromise { - if (!this.shown) { - // Do not return any content when not shown - return TPromise.as(null); - } - - return this.resolve() - .then(content => { - const startOffset = previousId !== void 0 ? previousId : this.startOffset; - this.endOffset = content.length; - if (this.startOffset === this.endOffset) { - // Content cleared - return { append: false, id: this.endOffset, value: '' }; - } - if (startOffset === this.endOffset) { - // Content not changed - return { append: true, id: this.endOffset, value: '' }; - } - if (startOffset > 0 && startOffset < this.endOffset) { - // Delta - const value = content.substring(startOffset, this.endOffset); - return { append: true, value, id: this.endOffset }; - } - // Replace - return { append: false, value: content, id: this.endOffset }; - }); - } - - clear(): void { - this.startOffset = this.endOffset; - this._onDidClear.fire(); - } - - private resolve(): TPromise { - if (!this.contentResolver) { - this.contentResolver = this.fileService.resolveContent(this.file) - .then(content => content.value); - } - return this.contentResolver; - } - - private watch(): void { - this.fileService.watchFileChanges(this.file); - this.disposables.push(this.fileService.onFileChanges(changes => { - if (changes.contains(this.file, FileChangeType.UPDATED)) { - this.contentResolver = null; - this._onDidChange.fire(); - } - })); - } - - private unwatch(): void { - this.fileService.unwatchFileChanges(this.file); - this.disposables = dispose(this.disposables); - } - - dispose(): void { - this.hide(); - super.dispose(); - } -} - export class OutputService implements IOutputService, ITextModelContentProvider { public _serviceBrand: any; @@ -273,6 +60,7 @@ export class OutputService implements IOutputService, ITextModelContentProvider @IModeService private modeService: IModeService, @ITextModelService textModelResolverService: ITextModelService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEnvironmentService private environmentService: IEnvironmentService ) { const channels = this.getChannels(); this.activeChannelId = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, channels && channels.length > 0 ? channels[0].id : null); @@ -329,7 +117,9 @@ export class OutputService implements IOutputService, ITextModelContentProvider private createChannel(id: string): OutputChannel { const channelDisposables = []; const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); - const channel = channelData && channelData.file ? this.instantiationService.createInstance(FileOutputChannel, channelData) : this.instantiationService.createInstance(BufferredOutputChannel, { id: id, label: '' }); + const file = channelData && channelData.file ? channelData.file : URI.file(paths.join(this.environmentService.userDataPath, 'outputs', toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''), `${id}.output.log`)); + const channel = channelData && channelData.file ? this.instantiationService.createInstance(OutputChannel, channelData) : + this.instantiationService.createInstance(WritableOutputChannel, { id, label: channelData ? channelData.label : '', file }); channelDisposables.push(this.instantiationService.createInstance(ChannelModelUpdater, channel)); channel.onDidChange(() => this._onDidChannelContentChange.fire(id), channelDisposables); channel.onDispose(() => { @@ -348,6 +138,7 @@ export class OutputService implements IOutputService, ITextModelContentProvider return channel; } + private isChannelShown(channelId: string): boolean { const panel = this.panelService.getActivePanel(); return panel && panel.getId() === OUTPUT_PANEL_ID && this.activeChannelId === channelId; @@ -402,6 +193,205 @@ export class OutputService implements IOutputService, ITextModelContentProvider } } +export interface IOutputDelta { + readonly value: string; + readonly id: number; + readonly append?: boolean; +} + +class OutputFileListener extends Disposable { + + private _onDidChange: Emitter = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private disposables: IDisposable[] = []; + + constructor( + private readonly file: URI, + private fileService: IFileService + ) { + super(); + } + + watch(): void { + this.fileService.watchFileChanges(this.file); + this.disposables.push(this.fileService.onFileChanges(changes => { + if (changes.contains(this.file, FileChangeType.UPDATED)) { + this._onDidChange.fire(); + } + })); + } + + loadContent(from: number): TPromise { + return this.fileService.resolveContent(this.file) + .then(({ value }) => value.substring(from)); + } + + unwatch(): void { + this.fileService.unwatchFileChanges(this.file); + this.disposables = dispose(this.disposables); + } + + dispose(): void { + this.unwatch(); + super.dispose(); + } +} + +class OutputChannel extends Disposable implements IOutputChannel { + + protected _onDidChange: Emitter = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + protected _onDidClear: Emitter = new Emitter(); + readonly onDidClear: Event = this._onDidClear.event; + + protected _onDispose: Emitter = new Emitter(); + readonly onDispose: Event = this._onDispose.event; + + scrollLock: boolean = false; + + protected readonly file: URI; + private disposables: IDisposable[] = []; + protected shown: boolean = false; + + private contentResolver: TPromise; + private startOffset: number; + private endOffset: number; + + constructor( + private readonly outputChannelIdentifier: IOutputChannelIdentifier, + @IFileService protected fileService: IFileService + ) { + super(); + this.file = outputChannelIdentifier.file; + this.startOffset = 0; + this.endOffset = 0; + } + + get id(): string { + return this.outputChannelIdentifier.id; + } + + get label(): string { + return this.outputChannelIdentifier.label; + } + + show(): TPromise { + if (!this.shown) { + this.shown = true; + this.watch(); + return this.resolve() as TPromise; + } + return TPromise.as(null); + } + + hide(): void { + if (this.shown) { + this.shown = false; + this.unwatch(); + this.contentResolver = null; + } + } + + append(message: string): void { + throw new Error(nls.localize('appendNotSupported', "Append is not supported on File output channel")); + } + + getOutputDelta(previousId?: number): TPromise { + return this.resolve() + .then(content => { + const startOffset = previousId !== void 0 ? previousId : this.startOffset; + if (this.startOffset === this.endOffset) { + // Content cleared + return { append: false, id: this.endOffset, value: '' }; + } + if (startOffset === this.endOffset) { + // Content not changed + return { append: true, id: this.endOffset, value: '' }; + } + if (startOffset > 0 && startOffset < this.endOffset) { + // Delta + const value = content.substring(startOffset, this.endOffset); + return { append: true, value, id: this.endOffset }; + } + // Replace + return { append: false, value: content, id: this.endOffset }; + }); + } + + clear(): void { + this.startOffset = this.endOffset; + this._onDidClear.fire(); + } + + private resolve(): TPromise { + if (!this.contentResolver) { + this.contentResolver = this.fileService.resolveContent(this.file) + .then(result => { + const content = result.value; + if (this.endOffset !== content.length) { + this.endOffset = content.length; + this._onDidChange.fire(); + } + return content; + }); + } + return this.contentResolver; + } + + private watch(): void { + this.fileService.watchFileChanges(this.file); + this.disposables.push(this.fileService.onFileChanges(changes => { + if (changes.contains(this.file, FileChangeType.UPDATED)) { + this.contentResolver = null; + this._onDidChange.fire(); + } + })); + } + + private unwatch(): void { + this.fileService.unwatchFileChanges(this.file); + this.disposables = dispose(this.disposables); + } + + dispose(): void { + this.hide(); + this._onDispose.fire(); + super.dispose(); + } +} + +class WritableOutputChannel extends OutputChannel implements IOutputChannel { + + private outputWriter: RotatingLogger; + private flushScheduler: RunOnceScheduler; + + constructor( + outputChannelIdentifier: IOutputChannelIdentifier, + @IFileService fileService: IFileService + ) { + super(outputChannelIdentifier, fileService); + this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, 1024 * 1024 * 5, 1); + this.outputWriter.clearFormatters(); + this.flushScheduler = new RunOnceScheduler(() => this.outputWriter.flush(), 300); + } + + append(message: string): void { + this.outputWriter.critical(message); + if (this.shown && !this.flushScheduler.isScheduled()) { + this.flushScheduler.schedule(); + } + } + + show(): TPromise { + if (!this.flushScheduler.isScheduled()) { + this.flushScheduler.schedule(); + } + return super.show(); + } +} + class ChannelModelUpdater extends Disposable { private updateInProgress: boolean = false; @@ -418,6 +408,7 @@ class ChannelModelUpdater extends Disposable { this._register(channel.onDidChange(() => this.onDidChange())); this._register(channel.onDidClear(() => this.onDidClear())); this._register(toDisposable(() => this.modelUpdater.cancel())); + this._register(this.modelService.onModelRemoved(this.onModelRemoved, this)); } private onDidChange(): void { @@ -434,10 +425,10 @@ class ChannelModelUpdater extends Disposable { } private doUpdate(): void { - this.channel.getOutputDelta(this.lastReadId) - .then(delta => { - const model = this.getModel(this.channel.id); - if (model && !model.isDisposed()) { + const model = this.getModel(this.channel.id); + if (model && !model.isDisposed()) { + this.channel.getOutputDelta(this.lastReadId) + .then(delta => { if (delta) { if (delta.append) { const lastLine = model.getLineCount(); @@ -451,12 +442,20 @@ class ChannelModelUpdater extends Disposable { (this.panelService.getActivePanel()).revealLastLine(); } } - } - this.updateInProgress = false; - }, () => this.updateInProgress = false); + this.updateInProgress = false; + }, () => this.updateInProgress = false); + } else { + this.updateInProgress = false; + } } private getModel(channel: string): IModel { return this.modelService.getModel(URI.from({ scheme: OUTPUT_SCHEME, path: channel })); } + + private onModelRemoved(model: IModel): void { + if (model.uri.fsPath === this.channel.id) { + this.lastReadId = void 0; + } + } } diff --git a/src/vs/workbench/parts/output/test/bufferedContent.test.ts b/src/vs/workbench/parts/output/test/bufferedContent.test.ts deleted file mode 100644 index ae4f7623f55..00000000000 --- a/src/vs/workbench/parts/output/test/bufferedContent.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { BufferedContent } from 'vs/workbench/parts/output/browser/outputServices'; - -suite('Workbench - Output Buffered Content', () => { - - test('Buffered Content - Simple', () => { - const bufferedContent = new BufferedContent(); - bufferedContent.append('first'); - bufferedContent.append('second'); - bufferedContent.append('third'); - const delta = bufferedContent.getDelta(); - assert.equal(bufferedContent.getDelta().value, 'firstsecondthird'); - bufferedContent.clear(); - assert.equal(bufferedContent.getDelta().value, ''); - assert.equal(bufferedContent.getDelta(delta.id).value, ''); - }); - - test('Buffered Content - Appending Output', () => { - const bufferedContent = new BufferedContent(); - bufferedContent.append('first'); - const firstDelta = bufferedContent.getDelta(); - bufferedContent.append('second'); - bufferedContent.append('third'); - const secondDelta = bufferedContent.getDelta(firstDelta.id); - assert.equal(secondDelta.append, true); - assert.equal(secondDelta.value, 'secondthird'); - bufferedContent.append('fourth'); - bufferedContent.append('fifth'); - assert.equal(bufferedContent.getDelta(firstDelta.id).value, 'secondthirdfourthfifth'); - assert.equal(bufferedContent.getDelta(secondDelta.id).value, 'fourthfifth'); - }); - - test('Buffered Content - Lots of Output', function () { - this.timeout(10000); - const bufferedContent = new BufferedContent(); - bufferedContent.append('first line'); - const firstDelta = bufferedContent.getDelta(); - let longString = ''; - for (let i = 0; i < 5000; i++) { - bufferedContent.append(i.toString()); - longString += i.toString(); - } - const secondDelta = bufferedContent.getDelta(firstDelta.id); - assert.equal(secondDelta.append, true); - assert.equal(secondDelta.value.substr(secondDelta.value.length - 4), '4999'); - longString = longString + longString + longString + longString; - bufferedContent.append(longString); - bufferedContent.append(longString); - const thirdDelta = bufferedContent.getDelta(firstDelta.id); - assert.equal(!!thirdDelta.append, true); - assert.equal(thirdDelta.value.substr(thirdDelta.value.length - 4), '4999'); - - bufferedContent.clear(); - assert.equal(bufferedContent.getDelta().value, ''); - }); -}); diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index cfe23986359..b624558e852 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -72,7 +72,7 @@ import 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet'; // ca import 'vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution'; -import 'vs/workbench/parts/output/browser/output.contribution'; +import 'vs/workbench/parts/output/electron-browser/output.contribution'; import 'vs/workbench/parts/output/browser/outputPanel'; // can be packaged separately import 'vs/workbench/parts/terminal/electron-browser/terminal.contribution'; diff --git a/yarn.lock b/yarn.lock index 2e034565a89..f241642dc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5042,9 +5042,9 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spdlog@0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.3.7.tgz#5f068efab0b7c85efa1aaed6eacd3da1d978fe24" +spdlog@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.5.0.tgz#5ec92c34e59f29328f4e19dfab17a1ba51cc0573" dependencies: bindings "^1.3.0" mkdirp "^0.5.1"