bugfix: #401 Duplicate lines in output (#623)

This commit is contained in:
James Read 2025-07-20 00:34:59 +01:00 committed by GitHub
parent b91c0d7e45
commit af75afa82f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 75 additions and 18 deletions

View File

@ -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()
})
}

24
webui.dev/js/Mutex.js Normal file
View File

@ -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);
}
}
}

View File

@ -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)
}
}

View File

@ -170,8 +170,6 @@ function onOutputChunk (evt) {
if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) {
window.terminal.write(chunk.output)
window.executionDialog.showOutput()
}
}