From bce3269e445376f3dc7c5b3faac65855c30209f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 15:41:10 +0100 Subject: [PATCH] async OSC/DCS registering/dispose tests on parser instance --- .../parser/EscapeSequenceParser.test.ts | 201 ++++++++++++++++-- src/common/parser/EscapeSequenceParser.ts | 18 +- 2 files changed, 192 insertions(+), 27 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 56d4c93e..6d00adda 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -102,7 +102,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public trackedStack: IParserStackState[] = []; public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { const result = super.parse(data, length, promiseResult); - if (result && this._trackStack) { + if (result instanceof Promise && this._trackStack) { this.trackedStack.push({ ...this.parseStack }); } return result; @@ -1742,9 +1742,9 @@ async function throwsAsync(fn: () => Promise, message?: string | undefined) } describe('EscapeSequenceParser - async', () => { - // sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | OSC 1;foo=bar ST + // sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN // needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1 - const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN'; let RESULT: any[]; let parser: TestEscapeSequenceParser; const callstack: any[] = []; @@ -1764,7 +1764,9 @@ describe('EscapeSequenceParser - async', () => { ['EXE \r'], ['EXE \n'], ['PRINT', '$>'], - ['OSC 1', 'foo=bar'] + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'FIN'] ]; parser = new TestEscapeSequenceParser(); parser.reset(); @@ -1786,9 +1788,10 @@ describe('EscapeSequenceParser - async', () => { parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); }); - it('sync handlers keep parsed in sync mode', () => { + it('sync handlers keep being parsed in sync mode', () => { // note: if we have only sync handlers, a parse call should never return anything assert.equal(!parseSync(parser, INPUT), true); assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused @@ -1819,7 +1822,8 @@ describe('EscapeSequenceParser - async', () => { parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; }); parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); - parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); }); it('sync parse call does not work anymore', () => { @@ -1849,7 +1853,12 @@ describe('EscapeSequenceParser - async', () => { await parseP(parser, INPUT); assert.deepEqual(callstack, RESULT); evalStackSaves(parser.trackedStack, [ - [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 0] + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('correct result on chunked awaited parse calls', async () => { @@ -1874,7 +1883,11 @@ describe('EscapeSequenceParser - async', () => { ['EXE \n'], ['PRINT', '$'], ['PRINT', '>'], - ['OSC 1', 'foo=bar'] + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'F'], + ['PRINT', 'I'], + ['PRINT', 'N'] ]; // split to single char input @@ -1888,7 +1901,9 @@ describe('EscapeSequenceParser - async', () => { [0, ParserStackType.CSI, 0], [0, ParserStackType.ESC, 0], [0, ParserStackType.ESC, 0], - [0, ParserStackType.CSI, 0] + [0, ParserStackType.CSI, 0], + [0, ParserStackType.DCS, 0], + [0, ParserStackType.OSC, 0] ]); }); it('multiple async SGR handlers', async () => { @@ -1906,7 +1921,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1917,7 +1934,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); @@ -1933,7 +1952,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 1], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 1] + [27, ParserStackType.CSI, 1], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1944,7 +1965,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('multiple async ESC handlers', async () => { @@ -1960,7 +1983,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 1], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1971,7 +1996,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); @@ -1986,7 +2013,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1997,7 +2026,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('sync/async SGR mixed', async () => { @@ -2020,7 +2051,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 2], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // dispose SGR2 (sync one) @@ -2039,7 +2072,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // dispose SGR3 (async one) @@ -2050,8 +2085,134 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); + it('multiple async OSC handlers', async () => { + // register with fallback + const OSC2 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return false; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.equal(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const OSC22 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return true; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.notEqual(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); + it('multiple async DCS handlers', async () => { + // register with fallback + const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.equal(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.notEqual(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); }); }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 79f8b844..3b5c1d05 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -492,7 +492,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * --> Maybe easier: Give up on non-mutating rule for async handlers... * (needs explanation in docs about exact executor/thenable/worker execution contexts) * - * Example for proper parsing of multiple chunks: + * Boilerplate for proper parsing of multiple chunks with async handlers: * * ```typescript * async function parseMultipleChunks(chunks: Uint32Array[]): Promise { @@ -518,10 +518,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { /** * Reject further parsing on improper continuation after pausing. - * This will happen with sync parse calls not awaiting a returned promise. - * It is a really bad condition with screwed up execution order, - * therefore we exit hard with an exception. - * FIXME: Do we need a method to escape from this broken parser state? (hard to achieve properly) + * This is a really bad condition with screwed up execution order and messed up terminal state, + * therefore we exit hard with an exception and reject any further parsing. + * + * Note: With `Terminal.write` usage this exception should never occur, as the top level + * calls are guaranteed to handle async conditions properly. If you ever encounter this + * exception in your terminal integration it indicates, that you injected data chunks to + * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for + * continuation of a running async handler. */ this._parseStack.state = ParserStackType.FAIL; throw new Error('improper continuation due to previous async handler, giving up parsing'); @@ -578,13 +582,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult; } - if (code === 0x1b) transition |= ParserState.ESCAPE; + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; break; } - // cleanup before continuing with the main loop + // cleanup before continuing with the main sync loop this._parseStack.state = ParserStackType.NONE; start = this._parseStack.chunkPos + 1; this.precedingCodepoint = 0;