diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a634114810e..6b87b5dbf6d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ IMPROVEMENTS: * api: Added node purge SDK functionality. [[GH-8142](https://github.com/hashicorp/nomad/issues/8142)] * csi: Improved the accuracy of plugin `Expected` allocation counts. [[GH-8699](https://github.com/hashicorp/nomad/pull/8699)] * driver/docker: Allow configurable image pull context timeout setting. [[GH-5718](https://github.com/hashicorp/nomad/issues/5718)] + * ui: Added exec keepalive heartbeat. [[GH-8759](https://github.com/hashicorp/nomad/pull/8759)] BUG FIXES: diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index c4a3dc2d9824..2f9669b1fc09 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -3,6 +3,8 @@ const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; import base64js from 'base64-js'; import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; +export const HEARTBEAT_INTERVAL = 10000; // ten seconds + export default class ExecSocketXtermAdapter { constructor(terminal, socket, token) { this.terminal = terminal; @@ -12,6 +14,7 @@ export default class ExecSocketXtermAdapter { socket.onopen = () => { this.sendWsHandshake(); this.sendTtySize(); + this.startHeartbeat(); terminal.onData(data => { this.handleData(data); @@ -28,6 +31,7 @@ export default class ExecSocketXtermAdapter { }; socket.onclose = () => { + this.stopHeartbeat(); this.terminal.writeln(''); this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('The connection has closed.'); @@ -49,6 +53,16 @@ export default class ExecSocketXtermAdapter { this.socket.send(JSON.stringify({ version: 1, auth_token: this.token || '' })); } + startHeartbeat() { + this.heartbeatTimer = setInterval(() => { + this.socket.send(JSON.stringify({})); + }, HEARTBEAT_INTERVAL); + } + + stopHeartbeat() { + clearInterval(this.heartbeatTimer); + } + handleData(data) { this.socket.send(JSON.stringify({ stdin: { data: encodeString(data) } })); } diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index c3276514d848..d6b78b109be5 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -4,6 +4,8 @@ import { module, test } from 'qunit'; import { render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { Terminal } from 'xterm'; +import { HEARTBEAT_INTERVAL } from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; +import sinon from 'sinon'; module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { setupRenderingTest(hooks); @@ -26,6 +28,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { if (firstMessage) { firstMessage = false; assert.deepEqual(message, JSON.stringify({ version: 1, auth_token: 'mysecrettoken' })); + mockSocket.onclose(); done(); } }, @@ -56,6 +59,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { if (firstMessage) { firstMessage = false; assert.deepEqual(message, JSON.stringify({ version: 1, auth_token: '' })); + mockSocket.onclose(); done(); } }, @@ -68,6 +72,40 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { await settled(); }); + test('a heartbeat is sent periodically', async function(assert) { + let done = assert.async(); + + const clock = sinon.useFakeTimers({ + now: new Date(), + shouldAdvanceTime: true, + }); + + let terminal = new Terminal(); + this.set('terminal', terminal); + + await render(hbs` + + `); + + await settled(); + + let mockSocket = new Object({ + send(message) { + if (!message.includes('version') && !message.includes('tty_size')) { + assert.deepEqual(message, JSON.stringify({})); + clock.restore(); + mockSocket.onclose(); + done(); + } + }, + }); + + new ExecSocketXtermAdapter(terminal, mockSocket, null); + mockSocket.onopen(); + await settled(); + clock.tick(HEARTBEAT_INTERVAL); + }); + test('resizing the window passes a resize message through the socket', async function(assert) { let done = assert.async(); @@ -86,6 +124,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { message, JSON.stringify({ tty_size: { width: terminal.cols, height: terminal.rows } }) ); + mockSocket.onclose(); done(); }, }); @@ -120,6 +159,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { }); await settled(); + mockSocket.onclose(); }); test('stderr frames are ignored', async function(assert) { @@ -155,5 +195,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { .trim(), 'sh-3.2 🥳$' ); + + mockSocket.onclose(); }); });