Skip to content

Commit b4635ed

Browse files
committed
make reset async aware
1 parent 6b57921 commit b4635ed

File tree

5 files changed

+111
-80
lines changed

5 files changed

+111
-80
lines changed

src/common/parser/DcsParser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@ export class DcsParser implements IDcsParser {
4848
}
4949

5050
public reset(): void {
51+
// force cleanup leftover handlers
5152
if (this._active.length) {
52-
this.unhook(false);
53+
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
54+
this._active[j].unhook(false);
55+
}
5356
}
57+
this._stack.paused = false;
5458
this._active = EMPTY_HANDLERS;
5559
this._ident = 0;
5660
}

src/common/parser/EscapeSequenceParser.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1847,7 +1847,9 @@ describe('EscapeSequenceParser - async', () => {
18471847
// keeps being broken for further parse calls (sync and async)
18481848
assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing');
18491849
await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing');
1850-
// FIXME: come up with a good recovery strategy
1850+
// reset should lift the error condition
1851+
parser.reset();
1852+
await parseP(parser, INPUT); // does not throw anymore
18511853
});
18521854
it('correct result on awaited parse call', async () => {
18531855
await parseP(parser, INPUT);

src/common/parser/EscapeSequenceParser.ts

Lines changed: 97 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
427427
this._errorHandler = this._errorHandlerFb;
428428
}
429429

430+
/**
431+
* Reset parser to initial values.
432+
*
433+
* This can also be used to lift the improper continuation error condition
434+
* when dealing with async handlers. Use this only as a last resort to silence
435+
* that error when the terminal has no pending data to be processed. Note that
436+
* the interrupted async handler might continue its work in the future messing
437+
* up the terminal state even further.
438+
*/
430439
public reset(): void {
431440
this.currentState = this.initialState;
432441
this._oscParser.reset();
@@ -435,6 +444,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
435444
this._params.addParam(0); // ZDM
436445
this._collect = 0;
437446
this.precedingCodepoint = 0;
447+
// abort pending continuation from async handler
448+
// Here the RESET type indicates, that the next parse call will
449+
// ignore any saved stack, instead continues sync with next codepoint from GROUND
450+
if (this._parseStack.state !== ParserStackType.NONE) {
451+
this._parseStack.state = ParserStackType.RESET;
452+
this._parseStack.handlers = []; // also release handlers ref
453+
}
438454
}
439455

440456
/**
@@ -486,12 +502,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
486502
* Important: With only sync handlers defined, parsing is completely synchronous as well.
487503
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
488504
*
489-
* FIXME: to be discussed
490-
* While awaiting parse promises the terminal buffer state may not change.
491-
* --> Implement lock semantics / promise chaining on buffer alterations? Waah, pandora's box ;)
492-
* --> Maybe easier: Give up on non-mutating rule for async handlers...
493-
* (needs explanation in docs about exact executor/thenable/worker execution contexts)
494-
*
495505
* Boilerplate for proper parsing of multiple chunks with async handlers:
496506
*
497507
* ```typescript
@@ -515,84 +525,95 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
515525

516526
// resume from async handler
517527
if (this._parseStack.state) {
518-
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
519-
/**
520-
* Reject further parsing on improper continuation after pausing.
521-
* This is a really bad condition with screwed up execution order and messed up terminal state,
522-
* therefore we exit hard with an exception and reject any further parsing.
523-
*
524-
* Note: With `Terminal.write` usage this exception should never occur, as the top level
525-
* calls are guaranteed to handle async conditions properly. If you ever encounter this
526-
* exception in your terminal integration it indicates, that you injected data chunks to
527-
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
528-
* continuation of a running async handler.
529-
*/
530-
this._parseStack.state = ParserStackType.FAIL;
531-
throw new Error('improper continuation due to previous async handler, giving up parsing');
532-
}
528+
// allow sync parser reset even in continuation mode
529+
// Note: can be used to recover parser from improper continuation error above
530+
if (this._parseStack.state === ParserStackType.RESET) {
531+
this._parseStack.state = ParserStackType.NONE;
532+
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
533+
} else {
534+
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
535+
/**
536+
* Reject further parsing on improper continuation after pausing.
537+
* This is a really bad condition with screwed up execution order and messed up terminal state,
538+
* therefore we exit hard with an exception and reject any further parsing.
539+
*
540+
* Note: With `Terminal.write` usage this exception should never occur, as the top level
541+
* calls are guaranteed to handle async conditions properly. If you ever encounter this
542+
* exception in your terminal integration it indicates, that you injected data chunks to
543+
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
544+
* continuation of a running async handler.
545+
*
546+
* Its possible to get rid of this error condition by calling `reset`, but dont rely on that,
547+
* as the pending async handler might mess up the terminal even further. Instead fix the faulty
548+
* async handling, so this error will not be thrown anymore.
549+
*/
550+
this._parseStack.state = ParserStackType.FAIL;
551+
throw new Error('improper continuation due to previous async handler, giving up parsing');
552+
}
533553

534-
// we have to resume the old handler loop if:
535-
// - return value of the promise was `false`
536-
// - handlers are not exhausted yet
537-
// FIXME: removing handlers from within a handler of the same sequence
538-
// is not supported atm (also true for sync handlers)!!
539-
const handlers = this._parseStack.handlers;
540-
let handlerPos = this._parseStack.handlerPos - 1;
541-
switch (this._parseStack.state) {
542-
case ParserStackType.CSI:
543-
if (promiseResult === false && handlerPos > -1) {
544-
for (; handlerPos >= 0; handlerPos--) {
545-
if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) {
546-
if (handlerResult instanceof Promise) {
547-
this._parseStack.handlerPos = handlerPos;
548-
return handlerResult;
554+
// we have to resume the old handler loop if:
555+
// - return value of the promise was `false`
556+
// - handlers are not exhausted yet
557+
// FIXME: removing handlers from within a handler of the same sequence
558+
// is not supported atm (also true for sync handlers)!!
559+
const handlers = this._parseStack.handlers;
560+
let handlerPos = this._parseStack.handlerPos - 1;
561+
switch (this._parseStack.state) {
562+
case ParserStackType.CSI:
563+
if (promiseResult === false && handlerPos > -1) {
564+
for (; handlerPos >= 0; handlerPos--) {
565+
if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) {
566+
if (handlerResult instanceof Promise) {
567+
this._parseStack.handlerPos = handlerPos;
568+
return handlerResult;
569+
}
570+
break;
549571
}
550-
break;
551572
}
552573
}
553-
}
554-
this._parseStack.handlers = [];
555-
break;
556-
case ParserStackType.ESC:
557-
if (promiseResult === false && handlerPos > -1) {
558-
for (; handlerPos >= 0; handlerPos--) {
559-
if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) {
560-
if (handlerResult instanceof Promise) {
561-
this._parseStack.handlerPos = handlerPos;
562-
return handlerResult;
574+
this._parseStack.handlers = [];
575+
break;
576+
case ParserStackType.ESC:
577+
if (promiseResult === false && handlerPos > -1) {
578+
for (; handlerPos >= 0; handlerPos--) {
579+
if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) {
580+
if (handlerResult instanceof Promise) {
581+
this._parseStack.handlerPos = handlerPos;
582+
return handlerResult;
583+
}
584+
break;
563585
}
564-
break;
565586
}
566587
}
567-
}
568-
this._parseStack.handlers = [];
569-
break;
570-
case ParserStackType.DCS:
571-
code = data[this._parseStack.chunkPos];
572-
if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) {
573-
return handlerResult;
574-
}
575-
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
576-
this._params.reset();
577-
this._params.addParam(0); // ZDM
578-
this._collect = 0;
579-
break;
580-
case ParserStackType.OSC:
581-
code = data[this._parseStack.chunkPos];
582-
if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) {
583-
return handlerResult;
584-
}
585-
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
586-
this._params.reset();
587-
this._params.addParam(0); // ZDM
588-
this._collect = 0;
589-
break;
588+
this._parseStack.handlers = [];
589+
break;
590+
case ParserStackType.DCS:
591+
code = data[this._parseStack.chunkPos];
592+
if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) {
593+
return handlerResult;
594+
}
595+
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
596+
this._params.reset();
597+
this._params.addParam(0); // ZDM
598+
this._collect = 0;
599+
break;
600+
case ParserStackType.OSC:
601+
code = data[this._parseStack.chunkPos];
602+
if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) {
603+
return handlerResult;
604+
}
605+
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
606+
this._params.reset();
607+
this._params.addParam(0); // ZDM
608+
this._collect = 0;
609+
break;
610+
}
611+
// cleanup before continuing with the main sync loop
612+
this._parseStack.state = ParserStackType.NONE;
613+
start = this._parseStack.chunkPos + 1;
614+
this.precedingCodepoint = 0;
615+
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
590616
}
591-
// cleanup before continuing with the main sync loop
592-
this._parseStack.state = ParserStackType.NONE;
593-
start = this._parseStack.chunkPos + 1;
594-
this.precedingCodepoint = 0;
595-
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
596617
}
597618

598619
// continue with main sync loop

src/common/parser/OscParser.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ export class OscParser implements IOscParser {
4646
}
4747

4848
public reset(): void {
49-
// cleanup handlers if payload was already sent
49+
// force cleanup handlers if payload was already sent
5050
if (this._state === OscState.PAYLOAD) {
51-
this.end(false);
51+
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
52+
this._active[j].end(false);
53+
}
5254
}
55+
this._stack.paused = false;
5356
this._active = EMPTY_HANDLERS;
5457
this._id = -1;
5558
this._state = OscState.START;

src/common/parser/Types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ export interface IHandlerCollection<T> {
245245
export const enum ParserStackType {
246246
NONE = 0,
247247
FAIL,
248+
RESET,
248249
CSI,
249250
ESC,
250251
OSC,

0 commit comments

Comments
 (0)