refactor: fine tune serial console

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-08-04 00:35:06 +08:00
parent 9f9069fb63
commit 12a51b2451
No known key found for this signature in database
GPG Key ID: 39DC4436AE3564D5
2 changed files with 130 additions and 28 deletions

View File

@ -34,6 +34,8 @@ export default {
firstTime: true,
queue: [],
isDraining: false,
drainScheduled: false,
drainTimer: null,
showSearch: false,
searchQuery: '',
searchOptions: {
@ -54,12 +56,17 @@ export default {
},
computed: {
serialConsoleUrl() {
return this.value?.getSerialConsolePath || '';
},
xtermConfig() {
return {
allowProposedApi: true,
cursorBlink: true,
cursorStyle: 'bar',
cursorWidth: 2,
scrollback: 5000,
useStyle: true,
fontFamily: 'JetBrains Mono, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 16,
@ -72,6 +79,14 @@ export default {
this.close();
},
watch: {
serialConsoleUrl() {
if (this.terminal) {
this.connect();
}
},
},
async mounted() {
this.textDecoder = new TextDecoder('utf-8');
await this.setupTerminal();
@ -146,6 +161,7 @@ export default {
this.fit();
this.flush();
this.scheduleDrainQueue();
terminal.onData((input) => {
const msg = this.str2ab(input);
@ -191,10 +207,17 @@ export default {
closeSearch() {
this.showSearch = false;
this.searchAddon?.clearDecorations();
this.searchQuery = '';
this.clearSearchHighlights();
this.terminal?.focus();
},
clearSearchHighlights() {
this.terminal?.clearSelection();
this.searchAddon?.clearActiveDecoration();
this.searchAddon?.clearDecorations();
},
findNext() {
if (!this.searchAddon || !this.searchQuery) {
return;
@ -213,7 +236,7 @@ export default {
onSearchInput() {
if (!this.searchQuery) {
this.searchAddon?.clearDecorations();
this.clearSearchHighlights();
return;
}
@ -282,7 +305,31 @@ export default {
enqueueMessage(messagePromise) {
this.queue.push(messagePromise);
this.scheduleDrainQueue();
},
scheduleDrainQueue() {
if (this.drainScheduled || this.isDraining || !this.terminal) {
return;
}
this.drainScheduled = true;
const shouldDrainImmediately = this.queue.length <= 3;
const runDrain = () => {
this.drainScheduled = false;
this.drainTimer = null;
this.drainQueue();
};
if (shouldDrainImmediately) {
runDrain();
return;
}
this.drainTimer = requestAnimationFrame(runDrain);
},
async decodeMessageData(data) {
@ -331,7 +378,11 @@ export default {
const output = messages.filter(Boolean).join('');
if (output) {
this.terminal.write(output);
await this.writeOutputInChunks(output);
}
if (this.queue.length > 0) {
await this.nextFrame();
}
}
} finally {
@ -340,17 +391,43 @@ export default {
// If data arrived between the last while-check and releasing the lock,
// immediately schedule another drain so output never gets stuck.
if (this.queue.length > 0 && this.terminal) {
this.drainQueue();
this.scheduleDrainQueue();
}
}
},
async writeOutputInChunks(output) {
if (!output) {
return;
}
const chunkSize = output.length > 20000 ? 16000 : output.length > 8000 ? 8000 : output.length;
if (chunkSize === output.length) {
this.terminal.write(output);
return;
}
for (let index = 0; index < output.length; index += chunkSize) {
this.terminal.write(output.slice(index, index + chunkSize));
if (index + chunkSize < output.length) {
await this.nextFrame();
}
}
},
nextFrame() {
return new Promise((resolve) => requestAnimationFrame(resolve));
},
clear() {
this.terminal.clear();
},
getSocketUrl() {
return `${ this.value?.getSerialConsolePath }`;
return this.serialConsoleUrl;
},
async connect() {
@ -432,6 +509,11 @@ export default {
this.onResize = null;
}
if (this.drainTimer) {
cancelAnimationFrame(this.drainTimer);
this.drainTimer = null;
}
if ( this.socket ) {
this.socket.disconnect();
}
@ -544,13 +626,22 @@ export default {
.shell-body {
flex: 1 1 auto;
height: 100%;
width: 100%;
min-height: 0;
padding: 0 10px;
box-sizing: border-box;
}
.terminal.xterm {
.terminal.xterm,
.terminal.xterm .xterm-viewport,
.terminal.xterm .xterm-screen,
.terminal.xterm .xterm-scrollable-element {
width: 100%;
height: 100%;
}
.terminal.xterm .xterm-scrollable-element {
min-height: 100%;
}
}
</style>

View File

@ -1,10 +1,9 @@
<script>
import Loading from '@shell/components/Loading';
import { HCI } from '../../../../../types';
import SerialConsole from '../../../../../components/SerialConsole';
export default {
components: { SerialConsole, Loading },
components: { SerialConsole },
async fetch() {
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
@ -39,11 +38,9 @@ export default {
<template>
<div class="serial-console-page">
<Loading v-if="$fetchState.pending" />
<SerialConsole
v-else
ref="serialConsole"
v-model:value="vmi"
:value="vmi || {}"
/>
</div>
</template>
@ -61,9 +58,23 @@ export default {
position: fixed;
inset: 0;
min-height: 0;
background: var(--body-bg);
color: var(--terminal-text);
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 6px;
pointer-events: none;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.25));
}
.harvester-shell-container {
height: 100%;
background: var(--terminal-bg);
}
}
</style>