From 94199ae124870bd2cd94379c33015f6bc6f1e66c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 13 Jul 2019 09:36:21 -0700 Subject: [PATCH] Introduce logLevel option Part of #1560 --- src/InputHandler.test.ts | 20 +++---- src/InputHandler.ts | 40 +++++++------- src/Terminal.ts | 7 ++- src/common/TestUtils.test.ts | 9 +++- src/common/services/LogService.ts | 78 +++++++++++++++++++++++++++ src/common/services/OptionsService.ts | 1 + src/common/services/Services.d.ts | 13 ++++- typings/xterm.d.ts | 17 ++++++ 8 files changed, 148 insertions(+), 37 deletions(-) create mode 100644 src/common/services/LogService.ts diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 2b274811..53d0bc49 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService, MockLogService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; function getCursor(term: TestTerminal): number[] { @@ -31,7 +31,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); @@ -50,7 +50,7 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const terminal = new MockInputHandlingTerminal(); - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); const collect = ' '; inputHandler.setCursorStyle(Params.fromArray([0]), collect); @@ -93,7 +93,7 @@ describe('InputHandler', () => { const terminal = new MockInputHandlingTerminal(); const collect = '?'; terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); // Set bracketed paste mode inputHandler.setMode(Params.fromArray([2004]), collect); assert.equal(terminal.bracketedPasteMode, true); @@ -112,7 +112,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -150,7 +150,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -191,7 +191,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -220,7 +220,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -355,7 +355,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -370,7 +370,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockOptionsService()); + handler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 9b09ad83..da94209d 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; /** @@ -44,6 +44,7 @@ class DECRQSS implements IDcsHandler { constructor( private _bufferService: IBufferService, private _coreService: ICoreService, + private _logService: ILogService, private _optionsService: IOptionsService ) { } @@ -78,8 +79,7 @@ class DECRQSS implements IDcsHandler { return this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); default: // invalid: DCS 0 $ r Pt ST (xterm) - // TODO: Move this into a log service - console.error('Unknown DCS $q %s', data); + this._logService.error('Unknown DCS $q %s', data); this._coreService.triggerDataEvent(`${C0.ESC}P0$r${C0.ESC}\\`); } } @@ -131,6 +131,7 @@ export class InputHandler extends Disposable implements IInputHandler { protected _terminal: IInputHandlingTerminal, private _bufferService: IBufferService, private _coreService: ICoreService, + private _logService: ILogService, private _optionsService: IOptionsService, private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { @@ -142,16 +143,16 @@ export class InputHandler extends Disposable implements IInputHandler { * custom fallback handlers */ this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - this._terminal.error('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); + this._logService.error('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); }); this._parser.setEscHandlerFallback((collect: string, flag: number) => { - this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); + this._logService.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); }); this._parser.setExecuteHandlerFallback((code: number) => { - this._terminal.error('Unknown EXECUTE code: ', { code }); + this._logService.error('Unknown EXECUTE code: ', { code }); }); this._parser.setOscHandlerFallback((identifier: number, data: string) => { - this._terminal.error('Unknown OSC code: ', { identifier, data }); + this._logService.error('Unknown OSC code: ', { identifier, data }); }); /** @@ -293,14 +294,14 @@ export class InputHandler extends Disposable implements IInputHandler { * error handler */ this._parser.setErrorHandler((state: IParsingState) => { - this._terminal.error('Parsing error: ', state); + this._logService.error('Parsing error: ', state); return state; }); /** * DCS handler */ - this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._optionsService)); + this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); } public dispose(): void { @@ -323,10 +324,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cursorStartX = buffer.x; const cursorStartY = buffer.y; - // TODO: Consolidate debug/logging #1560 - if ((this._terminal).debug) { - this._terminal.log('data: ' + data); - } + this._logService.debug('data: ' + data); if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); @@ -350,9 +348,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cursorStartY = buffer.y; // TODO: Consolidate debug/logging #1560 - if ((this._terminal).debug) { - this._terminal.log('data: ' + data); - } + this._logService.debug('data: ' + data); if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); @@ -1291,7 +1287,7 @@ export class InputHandler extends Disposable implements IInputHandler { // this.cursorBlink = true; break; case 66: - this._terminal.log('Serial port requested application keypad.'); + this._logService.info('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); @@ -1319,7 +1315,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._selectionService) { this._selectionService.disable(); } - this._terminal.log('Binding to mouse events.'); + this._logService.info('Binding to mouse events.'); break; case 1004: // send focusin/focusout events // focusin: ^[[I @@ -1494,7 +1490,7 @@ export class InputHandler extends Disposable implements IInputHandler { // this.cursorBlink = false; break; case 66: - this._terminal.log('Switching back to normal keypad.'); + this._logService.info('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); @@ -1785,7 +1781,7 @@ export class InputHandler extends Disposable implements IInputHandler { attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else { - this._terminal.error('Unknown SGR attribute: %d.', p); + this._logService.error('Unknown SGR attribute: %d.', p); } } } @@ -1999,7 +1995,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Enables the numeric keypad to send application sequences to the host. */ public keypadApplicationMode(): void { - this._terminal.log('Serial port requested application keypad.'); + this._logService.info('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); @@ -2012,7 +2008,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Enables the keypad to send numeric characters to the host. */ public keypadNumericMode(): void { - this._terminal.log('Switching back to normal keypad.'); + this._logService.info('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); diff --git a/src/Terminal.ts b/src/Terminal.ts index c79d871a..8fc41263 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -47,7 +47,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreService, ILogService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -58,6 +58,7 @@ import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; import { IParams } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; +import { LogService } from 'common/services/LogService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -110,6 +111,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // common services private _bufferService: IBufferService; private _coreService: ICoreService; + private _logService: ILogService; public optionsService: IOptionsService; // browser services @@ -241,6 +243,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._bufferService = new BufferService(this.optionsService); this._coreService = new CoreService(() => this.scrollToBottom(), this._bufferService, this.optionsService); this._coreService.onData(e => this._onData.fire(e)); + this._logService = new LogService(this.optionsService); this._setupOptionsListeners(); this._setup(); @@ -297,7 +300,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this.optionsService); + this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._logService, this.optionsService); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index e3a6c9b9..c786a06d 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -36,6 +36,13 @@ export class MockCoreService implements ICoreService { triggerDataEvent(data: string, wasUserInput?: boolean): void {} } +export class MockLogService implements ILogService { + debug(message: any, ...optionalParams: any[]): void {} + info(message: any, ...optionalParams: any[]): void {} + warn(message: any, ...optionalParams: any[]): void {} + error(message: any, ...optionalParams: any[]): void {} +} + export class MockOptionsService implements IOptionsService { options: ITerminalOptions = clone(DEFAULT_OPTIONS); onOptionChange: IEvent = new EventEmitter().event; diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts new file mode 100644 index 00000000..78e59e1c --- /dev/null +++ b/src/common/services/LogService.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ILogService, IOptionsService } from 'common/services/Services'; + +interface IConsole { + log(message?: any, ...optionalParams: any[]): void; + error(message?: any, ...optionalParams: any[]): void; + info(message?: any, ...optionalParams: any[]): void; + trace(message?: any, ...optionalParams: any[]): void; + warn(message?: any, ...optionalParams: any[]): void; +} + +// console is available on both node.js and browser contexts but the common +// module doesn't depend on them so we need to explicitly declare it. +declare const console: IConsole; + + +export enum LogLevel { + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, + Off = 4 +} + +const optionsKeyToLogLevel: { [key: string]: LogLevel } = { + debug: LogLevel.Debug, + info: LogLevel.Info, + warn: LogLevel.Warn, + error: LogLevel.Error, + off: LogLevel.Off +}; + +export class LogService implements ILogService { + private _logLevel!: LogLevel; + + constructor( + private readonly _optionsService: IOptionsService + ) { + this._updateLogLevel(); + this._optionsService.onOptionChange(key => { + if (key === 'logLevel') { + this._updateLogLevel(); + } + }) + } + + private _updateLogLevel(): void { + this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; + } + + debug(message: any, ...optionalParams: any[]): void { + if (this._logLevel <= LogLevel.Debug) { + console.log.call(console, message, optionalParams); + } + } + + info(message: any, ...optionalParams: any[]): void { + if (this._logLevel <= LogLevel.Info) { + console.info.call(console, message, optionalParams); + } + } + + warn(message: any, ...optionalParams: any[]): void { + if (this._logLevel <= LogLevel.Warn) { + console.warn.call(console, message, optionalParams); + } + } + + error(message: any, ...optionalParams: any[]): void { + if (this._logLevel <= LogLevel.Error) { + console.error.call(console, message, optionalParams); + } + } +} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ab041488..8b58de8d 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -29,6 +29,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ fontWeightBold: 'bold', lineHeight: 1.0, letterSpacing: 0, + logLevel: 'info', scrollback: 1000, screenReaderMode: false, macOptionIsMeta: false, diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.d.ts index f46f3d76..9ef5defc 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.d.ts @@ -38,6 +38,13 @@ export interface ICoreService { triggerDataEvent(data: string, wasUserInput?: boolean): void; } +export interface ILogService { + debug(message: any, ...optionalParams: any[]): void; + info(message: any, ...optionalParams: any[]): void; + warn(message: any, ...optionalParams: any[]): void; + error(message: any, ...optionalParams: any[]): void; +} + export interface IOptionsService { readonly options: ITerminalOptions; @@ -48,7 +55,7 @@ export interface IOptionsService { } export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; - +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; export type RendererType = 'dom' | 'canvas'; export interface IPartialTerminalOptions { @@ -66,6 +73,7 @@ export interface IPartialTerminalOptions { fontWeightBold?: FontWeight; letterSpacing?: number; lineHeight?: number; + logLevel?: LogLevel; macOptionIsMeta?: boolean; macOptionClickForcesSelection?: boolean; rendererType?: RendererType; @@ -86,6 +94,7 @@ export interface ITerminalOptions { cols: number; cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; + debug: boolean; disableStdin: boolean; drawBoldTextInBrightColors: boolean; fontSize: number; @@ -94,6 +103,7 @@ export interface ITerminalOptions { fontWeightBold: FontWeight; letterSpacing: number; lineHeight: number; + logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; rendererType: RendererType; @@ -109,7 +119,6 @@ export interface ITerminalOptions { [key: string]: any; cancelEvents: boolean; convertEol: boolean; - debug: boolean; screenKeys: boolean; termName: string; useFlowControl: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c915a4ba..c4062db8 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -15,6 +15,11 @@ declare module 'xterm' { */ export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + /** + * A string representing log level. + */ + export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; + /** * A string representing a renderer type. */ @@ -107,6 +112,18 @@ declare module 'xterm' { */ lineHeight?: number; + /** + * What log level to use, this will log for all levels below and including + * what is set: + * + * 1. debug + * 2. info (default) + * 3. warn + * 4. error + * 5. off + */ + logLevel?: LogLevel; + /** * Whether to treat option as the meta key. */