From d59f4d2f6f6da9fe862e52e0152b8d87583d4b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 01:53:59 +0100 Subject: [PATCH] async impl for DCS, DcsParser tests --- src/common/parser/DcsParser.test.ts | 205 ++++++++++++++++++++++ src/common/parser/DcsParser.ts | 63 +++++-- src/common/parser/EscapeSequenceParser.ts | 45 +++-- src/common/parser/Types.d.ts | 6 +- 4 files changed, 289 insertions(+), 30 deletions(-) diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index cf174139..096ebb58 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -252,3 +252,208 @@ describe('DcsParser', () => { }); }); }); + + +class TestHandlerAsync implements IDcsHandler { + constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} + public hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); + } + public async unhook(success: boolean): Promise { + // simple sleep to check in tests whether ordering gets messed up + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, 'UNHOOK', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function unhookP(parser: DcsParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.unhook(success, prev)) { + prev = await result; + } +} + + +describe('DcsParser - async tests', () => { + let parser: DcsParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new DcsParser(); + parser.setHandlerFallback((id, action, data) => { + if (action === 'HOOK') { + data = data.toArray(); + } + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', false], // important: a1 before s1 + ['s1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', true], // important: a1 before s1 + ['s1', 'UNHOOK', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', false], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', true], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', true] + ]); + }); + }); + describe('DcsHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); + dispo.dispose(); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + data = toUtf32('some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 2fa28992..9f326099 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -15,11 +15,11 @@ export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; - private _handlerFb: DcsFallbackHandlerType = () => {}; + private _handlerFb: DcsFallbackHandlerType = () => { }; public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; this._active = EMPTY_HANDLERS; } @@ -79,20 +79,46 @@ export class DcsParser implements IDcsParser { } } - public unhook(success: boolean): void { + private _stack = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + public unhook(success: boolean, promiseResult?: boolean): void | Promise { if (!this._active.length) { this._handlerFb(this._ident, 'UNHOOK', success); } else { + let handlerResult: any = false; let j = this._active.length - 1; - for (; j >= 0; j--) { - if (this._active[j].unhook(success)) { - break; - } + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; } - j--; - // cleanup left over handlers + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].unhook(success)) !== false) { + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + break; + } + } + j--; + } + // cleanup left over handlers (fallThrough for async) for (; j >= 0; j--) { - this._active[j].unhook(false); + if ((handlerResult = this._active[j].unhook(false)) instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } } } this._active = EMPTY_HANDLERS; @@ -113,7 +139,7 @@ export class DcsHandler implements IDcsHandler { private _params: IParams = EMPTY_PARAMS; private _hitLimit: boolean = false; - constructor(private _handler: (data: string, params: IParams) => boolean) {} + constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { } public hook(params: IParams): void { // since we need to preserve params until `unhook`, we have to clone it @@ -136,12 +162,21 @@ export class DcsHandler implements IDcsHandler { } } - public unhook(success: boolean): boolean { - let ret = false; + public unhook(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data, this._params); + if ((ret = this._handler(this._data, this._params)) instanceof Promise) { + // FIXME: should this be behind a catch rule? + return ret.then(res => { + // cleanup handler state late + this._params = EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + return res; + }); + } } this._params = EMPTY_PARAMS; this._data = ''; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 1ac1a22b..842a99b8 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: PrintHandlerType; - protected _executeHandlers: {[flag: number]: ExecuteHandlerType}; + protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; @@ -280,7 +280,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.registerEscHandler({final: '\\'}, () => true); + this.registerEscHandler({ final: '\\' }, () => true); } protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { @@ -452,8 +452,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP handlers: ResumableHandlersType, handlerPos: number, transition: number, - chunkPos: number): void - { + chunkPos: number): void { this._parseStack.state = state; this._parseStack.handlers = handlers; this._parseStack.handlerPos = handlerPos; @@ -534,10 +533,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // - handlers are not exhausted yet // FIXME: removing handlers from within a handler of the same sequence // is not supported atm (also true for sync handlers)!! - if (promiseResult === false && handlerPos > -1) { - const handlers = this._parseStack.handlers; - switch (this._parseStack.state) { - case ParserStackType.CSI: + let handlers: ResumableHandlersType; + switch (this._parseStack.state) { + case ParserStackType.CSI: + if (promiseResult === false && handlerPos > -1) { + handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { if (handlerResult instanceof Promise) { @@ -547,8 +547,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; } } - break; - case ParserStackType.ESC: + } + break; + case ParserStackType.ESC: + if (promiseResult === false && handlerPos > -1) { + handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { if (handlerResult instanceof Promise) { @@ -558,8 +561,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; } } - break; - } + } + break; + case ParserStackType.DCS: + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserStackType.OSC: + // TODO + break; } // cleanup before continuing with the main loop this._parseStack.state = ParserStackType.NONE; @@ -700,7 +716,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.DCS_UNHOOK: - this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + this._parseStack.state = ParserStackType.DCS; + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index f6db77fa..30217cc6 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -5,7 +5,7 @@ import { IDisposable } from 'common/Types'; import { ParserState } from 'common/parser/Constants'; -import { OscParser } from 'common/parser/OscParser'; + /** sequence params serialized to js arrays */ export type ParamsArray = (number | number[])[]; @@ -94,7 +94,7 @@ export interface IDcsHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - unhook(success: boolean): boolean; + unhook(success: boolean): boolean | Promise; } export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; @@ -219,7 +219,7 @@ export interface IOscParser extends ISubParser { hook(ident: number, params: IParams): void; - unhook(success: boolean): void; + unhook(success: boolean, promiseResult?: boolean): void | Promise; } /**