From af75afa82f6be0f2d45fd6b1aebd855fe43325b8 Mon Sep 17 00:00:00 2001 From: James Read Date: Sun, 20 Jul 2025 00:34:59 +0100 Subject: [PATCH] bugfix: #401 Duplicate lines in output (#623) --- webui.dev/js/ExecutionDialog.js | 20 ++++++-------- webui.dev/js/Mutex.js | 24 +++++++++++++++++ webui.dev/js/OutputTerminal.js | 47 ++++++++++++++++++++++++++++++--- webui.dev/js/marshaller.js | 2 -- 4 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 webui.dev/js/Mutex.js diff --git a/webui.dev/js/ExecutionDialog.js b/webui.dev/js/ExecutionDialog.js index e06a557..43c7d91 100644 --- a/webui.dev/js/ExecutionDialog.js +++ b/webui.dev/js/ExecutionDialog.js @@ -28,24 +28,20 @@ export class ExecutionDialog { window.terminal = new OutputTerminal() window.terminal.open(this.domOutput) - } - - showOutput () { - this.domOutput.hidden = false - this.domOutput.hidden = false + window.terminal.resize(80, 24) } toggleSize () { if (this.dlg.classList.contains('big')) { this.dlg.classList.remove('big') + window.terminal.resize(80, 24) } else { this.dlg.classList.add('big') + window.terminal.fit() } - - window.terminal.fit() } - reset () { + async reset () { this.executionSeconds = 0 this.executionTrackingId = 'notset' @@ -69,7 +65,7 @@ export class ExecutionDialog { this.domExecutionDetails.hidden = true - window.terminal.reset() + await window.terminal.reset() window.terminal.fit() } @@ -193,7 +189,7 @@ export class ExecutionDialog { } } - renderExecutionResult (res) { + async renderExecutionResult (res) { this.res = res clearInterval(window.executionDialogTicker) @@ -229,8 +225,8 @@ export class ExecutionDialog { this.updateDuration(res.logEntry) - window.terminal.reset() - window.terminal.write(res.logEntry.output, () => { + await window.terminal.reset() + await window.terminal.write(res.logEntry.output, () => { window.terminal.fit() }) } diff --git a/webui.dev/js/Mutex.js b/webui.dev/js/Mutex.js new file mode 100644 index 0000000..25f9212 --- /dev/null +++ b/webui.dev/js/Mutex.js @@ -0,0 +1,24 @@ +export class Mutex { + constructor() { + this._locked = false; + this._waiting = []; + } + + lock() { + const unlock = () => { + const next = this._waiting.shift(); + if (next) { + next(unlock); + } else { + this._locked = false; + } + }; + + if (this._locked) { + return new Promise(resolve => this._waiting.push(resolve)).then(() => unlock); + } else { + this._locked = true; + return Promise.resolve(unlock); + } + } +} diff --git a/webui.dev/js/OutputTerminal.js b/webui.dev/js/OutputTerminal.js index 6cbafc1..13e69e0 100644 --- a/webui.dev/js/OutputTerminal.js +++ b/webui.dev/js/OutputTerminal.js @@ -1,8 +1,22 @@ import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' +import { Mutex } from './Mutex.js' +/** + * xterm.js based terminal output for the execution dialog. + * + * the xterm.js methods for write(), reset() and clear() appear to be async, + * but they do not return a Promise and instead use a callback. When calling + * these methods in quick succession, the output can get garbled due to race + * conditions. + * + * To avoid this, this class uses Mutex around those methods to ensure that + * only one write OR reset is executed at a time, is completed, and the calls + * occour in sequential order. + */ export class OutputTerminal { constructor () { + this.writeMutex = new Mutex() this.terminal = new Terminal({ convertEol: true }) @@ -12,8 +26,33 @@ export class OutputTerminal { this.terminal.fit = fitAddon } - write (out, then) { - this.terminal.write(out, then) + async write (out, then) { + const unlock = await this.writeMutex.lock() + + try { + await new Promise(resolve => { + this.terminal.write(out, () => { + resolve() + }) + }) + } finally { + unlock() + then() + } + } + + async reset () { + const unlock = await this.writeMutex.lock() + + try { + await new Promise(resolve => { + this.terminal.clear() + this.terminal.reset() + resolve() + }) + } finally { + unlock() + } } fit () { @@ -24,7 +63,7 @@ export class OutputTerminal { this.terminal.open(el) } - reset () { - this.terminal.reset() + resize (cols, rows) { + this.terminal.resize(cols, rows) } } diff --git a/webui.dev/js/marshaller.js b/webui.dev/js/marshaller.js index e0b1018..9dcea0f 100644 --- a/webui.dev/js/marshaller.js +++ b/webui.dev/js/marshaller.js @@ -170,8 +170,6 @@ function onOutputChunk (evt) { if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) { window.terminal.write(chunk.output) - - window.executionDialog.showOutput() } }