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) // 1. Laravel Pusher/Echo connection (not possible without a sdk)
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) // 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 // 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); $this->dispatch('send-back-command', $command);
} }

View File

@@ -53,6 +53,7 @@ services:
- "6002:6002" - "6002:6002"
volumes: volumes:
- ./storage:/var/www/html/storage - ./storage:/var/www/html/storage
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
environment: environment:
SOKETI_DEBUG: "false" SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" 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); const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) { 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(' '); const commandString = command[0].split('\n').join(' ');
@@ -129,7 +134,8 @@ async function handleCommand(ws, command, userId) {
userSession.ptyProcess = ptyProcess; userSession.ptyProcess = ptyProcess;
userSession.isActive = true; userSession.isActive = true;
ptyProcess.write(hereDocContent + '\n'); 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'); ws.send('pty-ready');
@@ -162,12 +168,32 @@ async function killPtyProcess(userId) {
if (!session?.ptyProcess) return false; if (!session?.ptyProcess) return false;
return new Promise((resolve) => { return new Promise((resolve) => {
session.ptyProcess.on('exit', () => { // Loop to ensure terminal is killed before continuing
session.isActive = false; let killAttempts = 0;
resolve(true); 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-data="data()">
<div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]"> <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
<div <div class="w-full h-full border rounded dark:bg-coolgray-100 dark:border-coolgray-300">
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" x-text="message"></span>
<span class="font-mono text-sm text-gray-500 ">(connection closed)</span>
</div> </div>
</div> </div>
<div x-ref="terminalWrapper" <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> <div id="terminal" wire:ignore></div>
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg <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"> class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -49,6 +48,7 @@
} }
function handleSocketMessage(event) { function handleSocketMessage(event) {
$data.message = '(connection closed)';
// Initialize Terminal // Initialize Terminal
if (event.data === 'pty-ready') { if (event.data === 'pty-ready') {
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
@@ -57,6 +57,10 @@
term.focus(); term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded') document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
$data.resizeTerminal() $data.resizeTerminal()
} else if (event.data === 'unprocessable') {
term.reset();
$data.terminalActive = false;
$data.message = '(sorry, something went wrong, please try again)';
} else { } else {
pendingWrites++; pendingWrites++;
term.write(event.data, flowControlCallback); term.write(event.data, flowControlCallback);
@@ -91,7 +95,6 @@
checkIfProcessIsRunningAndKillIt(); checkIfProcessIsRunningAndKillIt();
setTimeout(() => { setTimeout(() => {
term.reset(); term.reset();
term.write('(connection closed)');
$data.terminalActive = false; $data.terminalActive = false;
}, 500); }, 500);
commandBuffer = ''; commandBuffer = '';
@@ -152,6 +155,7 @@
Alpine.data('data', () => ({ Alpine.data('data', () => ({
fullscreen: false, fullscreen: false,
terminalActive: false, terminalActive: false,
message: '(connection closed)',
init() { init() {
this.$watch('terminalActive', (value) => { this.$watch('terminalActive', (value) => {
this.$nextTick(() => { this.$nextTick(() => {