make reset async aware

This commit is contained in:
Jörg Breitbart
2021-02-25 19:04:37 +01:00
parent 6b57921567
commit b4635eddf3
5 changed files with 111 additions and 80 deletions
+5 -1
View File
@@ -48,9 +48,13 @@ export class DcsParser implements IDcsParser {
} }
public reset(): void { public reset(): void {
// force cleanup leftover handlers
if (this._active.length) { if (this._active.length) {
this.unhook(false); for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].unhook(false);
}
} }
this._stack.paused = false;
this._active = EMPTY_HANDLERS; this._active = EMPTY_HANDLERS;
this._ident = 0; this._ident = 0;
} }
@@ -1847,7 +1847,9 @@ describe('EscapeSequenceParser - async', () => {
// keeps being broken for further parse calls (sync and async) // keeps being broken for further parse calls (sync and async)
assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing'); assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing');
await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing'); await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing');
// FIXME: come up with a good recovery strategy // reset should lift the error condition
parser.reset();
await parseP(parser, INPUT); // does not throw anymore
}); });
it('correct result on awaited parse call', async () => { it('correct result on awaited parse call', async () => {
await parseP(parser, INPUT); await parseP(parser, INPUT);
+97 -76
View File
@@ -427,6 +427,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandler = this._errorHandlerFb; this._errorHandler = this._errorHandlerFb;
} }
/**
* Reset parser to initial values.
*
* This can also be used to lift the improper continuation error condition
* when dealing with async handlers. Use this only as a last resort to silence
* that error when the terminal has no pending data to be processed. Note that
* the interrupted async handler might continue its work in the future messing
* up the terminal state even further.
*/
public reset(): void { public reset(): void {
this.currentState = this.initialState; this.currentState = this.initialState;
this._oscParser.reset(); this._oscParser.reset();
@@ -435,6 +444,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._params.addParam(0); // ZDM this._params.addParam(0); // ZDM
this._collect = 0; this._collect = 0;
this.precedingCodepoint = 0; this.precedingCodepoint = 0;
// abort pending continuation from async handler
// Here the RESET type indicates, that the next parse call will
// ignore any saved stack, instead continues sync with next codepoint from GROUND
if (this._parseStack.state !== ParserStackType.NONE) {
this._parseStack.state = ParserStackType.RESET;
this._parseStack.handlers = []; // also release handlers ref
}
} }
/** /**
@@ -486,12 +502,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* Important: With only sync handlers defined, parsing is completely synchronous as well. * Important: With only sync handlers defined, parsing is completely synchronous as well.
* As soon as an async handler is involved, synchronous parsing is not possible anymore. * As soon as an async handler is involved, synchronous parsing is not possible anymore.
* *
* FIXME: to be discussed
* While awaiting parse promises the terminal buffer state may not change.
* --> Implement lock semantics / promise chaining on buffer alterations? Waah, pandora's box ;)
* --> Maybe easier: Give up on non-mutating rule for async handlers...
* (needs explanation in docs about exact executor/thenable/worker execution contexts)
*
* Boilerplate for proper parsing of multiple chunks with async handlers: * Boilerplate for proper parsing of multiple chunks with async handlers:
* *
* ```typescript * ```typescript
@@ -515,84 +525,95 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// resume from async handler // resume from async handler
if (this._parseStack.state) { if (this._parseStack.state) {
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { // allow sync parser reset even in continuation mode
/** // Note: can be used to recover parser from improper continuation error above
* Reject further parsing on improper continuation after pausing. if (this._parseStack.state === ParserStackType.RESET) {
* This is a really bad condition with screwed up execution order and messed up terminal state, this._parseStack.state = ParserStackType.NONE;
* therefore we exit hard with an exception and reject any further parsing. start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
* } else {
* Note: With `Terminal.write` usage this exception should never occur, as the top level if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
* 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 * Reject further parsing on improper continuation after pausing.
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for * This is a really bad condition with screwed up execution order and messed up terminal state,
* continuation of a running async handler. * therefore we exit hard with an exception and reject any further parsing.
*/ *
this._parseStack.state = ParserStackType.FAIL; * Note: With `Terminal.write` usage this exception should never occur, as the top level
throw new Error('improper continuation due to previous async handler, giving up parsing'); * 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.
*
* Its possible to get rid of this error condition by calling `reset`, but dont rely on that,
* as the pending async handler might mess up the terminal even further. Instead fix the faulty
* async handling, so this error will not be thrown anymore.
*/
this._parseStack.state = ParserStackType.FAIL;
throw new Error('improper continuation due to previous async handler, giving up parsing');
}
// we have to resume the old handler loop if: // we have to resume the old handler loop if:
// - return value of the promise was `false` // - return value of the promise was `false`
// - handlers are not exhausted yet // - handlers are not exhausted yet
// FIXME: removing handlers from within a handler of the same sequence // FIXME: removing handlers from within a handler of the same sequence
// is not supported atm (also true for sync handlers)!! // is not supported atm (also true for sync handlers)!!
const handlers = this._parseStack.handlers; const handlers = this._parseStack.handlers;
let handlerPos = this._parseStack.handlerPos - 1; let handlerPos = this._parseStack.handlerPos - 1;
switch (this._parseStack.state) { switch (this._parseStack.state) {
case ParserStackType.CSI: case ParserStackType.CSI:
if (promiseResult === false && handlerPos > -1) { if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) { for (; handlerPos >= 0; handlerPos--) {
if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) {
if (handlerResult instanceof Promise) { if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos; this._parseStack.handlerPos = handlerPos;
return handlerResult; return handlerResult;
}
break;
} }
break;
} }
} }
} this._parseStack.handlers = [];
this._parseStack.handlers = []; break;
break; case ParserStackType.ESC:
case ParserStackType.ESC: if (promiseResult === false && handlerPos > -1) {
if (promiseResult === false && handlerPos > -1) { for (; handlerPos >= 0; handlerPos--) {
for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) {
if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { if (handlerResult instanceof Promise) {
if (handlerResult instanceof Promise) { this._parseStack.handlerPos = handlerPos;
this._parseStack.handlerPos = handlerPos; return handlerResult;
return handlerResult; }
break;
} }
break;
} }
} }
} this._parseStack.handlers = [];
this._parseStack.handlers = []; break;
break; case ParserStackType.DCS:
case ParserStackType.DCS: code = data[this._parseStack.chunkPos];
code = data[this._parseStack.chunkPos]; if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) {
if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult;
return handlerResult; }
} if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; this._params.reset();
this._params.reset(); this._params.addParam(0); // ZDM
this._params.addParam(0); // ZDM this._collect = 0;
this._collect = 0; break;
break; case ParserStackType.OSC:
case ParserStackType.OSC: code = data[this._parseStack.chunkPos];
code = data[this._parseStack.chunkPos]; if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) {
if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult;
return handlerResult; }
} if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; this._params.reset();
this._params.reset(); this._params.addParam(0); // ZDM
this._params.addParam(0); // ZDM this._collect = 0;
this._collect = 0; break;
break; }
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1;
this.precedingCodepoint = 0;
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
} }
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1;
this.precedingCodepoint = 0;
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
} }
// continue with main sync loop // continue with main sync loop
+5 -2
View File
@@ -46,10 +46,13 @@ export class OscParser implements IOscParser {
} }
public reset(): void { public reset(): void {
// cleanup handlers if payload was already sent // force cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) { if (this._state === OscState.PAYLOAD) {
this.end(false); for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
} }
this._stack.paused = false;
this._active = EMPTY_HANDLERS; this._active = EMPTY_HANDLERS;
this._id = -1; this._id = -1;
this._state = OscState.START; this._state = OscState.START;
+1
View File
@@ -245,6 +245,7 @@ export interface IHandlerCollection<T> {
export const enum ParserStackType { export const enum ParserStackType {
NONE = 0, NONE = 0,
FAIL, FAIL,
RESET,
CSI, CSI,
ESC, ESC,
OSC, OSC,