From 018a03910b51db892d89bd99aff2dd5bd45ea8b9 Mon Sep 17 00:00:00 2001 From: SoniaSandler <66797193+SoniaSandler@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:01:00 -0400 Subject: [PATCH] chore: use Xterm serialization to save container terminal content (#8498) * chore: use Xterm serialization to save container terminal content Signed-off-by: Sonia Sandler --- packages/renderer/package.json | 1 + .../ContainerDetailsTerminal.spec.ts | 16 +++-- .../container/ContainerDetailsTerminal.svelte | 58 ++++++++++--------- .../src/stores/container-terminal-store.ts | 7 ++- yarn.lock | 5 ++ 5 files changed, 54 insertions(+), 33 deletions(-) diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 724859de5f2..75cd61704ee 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -25,6 +25,7 @@ "@types/js-yaml": "^4.0.9", "@types/validator": "^13.12.0", "@typescript-eslint/eslint-plugin": "7.18.0", + "@xterm/addon-serialize": "^0.13.0", "autoprefixer": "^10.4.20", "eslint-plugin-svelte": "^2.43.0", "filesize": "^10.1.4", diff --git a/packages/renderer/src/lib/container/ContainerDetailsTerminal.spec.ts b/packages/renderer/src/lib/container/ContainerDetailsTerminal.spec.ts index e182041e872..909e0391e9b 100644 --- a/packages/renderer/src/lib/container/ContainerDetailsTerminal.spec.ts +++ b/packages/renderer/src/lib/container/ContainerDetailsTerminal.spec.ts @@ -67,7 +67,7 @@ test('expect being able to reconnect ', async () => { ); // render the component with a terminal - const renderObject = render(ContainerDetailsTerminal, { container, screenReaderMode: true }); + let renderObject = render(ContainerDetailsTerminal, { container, screenReaderMode: true }); // wait shellInContainerMock is called await waitFor(() => expect(shellInContainerMock).toHaveBeenCalled()); @@ -96,11 +96,19 @@ test('expect being able to reconnect ', async () => { expect(terminalsAfterDestroy.length).toBe(1); // ok, now render a new terminal widget, it should reuse data from the store - render(ContainerDetailsTerminal, { container, screenReaderMode: true }); + renderObject = render(ContainerDetailsTerminal, { container, screenReaderMode: true }); + + // wait shellInContainerMock is called + await waitFor(() => expect(shellInContainerMock).toHaveBeenCalledTimes(2)); // wait 1s that everything is done await new Promise(resolve => setTimeout(resolve, 1000)); - // no new call to shellInContainerMock should be done - expect(shellInContainerMock).toHaveBeenCalledTimes(1); + const terminalLinesLiveRegion2 = renderObject.container.querySelector('div[aria-live="assertive"]'); + + // check the content + expect(terminalLinesLiveRegion2).toHaveTextContent('hello world'); + + // creating a new terminal requires new shellInContainer call + expect(shellInContainerMock).toHaveBeenCalledTimes(2); }); diff --git a/packages/renderer/src/lib/container/ContainerDetailsTerminal.svelte b/packages/renderer/src/lib/container/ContainerDetailsTerminal.svelte index 76e0dba5a98..9d2b297afac 100644 --- a/packages/renderer/src/lib/container/ContainerDetailsTerminal.svelte +++ b/packages/renderer/src/lib/container/ContainerDetailsTerminal.svelte @@ -3,6 +3,7 @@ import '@xterm/xterm/css/xterm.css'; import { EmptyScreen } from '@podman-desktop/ui-svelte'; import { FitAddon } from '@xterm/addon-fit'; +import { SerializeAddon } from '@xterm/addon-serialize'; import { Terminal } from '@xterm/xterm'; import { onDestroy, onMount } from 'svelte'; import { router } from 'tinro'; @@ -20,6 +21,8 @@ let terminalXtermDiv: HTMLDivElement; let shellTerminal: Terminal; let currentRouterPath: string; let sendCallbackId: number | undefined; +let terminalContent: string = ''; +let serializeAddon: SerializeAddon; // update current route scheme router.subscribe(route => { @@ -50,24 +53,22 @@ async function executeShellIntoContainer() { return; } - if (!sendCallbackId) { - // grab logs of the container - const callbackId = await window.shellInContainer( - container.engineId, - container.id, - receiveDataCallback, - () => {}, - receiveEndCallback, - ); - await window.shellInContainerResize(callbackId, shellTerminal.cols, shellTerminal.rows); - // pass data from xterm to container - shellTerminal?.onData(data => { - window.shellInContainerSend(callbackId, data); - }); + // grab logs of the container + const callbackId = await window.shellInContainer( + container.engineId, + container.id, + receiveDataCallback, + () => {}, + receiveEndCallback, + ); + await window.shellInContainerResize(callbackId, shellTerminal.cols, shellTerminal.rows); + // pass data from xterm to container + shellTerminal?.onData(data => { + window.shellInContainerSend(callbackId, data); + }); - // store it - sendCallbackId = callbackId; - } + // store it + sendCallbackId = callbackId; } // refresh @@ -88,24 +89,24 @@ async function refreshTerminal() { // get terminal if any const existingTerminal = getExistingTerminal(container.engineId, container.id); + shellTerminal = new Terminal({ + fontSize, + lineHeight, + screenReaderMode, + theme: getTerminalTheme(), + }); if (existingTerminal) { - sendCallbackId = existingTerminal.callbackId; - shellTerminal = existingTerminal.terminal; shellTerminal.options = { fontSize, lineHeight, }; - } else { - shellTerminal = new Terminal({ - fontSize, - lineHeight, - screenReaderMode, - theme: getTerminalTheme(), - }); + shellTerminal.write(existingTerminal.terminal); } const fitAddon = new FitAddon(); + serializeAddon = new SerializeAddon(); shellTerminal.loadAddon(fitAddon); + shellTerminal.loadAddon(serializeAddon); shellTerminal.open(terminalXtermDiv); @@ -126,13 +127,16 @@ onMount(async () => { }); onDestroy(() => { + terminalContent = serializeAddon.serialize(); // register terminal for reusing it registerTerminal({ engineId: container.engineId, containerId: container.id, - terminal: shellTerminal, + terminal: terminalContent, callbackId: sendCallbackId, }); + serializeAddon?.dispose(); + shellTerminal?.dispose(); }); diff --git a/packages/renderer/src/stores/container-terminal-store.ts b/packages/renderer/src/stores/container-terminal-store.ts index 6d946336f52..9791f319f54 100644 --- a/packages/renderer/src/stores/container-terminal-store.ts +++ b/packages/renderer/src/stores/container-terminal-store.ts @@ -16,7 +16,6 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import type { Terminal } from '@xterm/xterm'; import type { Writable } from 'svelte/store'; import { get, writable } from 'svelte/store'; @@ -33,7 +32,7 @@ export interface TerminalOfContainer { // id of the callbacks callbackId?: number; - terminal: Terminal; + terminal: string; } /** @@ -68,6 +67,10 @@ containersInfos.subscribe(containers => { export function registerTerminal(terminal: TerminalOfContainer) { containerTerminals.update(terminals => { + // remove old instance(s) of terminal if exists + terminals = terminals.filter( + term => !(terminal.containerId === term.containerId && terminal.engineId === term.engineId), + ); terminals.push(terminal); return terminals; }); diff --git a/yarn.lock b/yarn.lock index 9f1e992c76b..eff065ae781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,6 +6974,11 @@ resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0.tgz#275fb8f6e14afa6e8a0c05d4ebc94523ff775396" integrity sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A== +"@xterm/addon-serialize@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0.tgz#f6e687708cacae67c4fc717fad37b9d11065897b" + integrity sha512-kGs8o6LWAmN1l2NpMp01/YkpxbmO4UrfWybeGu79Khw5K9+Krp7XhXbBTOTc3GJRRhd6EmILjpR8k5+odY39YQ== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"