Skip to content

Commit

Permalink
#57618: ExtHost: Implement writable output channels using spdlog
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Sep 4, 2018
1 parent a030947 commit 5dbd093
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 111 deletions.
24 changes: 24 additions & 0 deletions src/vs/platform/output/node/outputAppender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RotatingLogger } from 'spdlog';

export class OutputAppender {

private appender: RotatingLogger;

constructor(name: string, file: string) {
this.appender = new RotatingLogger(name, file, 1024 * 1024 * 30, 1);
this.appender.clearFormatters();
}

append(content: string): void {
this.appender.critical(content);
}

flush(): void {
this.appender.flush();
}

}
43 changes: 28 additions & 15 deletions src/vs/workbench/api/electron-browser/mainThreadOutputService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { MainThreadOutputServiceShape, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { UriComponents, URI } from 'vs/base/common/uri';

@extHostNamedCustomer(MainContext.MainThreadOutputService)
export class MainThreadOutputService implements MainThreadOutputServiceShape {
Expand All @@ -34,28 +35,33 @@ export class MainThreadOutputService implements MainThreadOutputServiceShape {
// Leave all the existing channels intact (e.g. might help with troubleshooting)
}

public $append(channelId: string, label: string, value: string): TPromise<void> {
this._getChannel(channelId, label).append(value);
return undefined;
public $register(id: string, label: string, file?: UriComponents): TPromise<void> {
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id, label, file: file ? URI.revive(file) : null, log: false });
return TPromise.as(null);
}

public $clear(channelId: string, label: string): TPromise<void> {
this._getChannel(channelId, label).clear();
public $append(channelId: string, value: string): TPromise<void> {
const channel = this._getChannel(channelId);
if (channel) {
channel.append(value);
}
return undefined;
}

public $reveal(channelId: string, label: string, preserveFocus: boolean): TPromise<void> {
const channel = this._getChannel(channelId, label);
this._outputService.showChannel(channel.id, preserveFocus);
public $clear(channelId: string): TPromise<void> {
const channel = this._getChannel(channelId);
if (channel) {
channel.clear();
}
return undefined;
}

private _getChannel(channelId: string, label: string): IOutputChannel {
if (!Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(channelId)) {
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel(channelId, label);
public $reveal(channelId: string, preserveFocus: boolean): TPromise<void> {
const channel = this._getChannel(channelId);
if (channel) {
this._outputService.showChannel(channel.id, preserveFocus);
}

return this._outputService.getChannel(channelId);
return undefined;
}

public $close(channelId: string): TPromise<void> {
Expand All @@ -67,8 +73,15 @@ export class MainThreadOutputService implements MainThreadOutputServiceShape {
return undefined;
}

public $dispose(channelId: string, label: string): TPromise<void> {
this._getChannel(channelId, label).dispose();
public $dispose(channelId: string): TPromise<void> {
const channel = this._getChannel(channelId);
if (channel) {
channel.dispose();
}
return undefined;
}

private _getChannel(channelId: string): IOutputChannel {
return this._outputService.getChannel(channelId);
}
}
5 changes: 4 additions & 1 deletion src/vs/workbench/api/node/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview';
import { ExtHostComments } from './extHostComments';
import { ExtHostSearch } from './extHostSearch';
import { ExtHostUrls } from './extHostUrls';
import { toLocalISOString } from 'vs/base/common/date';
import { posix } from 'path';

export interface IExtensionApiFactory {
(extension: IExtensionDescription): typeof vscode;
Expand Down Expand Up @@ -136,7 +138,8 @@ export function createApiFactory(
const extHostMessageService = new ExtHostMessageService(rpcProtocol);
const extHostDialogs = new ExtHostDialogs(rpcProtocol);
const extHostStatusBar = new ExtHostStatusBar(rpcProtocol);
const extHostOutputService = new ExtHostOutputService(rpcProtocol);
const outputDir = posix.join(initData.logsPath, `output_logging_${initData.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`);
const extHostOutputService = new ExtHostOutputService(outputDir, rpcProtocol);
const extHostLanguages = new ExtHostLanguages(rpcProtocol);

// Register API-ish commands
Expand Down
9 changes: 5 additions & 4 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,12 @@ export interface MainThreadMessageServiceShape extends IDisposable {
}

export interface MainThreadOutputServiceShape extends IDisposable {
$append(channelId: string, label: string, value: string): TPromise<void>;
$clear(channelId: string, label: string): TPromise<void>;
$dispose(channelId: string, label: string): TPromise<void>;
$reveal(channelId: string, label: string, preserveFocus: boolean): TPromise<void>;
$register(channelId: string, label: string, file?: UriComponents): TPromise<void>;
$append(channelId: string, value: string): TPromise<void>;
$clear(channelId: string): TPromise<void>;
$reveal(channelId: string, preserveFocus: boolean): TPromise<void>;
$close(channelId: string): TPromise<void>;
$dispose(channelId: string): TPromise<void>;
}

export interface MainThreadProgressShape extends IDisposable {
Expand Down
87 changes: 59 additions & 28 deletions src/vs/workbench/api/node/extHostOutputService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,32 @@

import { MainContext, MainThreadOutputServiceShape, IMainContext } from './extHost.protocol';
import * as vscode from 'vscode';
import { URI } from 'vs/base/common/uri';
import { posix } from 'path';
import { OutputAppender } from 'vs/platform/output/node/outputAppender';
import { TPromise } from 'vs/base/common/winjs.base';

export class ExtHostOutputChannel implements vscode.OutputChannel {
export abstract class AbstractExtHostOutputChannel implements vscode.OutputChannel {

private static _idPool = 1;

private _proxy: MainThreadOutputServiceShape;
private _name: string;
private _id: string;
protected readonly _id: string;
private readonly _name: string;
protected readonly _proxy: MainThreadOutputServiceShape;
protected _registerationPromise: TPromise<void> = TPromise.as(null);
private _disposed: boolean;

constructor(name: string, proxy: MainThreadOutputServiceShape) {
this._id = 'extension-output-#' + (AbstractExtHostOutputChannel._idPool++);
this._name = name;
this._id = 'extension-output-#' + (ExtHostOutputChannel._idPool++);
this._proxy = proxy;
}

get name(): string {
return this._name;
}

dispose(): void {
if (!this._disposed) {
this._proxy.$dispose(this._id, this._name).then(() => {
this._disposed = true;
});
}
}

append(value: string): void {
this.validate();
this._proxy.$append(this._id, this._name, value);
}
abstract append(value: string): void;

appendLine(value: string): void {
this.validate();
Expand All @@ -46,44 +40,81 @@ export class ExtHostOutputChannel implements vscode.OutputChannel {

clear(): void {
this.validate();
this._proxy.$clear(this._id, this._name);
this._registerationPromise.then(() => this._proxy.$clear(this._id));
}

show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void {
this.validate();
if (typeof columnOrPreserveFocus === 'boolean') {
preserveFocus = columnOrPreserveFocus;
}

this._proxy.$reveal(this._id, this._name, preserveFocus);
this._registerationPromise.then(() => this._proxy.$reveal(this._id, typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus));
}

hide(): void {
this.validate();
this._proxy.$close(this._id);
this._registerationPromise.then(() => this._proxy.$close(this._id));
}

private validate(): void {
protected validate(): void {
if (this._disposed) {
throw new Error('Channel has been closed');
}
}

dispose(): void {
if (!this._disposed) {
this._registerationPromise
.then(() => this._proxy.$dispose(this._id))
.then(() => this._disposed = true);
}
}
}

export class ExtHostOutputChannel extends AbstractExtHostOutputChannel {

constructor(name: string, proxy: MainThreadOutputServiceShape) {
super(name, proxy);
this._registerationPromise = proxy.$register(this._id, name);
}

append(value: string): void {
this.validate();
this._registerationPromise.then(() => this._proxy.$append(this._id, value));
}
}

export class ExtHostLoggingOutputChannel extends AbstractExtHostOutputChannel {

private _appender: OutputAppender;

constructor(name: string, outputDir: string, proxy: MainThreadOutputServiceShape) {
super(name, proxy);
const file = URI.file(posix.join(outputDir, `${this._id}.log`));
this._appender = new OutputAppender(this._id, file.fsPath);
this._registerationPromise = proxy.$register(this._id, this.name, file);

}

append(value: string): void {
this.validate();
this._appender.append(value);
}
}

export class ExtHostOutputService {

private _proxy: MainThreadOutputServiceShape;
private _outputDir: string;

constructor(mainContext: IMainContext) {
constructor(outputDir: string, mainContext: IMainContext) {
this._outputDir = outputDir;
this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService);
}

createOutputChannel(name: string): vscode.OutputChannel {
createOutputChannel(name: string, logging?: boolean): vscode.OutputChannel {
name = name.trim();
if (!name) {
throw new Error('illegal argument `name`. must not be falsy');
} else {
return new ExtHostOutputChannel(name, this._proxy);
return logging ? new ExtHostLoggingOutputChannel(name, this._outputDir, this._proxy) : new ExtHostOutputChannel(name, this._proxy);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase
workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting);

Registry.as<IOutputChannelRegistry>(OutputExtensions.OutputChannels)
.registerChannel(ExtensionsChannelId, ExtensionsLabel);
.registerChannel({ id: ExtensionsChannelId, label: ExtensionsLabel, log: false });

// Quickopen
Registry.as<IQuickOpenRegistry>(Extensions.Quickopen).registerQuickOpenHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution {
) {
super();
let outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels);
outputChannelRegistry.registerChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Log (Main)"), URI.file(join(this.environmentService.logsPath, `main.log`)));
outputChannelRegistry.registerChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Log (Shared)"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`)));
outputChannelRegistry.registerChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Log (Window)"), URI.file(join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`)));
outputChannelRegistry.registerChannel(Constants.extHostLogChannelId, nls.localize('extensionsLog', "Log (Extension Host)"), URI.file(join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`)));
outputChannelRegistry.registerChannel({ id: Constants.mainLogChannelId, label: nls.localize('mainLog', "Log (Main)"), file: URI.file(join(this.environmentService.logsPath, `main.log`)), log: true });
outputChannelRegistry.registerChannel({ id: Constants.sharedLogChannelId, label: nls.localize('sharedLog', "Log (Shared)"), file: URI.file(join(this.environmentService.logsPath, `sharedprocess.log`)), log: true });
outputChannelRegistry.registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Log (Window)"), file: URI.file(join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`)), log: true });
outputChannelRegistry.registerChannel({ id: Constants.extHostLogChannelId, label: nls.localize('extensionsLog', "Log (Extension Host)"), file: URI.file(join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`)), log: true });

const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
const devCategory = nls.localize('developer', "Developer");
Expand Down
40 changes: 19 additions & 21 deletions src/vs/workbench/parts/output/browser/outputActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { IAction, Action } from 'vs/base/common/actions';
import { IOutputService, OUTPUT_PANEL_ID, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannelIdentifier, COMMAND_OPEN_LOG_VIEWER } from 'vs/workbench/parts/output/common/output';
import { IOutputService, OUTPUT_PANEL_ID, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannelDescriptor, COMMAND_OPEN_LOG_VIEWER } from 'vs/workbench/parts/output/common/output';
import { SelectActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
Expand All @@ -20,7 +20,6 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
import { Registry } from 'vs/platform/registry/common/platform';
import { groupBy } from 'vs/base/common/arrays';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { URI } from 'vs/base/common/uri';

export class ToggleOutputAction extends TogglePanelAction {

Expand Down Expand Up @@ -112,8 +111,8 @@ export class SwitchOutputActionItem extends SelectActionItem {

private static readonly SEPARATOR = '─────────';

private normalChannels: IOutputChannelIdentifier[];
private fileChannels: IOutputChannelIdentifier[];
private outputChannels: IOutputChannelDescriptor[];
private logChannels: IOutputChannelDescriptor[];

constructor(
action: IAction,
Expand All @@ -133,30 +132,30 @@ export class SwitchOutputActionItem extends SelectActionItem {
}

protected getActionContext(option: string, index: number): string {
const channel = index < this.normalChannels.length ? this.normalChannels[index] : this.fileChannels[index - this.normalChannels.length - 1];
const channel = index < this.outputChannels.length ? this.outputChannels[index] : this.logChannels[index - this.outputChannels.length - 1];
return channel ? channel.id : option;
}

private updateOtions(selectedChannel: string): void {
const groups = groupBy(this.outputService.getChannels(), (c1: IOutputChannelIdentifier, c2: IOutputChannelIdentifier) => {
if (!c1.file && c2.file) {
const groups = groupBy(this.outputService.getChannelDescriptors(), (c1: IOutputChannelDescriptor, c2: IOutputChannelDescriptor) => {
if (!c1.log && c2.log) {
return -1;
}
if (c1.file && !c2.file) {
if (c1.log && !c2.log) {
return 1;
}
return 0;
});
this.normalChannels = groups[0] || [];
this.fileChannels = groups[1] || [];
const showSeparator = this.normalChannels.length && this.fileChannels.length;
const separatorIndex = showSeparator ? this.normalChannels.length : -1;
const options: string[] = [...this.normalChannels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionItem.SEPARATOR] : []), ...this.fileChannels.map(c => c.label)];
this.outputChannels = groups[0] || [];
this.logChannels = groups[1] || [];
const showSeparator = this.outputChannels.length && this.logChannels.length;
const separatorIndex = showSeparator ? this.outputChannels.length : -1;
const options: string[] = [...this.outputChannels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionItem.SEPARATOR] : []), ...this.logChannels.map(c => c.label)];
let selected = 0;
if (selectedChannel) {
selected = this.normalChannels.map(c => c.id).indexOf(selectedChannel);
selected = this.outputChannels.map(c => c.id).indexOf(selectedChannel);
if (selected === -1) {
selected = separatorIndex + 1 + this.fileChannels.map(c => c.id).indexOf(selectedChannel);
selected = separatorIndex + 1 + this.logChannels.map(c => c.id).indexOf(selectedChannel);
}
}
this.setOptions(options, Math.max(0, selected), separatorIndex !== -1 ? separatorIndex : void 0);
Expand All @@ -180,17 +179,16 @@ export class OpenLogOutputFile extends Action {
}

private update(): void {
const logFile = this.getActiveLogChannelFile();
this.enabled = !!logFile;
this.enabled = this.isLogChannel();
}

public run(): TPromise<any> {
return this.commandService.executeCommand(COMMAND_OPEN_LOG_VIEWER, this.getActiveLogChannelFile());
return this.commandService.executeCommand(COMMAND_OPEN_LOG_VIEWER, this.isLogChannel());
}

private getActiveLogChannelFile(): URI {
private isLogChannel(): boolean {
const channel = this.outputService.getActiveChannel();
const identifier = channel ? this.outputService.getChannels().filter(c => c.id === channel.id)[0] : null;
return identifier ? identifier.file : null;
const descriptor = channel ? this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0] : null;
return descriptor && descriptor.log;
}
}
Loading

0 comments on commit 5dbd093

Please sign in to comment.