fix: grouped process and docker execs are killed with ssh process

fix: run clear command only if exists
fix: link terminal js on dev compose better dx
fix: add error on terminal ux
This commit is contained in:
Luan Estradioto
2024-09-12 01:58:56 -03:00
parent 2edcd01493
commit 35dfb1b0f8
4 changed files with 46 additions and 12 deletions

View File

@@ -38,6 +38,9 @@ class Terminal extends Component
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
// 4. Follow-up discussions here:
// - https://github.com/coollabsio/coolify/issues/2298
// - https://github.com/coollabsio/coolify/discussions/3362
$this->dispatch('send-back-command', $command);
}

View File

@@ -53,6 +53,7 @@ services:
- "6002:6002"
volumes:
- ./storage:/var/www/html/storage
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
environment:
SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"

View File

@@ -106,7 +106,12 @@ async function handleCommand(ws, command, userId) {
const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) {
await killPtyProcess(userId);
const result = await killPtyProcess(userId);
if (!result) {
// if terminal is still active, even after we tried to kill it, dont continue and show error
ws.send('unprocessable');
return;
}
}
const commandString = command[0].split('\n').join(' ');
@@ -129,7 +134,8 @@ async function handleCommand(ws, command, userId) {
userSession.ptyProcess = ptyProcess;
userSession.isActive = true;
ptyProcess.write(hereDocContent + '\n');
ptyProcess.write('clear\n');
// clear the terminal if the user has clear command
ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n');
ws.send('pty-ready');
@@ -162,12 +168,32 @@ async function killPtyProcess(userId) {
if (!session?.ptyProcess) return false;
return new Promise((resolve) => {
session.ptyProcess.on('exit', () => {
session.isActive = false;
resolve(true);
});
// Loop to ensure terminal is killed before continuing
let killAttempts = 0;
const maxAttempts = 5;
session.ptyProcess.kill();
const attemptKill = () => {
killAttempts++;
// session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098
// patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947
session.ptyProcess.write('kill -TERM -$$ && exit\n');
setTimeout(() => {
if (!session.isActive || !session.ptyProcess) {
resolve(true);
return;
}
if (killAttempts < maxAttempts) {
attemptKill();
} else {
resolve(false);
}
}, 500);
};
attemptKill();
});
}

View File

@@ -1,12 +1,11 @@
<div x-data="data()">
<div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
<div
class="w-full h-full bg-white border border-solid rounded dark:text-white dark:bg-coolgray-100 scrollbar border-neutral-300 dark:border-coolgray-300 p-1">
<span class="font-mono text-sm text-gray-500 ">(connection closed)</span>
<div class="w-full h-full border rounded dark:bg-coolgray-100 dark:border-coolgray-300">
<span class="font-mono text-sm text-gray-500" x-text="message"></span>
</div>
</div>
<div x-ref="terminalWrapper"
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
<div id="terminal" wire:ignore></div>
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -49,6 +48,7 @@
}
function handleSocketMessage(event) {
$data.message = '(connection closed)';
// Initialize Terminal
if (event.data === 'pty-ready') {
term.open(document.getElementById('terminal'));
@@ -57,6 +57,10 @@
term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
$data.resizeTerminal()
} else if (event.data === 'unprocessable') {
term.reset();
$data.terminalActive = false;
$data.message = '(sorry, something went wrong, please try again)';
} else {
pendingWrites++;
term.write(event.data, flowControlCallback);
@@ -91,7 +95,6 @@
checkIfProcessIsRunningAndKillIt();
setTimeout(() => {
term.reset();
term.write('(connection closed)');
$data.terminalActive = false;
}, 500);
commandBuffer = '';
@@ -152,6 +155,7 @@
Alpine.data('data', () => ({
fullscreen: false,
terminalActive: false,
message: '(connection closed)',
init() {
this.$watch('terminalActive', (value) => {
this.$nextTick(() => {