fixes for terminal
This commit is contained in:
@@ -11,17 +11,19 @@ class Terminal extends Component
|
|||||||
#[On('send-terminal-command')]
|
#[On('send-terminal-command')]
|
||||||
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||||
{
|
{
|
||||||
$server = Server::whereUuid($serverUuid)->firstOrFail();
|
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||||
|
|
||||||
if (auth()->user()) {
|
// if (auth()->user()) {
|
||||||
$teams = auth()->user()->teams->pluck('id');
|
// $teams = auth()->user()->teams->pluck('id');
|
||||||
if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
|
// if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
|
||||||
throw new \Exception('User is not part of the team that owns this server');
|
// throw new \Exception('User is not part of the team that owns this server');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if ($isContainer) {
|
if ($isContainer) {
|
||||||
|
ray($identifier);
|
||||||
$status = getContainerStatus($server, $identifier);
|
$status = getContainerStatus($server, $identifier);
|
||||||
|
ray($status);
|
||||||
if ($status !== 'running') {
|
if ($status !== 'running') {
|
||||||
return handleError(new \Exception('Container is not running'), $this);
|
return handleError(new \Exception('Container is not running'), $this);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ class RunCommand extends Component
|
|||||||
|
|
||||||
private function getAllActiveContainers()
|
private function getAllActiveContainers()
|
||||||
{
|
{
|
||||||
return Server::all()->flatMap(function ($server) {
|
return collect($this->servers)->flatMap(function ($server) {
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -31,25 +30,52 @@ class RunCommand extends Component
|
|||||||
return $server->definedResources()
|
return $server->definedResources()
|
||||||
->filter(function ($resource) {
|
->filter(function ($resource) {
|
||||||
$status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited');
|
$status = method_exists($resource, 'realStatus') ? $resource->realStatus() : (method_exists($resource, 'status') ? $resource->status() : 'exited');
|
||||||
|
|
||||||
return str_starts_with($status, 'running:');
|
return str_starts_with($status, 'running:');
|
||||||
})
|
})
|
||||||
->map(function ($resource) use ($server) {
|
->map(function ($resource) use ($server) {
|
||||||
$container_name = $resource->uuid;
|
if (isDev()) {
|
||||||
|
if (data_get($resource, 'name') === 'coolify-db') {
|
||||||
|
$container_name = 'coolify-db';
|
||||||
|
|
||||||
if (class_basename($resource) === 'Application' || class_basename($resource) === 'Service') {
|
return [
|
||||||
if ($server->isSwarm()) {
|
'name' => $resource->name,
|
||||||
$container_name = $resource->uuid.'_'.$resource->uuid;
|
'connection_name' => $container_name,
|
||||||
} else {
|
'uuid' => $resource->uuid,
|
||||||
$current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true);
|
'status' => 'running',
|
||||||
$container_name = data_get($current_containers->first(), 'Names');
|
'server' => $server,
|
||||||
|
'server_uuid' => $server->uuid,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (class_basename($resource) === 'Application') {
|
||||||
|
if (! $server->isSwarm()) {
|
||||||
|
$current_containers = getCurrentApplicationContainerStatus($server, $resource->id, includePullrequests: true);
|
||||||
|
}
|
||||||
|
$status = $resource->status;
|
||||||
|
} elseif (class_basename($resource) === 'Service') {
|
||||||
|
$current_containers = getCurrentServiceContainerStatus($server, $resource->id);
|
||||||
|
$status = $resource->status();
|
||||||
|
} else {
|
||||||
|
$status = getContainerStatus($server, $resource->uuid);
|
||||||
|
if ($status === 'running') {
|
||||||
|
$current_containers = collect([
|
||||||
|
'Names' => $resource->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($server->isSwarm()) {
|
||||||
|
$container_name = $resource->uuid.'_'.$resource->uuid;
|
||||||
|
} else {
|
||||||
|
$container_name = data_get($current_containers->first(), 'Names');
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $resource->name,
|
'name' => $resource->name,
|
||||||
'connection_name' => $container_name,
|
'connection_name' => $container_name,
|
||||||
'uuid' => $resource->uuid,
|
'uuid' => $resource->uuid,
|
||||||
'status' => $resource->status,
|
'status' => $status,
|
||||||
'server' => $server,
|
'server' => $server,
|
||||||
'server_uuid' => $server->uuid,
|
'server_uuid' => $server->uuid,
|
||||||
];
|
];
|
||||||
|
@@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
|
|||||||
return $containers;
|
return $containers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentServiceContainerStatus(Server $server, int $id): Collection
|
||||||
|
{
|
||||||
|
$containers = collect([]);
|
||||||
|
if (! $server->isSwarm()) {
|
||||||
|
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server);
|
||||||
|
$containers = format_docker_command_output_to_json($containers);
|
||||||
|
$containers = $containers->filter();
|
||||||
|
|
||||||
|
return $containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $containers;
|
||||||
|
}
|
||||||
|
|
||||||
function format_docker_command_output_to_json($rawOutput): Collection
|
function format_docker_command_output_to_json($rawOutput): Collection
|
||||||
{
|
{
|
||||||
$outputLines = explode(PHP_EOL, $rawOutput);
|
$outputLines = explode(PHP_EOL, $rawOutput);
|
||||||
|
@@ -43,25 +43,23 @@ services:
|
|||||||
- /data/coolify/_volumes/redis/:/data
|
- /data/coolify/_volumes/redis/:/data
|
||||||
# - coolify-redis-data-dev:/data
|
# - coolify-redis-data-dev:/data
|
||||||
soketi:
|
soketi:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./docker/coolify-realtime/Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/soketi-entrypoint/soketi-entrypoint.sh:/soketi-entrypoint.sh
|
|
||||||
- ./package.json:/terminal/package.json
|
|
||||||
- ./package-lock.json:/terminal/package-lock.json
|
|
||||||
- ./terminal-server.js:/terminal/terminal-server.js
|
|
||||||
- ./storage:/var/www/html/storage
|
- ./storage:/var/www/html/storage
|
||||||
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
|
|
||||||
environment:
|
environment:
|
||||||
SOKETI_DEBUG: "false"
|
SOKETI_DEBUG: "false"
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||||
vite:
|
vite:
|
||||||
image: node:alpine
|
image: node:20
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
working_dir: /var/www/html
|
working_dir: /var/www/html
|
||||||
# environment:
|
# environment:
|
||||||
|
@@ -109,6 +109,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
|
image: 'ghcr.io/coollabsio/coolify-realtime:latest'
|
||||||
ports:
|
ports:
|
||||||
- "${SOKETI_PORT:-6001}:6001"
|
- "${SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
|
@@ -102,8 +102,8 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
image: 'quay.io/soketi/soketi:1.6-16-alpine'
|
image: 'ghcr.io/coollabsio/coolify-realtime:latest'
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify-realtime
|
container_name: coolify-realtime
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -24,7 +24,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- coolify
|
- coolify
|
||||||
soketi:
|
soketi:
|
||||||
image: 'quay.io/soketi/soketi:1.6-16-alpine'
|
|
||||||
container_name: coolify-realtime
|
container_name: coolify-realtime
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
|
9
docker/coolify-realtime/Dockerfile
Normal file
9
docker/coolify-realtime/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM quay.io/soketi/soketi:1.6-16-alpine
|
||||||
|
WORKDIR /terminal
|
||||||
|
RUN apk add --no-cache openssh-client make g++ python3
|
||||||
|
COPY docker/coolify-realtime/package.json ./
|
||||||
|
RUN npm i
|
||||||
|
RUN npm rebuild node-pty --update-binary
|
||||||
|
COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh
|
||||||
|
COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js
|
||||||
|
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]
|
13
docker/coolify-realtime/package.json
Normal file
13
docker/coolify-realtime/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
|
"axios": "1.7.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"node-pty": "^1.0.0",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Install openssh-client
|
|
||||||
apk add --no-cache openssh-client make g++ python3
|
|
||||||
|
|
||||||
cd /terminal
|
|
||||||
|
|
||||||
# Install npm dependencies
|
|
||||||
npm ci
|
|
||||||
|
|
||||||
# Rebuild node-pty
|
|
||||||
npm rebuild node-pty --update-binary
|
|
||||||
|
|
||||||
# Function to timestamp logs
|
# Function to timestamp logs
|
||||||
timestamp() {
|
timestamp() {
|
||||||
date "+%Y-%m-%d %H:%M:%S"
|
date "+%Y-%m-%d %H:%M:%S"
|
@@ -16,40 +16,40 @@ const server = http.createServer((req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const verifyClient = async (info, callback) => {
|
const verifyClient = async (info, callback) => {
|
||||||
const cookies = cookie.parse(info.req.headers.cookie || '');
|
const cookies = cookie.parse(info.req.headers.cookie || '');
|
||||||
const origin = new URL(info.origin);
|
const origin = new URL(info.origin);
|
||||||
const protocol = origin.protocol;
|
const protocol = origin.protocol;
|
||||||
const xsrfToken = cookies['XSRF-TOKEN'];
|
const xsrfToken = cookies['XSRF-TOKEN'];
|
||||||
|
|
||||||
// Generate session cookie name based on APP_NAME
|
// Generate session cookie name based on APP_NAME
|
||||||
const appName = process.env.APP_NAME || 'laravel';
|
const appName = process.env.APP_NAME || 'laravel';
|
||||||
const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`;
|
const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`;
|
||||||
const laravelSession = cookies[sessionCookieName];
|
const laravelSession = cookies[sessionCookieName];
|
||||||
|
|
||||||
// Verify presence of required tokens
|
// Verify presence of required tokens
|
||||||
if (!laravelSession || !xsrfToken) {
|
if (!laravelSession || !xsrfToken) {
|
||||||
return callback(false, 401, 'Unauthorized: Missing required tokens');
|
return callback(false, 401, 'Unauthorized: Missing required tokens');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Authenticate with Laravel backend
|
// Authenticate with Laravel backend
|
||||||
const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, {
|
const response = await axios.post(`${protocol}//coolify/terminal/auth`, null, {
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': `${sessionCookieName}=${laravelSession}`,
|
'Cookie': `${sessionCookieName}=${laravelSession}`,
|
||||||
'X-XSRF-TOKEN': xsrfToken
|
'X-XSRF-TOKEN': xsrfToken
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// Authentication successful
|
// Authentication successful
|
||||||
callback(true);
|
callback(true);
|
||||||
} else {
|
} else {
|
||||||
callback(false, 401, 'Unauthorized: Invalid credentials');
|
callback(false, 401, 'Unauthorized: Invalid credentials');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication error:', error.message);
|
||||||
|
callback(false, 500, 'Internal Server Error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Authentication error:', error.message);
|
|
||||||
callback(false, 500, 'Internal Server Error');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@@ -1,11 +1,12 @@
|
|||||||
<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 class="w-full h-full border rounded dark:bg-coolgray-100 dark:border-coolgray-300">
|
<div
|
||||||
<span class="font-mono text-sm text-gray-500">(connection closed)</span>
|
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>
|
</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">
|
||||||
@@ -24,169 +25,171 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@script
|
@script
|
||||||
<script>
|
<script>
|
||||||
const MAX_PENDING_WRITES = 5;
|
const MAX_PENDING_WRITES = 5;
|
||||||
let pendingWrites = 0;
|
let pendingWrites = 0;
|
||||||
let paused = false;
|
let paused = false;
|
||||||
|
|
||||||
let socket;
|
let socket;
|
||||||
let commandBuffer = '';
|
let commandBuffer = '';
|
||||||
|
|
||||||
function initializeWebSocket() {
|
function initializeWebSocket() {
|
||||||
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
||||||
const url = "{{ str_replace(['http://', 'https://'], '', config('app.url')) }}" || window.location.hostname;
|
let url = "{{ str_replace(['http://', 'https://'], '', config('app.url')) }}" || window.location.hostname;
|
||||||
socket = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
// make sure the port is not included
|
||||||
url +
|
url = url.split(':')[0];
|
||||||
':6002/terminal');
|
socket = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
||||||
socket.onmessage = handleSocketMessage;
|
url +
|
||||||
socket.onerror = (e) => {
|
':6002/terminal');
|
||||||
console.error('WebSocket error:', e);
|
socket.onmessage = handleSocketMessage;
|
||||||
};
|
socket.onerror = (e) => {
|
||||||
}
|
console.error('WebSocket error:', e);
|
||||||
}
|
};
|
||||||
|
|
||||||
function handleSocketMessage(event) {
|
|
||||||
// Initialize Terminal
|
|
||||||
if (event.data === 'pty-ready') {
|
|
||||||
term.open(document.getElementById('terminal'));
|
|
||||||
$data.terminalActive = true;
|
|
||||||
term.reset();
|
|
||||||
term.focus();
|
|
||||||
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
|
|
||||||
$data.resizeTerminal()
|
|
||||||
} else {
|
|
||||||
pendingWrites++;
|
|
||||||
term.write(event.data, flowControlCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function flowControlCallback() {
|
|
||||||
pendingWrites--;
|
|
||||||
if (pendingWrites > MAX_PENDING_WRITES && !paused) {
|
|
||||||
paused = true;
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
pause: true
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pendingWrites <= MAX_PENDING_WRITES && paused) {
|
|
||||||
paused = false;
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
resume: true
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
term.onData((data) => {
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
message: data
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Type CTRL + D or exit in the terminal
|
|
||||||
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim() === 'exit')) {
|
|
||||||
checkIfProcessIsRunningAndKillIt();
|
|
||||||
setTimeout(() => {
|
|
||||||
term.reset();
|
|
||||||
term.write('(connection closed)');
|
|
||||||
$data.terminalActive = false;
|
|
||||||
}, 500);
|
|
||||||
commandBuffer = '';
|
|
||||||
} else if (data === '\r') {
|
|
||||||
commandBuffer = '';
|
|
||||||
} else {
|
|
||||||
commandBuffer += data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function stripAnsiCommands(input) {
|
|
||||||
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy and paste
|
|
||||||
// Enables ctrl + c and ctrl + v
|
|
||||||
// defaults otherwise to ctrl + insert, shift + insert
|
|
||||||
term.attachCustomKeyEventHandler((arg) => {
|
|
||||||
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
|
|
||||||
navigator.clipboard.readText()
|
|
||||||
.then(text => {
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
message: text
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
|
|
||||||
const selection = term.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
navigator.clipboard.writeText(selection);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
$wire.on('send-back-command', function(command) {
|
function handleSocketMessage(event) {
|
||||||
socket.send(JSON.stringify({
|
// Initialize Terminal
|
||||||
command: command
|
if (event.data === 'pty-ready') {
|
||||||
}));
|
term.open(document.getElementById('terminal'));
|
||||||
});
|
$data.terminalActive = true;
|
||||||
|
term.reset();
|
||||||
|
term.focus();
|
||||||
|
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
|
||||||
|
$data.resizeTerminal()
|
||||||
|
} else {
|
||||||
|
pendingWrites++;
|
||||||
|
term.write(event.data, flowControlCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function(e) {
|
function flowControlCallback() {
|
||||||
checkIfProcessIsRunningAndKillIt();
|
pendingWrites--;
|
||||||
});
|
if (pendingWrites > MAX_PENDING_WRITES && !paused) {
|
||||||
|
paused = true;
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
pause: true
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pendingWrites <= MAX_PENDING_WRITES && paused) {
|
||||||
|
paused = false;
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
resume: true
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkIfProcessIsRunningAndKillIt() {
|
term.onData((data) => {
|
||||||
socket.send(JSON.stringify({
|
|
||||||
checkActive: 'force'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onresize = function() {
|
|
||||||
$data.resizeTerminal()
|
|
||||||
};
|
|
||||||
|
|
||||||
Alpine.data('data', () => ({
|
|
||||||
fullscreen: false,
|
|
||||||
terminalActive: false,
|
|
||||||
init() {
|
|
||||||
this.$watch('terminalActive', (value) => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (value) {
|
|
||||||
$refs.terminalWrapper.style.display = 'block';
|
|
||||||
this.resizeTerminal();
|
|
||||||
} else {
|
|
||||||
$refs.terminalWrapper.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
makeFullscreen() {
|
|
||||||
this.fullscreen = !this.fullscreen;
|
|
||||||
$nextTick(() => {
|
|
||||||
this.resizeTerminal()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
resizeTerminal() {
|
|
||||||
if (!this.terminalActive) return;
|
|
||||||
|
|
||||||
fitAddon.fit();
|
|
||||||
const height = $refs.terminalWrapper.clientHeight;
|
|
||||||
const rows = height / term._core._renderService._charSizeService.height - 1;
|
|
||||||
var termWidth = term.cols;
|
|
||||||
var termHeight = parseInt(rows.toString(), 10);
|
|
||||||
term.resize(termWidth, termHeight);
|
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
resize: {
|
message: data
|
||||||
cols: termWidth,
|
}));
|
||||||
rows: termHeight
|
|
||||||
|
// Type CTRL + D or exit in the terminal
|
||||||
|
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim() === 'exit')) {
|
||||||
|
checkIfProcessIsRunningAndKillIt();
|
||||||
|
setTimeout(() => {
|
||||||
|
term.reset();
|
||||||
|
term.write('(connection closed)');
|
||||||
|
$data.terminalActive = false;
|
||||||
|
}, 500);
|
||||||
|
commandBuffer = '';
|
||||||
|
} else if (data === '\r') {
|
||||||
|
commandBuffer = '';
|
||||||
|
} else {
|
||||||
|
commandBuffer += data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function stripAnsiCommands(input) {
|
||||||
|
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy and paste
|
||||||
|
// Enables ctrl + c and ctrl + v
|
||||||
|
// defaults otherwise to ctrl + insert, shift + insert
|
||||||
|
term.attachCustomKeyEventHandler((arg) => {
|
||||||
|
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
|
||||||
|
navigator.clipboard.readText()
|
||||||
|
.then(text => {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
message: text
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
|
||||||
|
const selection = term.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
navigator.clipboard.writeText(selection);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$wire.on('send-back-command', function(command) {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
command: command
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
checkIfProcessIsRunningAndKillIt();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkIfProcessIsRunningAndKillIt() {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
checkActive: 'force'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
initializeWebSocket();
|
window.onresize = function() {
|
||||||
</script>
|
$data.resizeTerminal()
|
||||||
|
};
|
||||||
|
|
||||||
|
Alpine.data('data', () => ({
|
||||||
|
fullscreen: false,
|
||||||
|
terminalActive: false,
|
||||||
|
init() {
|
||||||
|
this.$watch('terminalActive', (value) => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (value) {
|
||||||
|
$refs.terminalWrapper.style.display = 'block';
|
||||||
|
this.resizeTerminal();
|
||||||
|
} else {
|
||||||
|
$refs.terminalWrapper.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
makeFullscreen() {
|
||||||
|
this.fullscreen = !this.fullscreen;
|
||||||
|
$nextTick(() => {
|
||||||
|
this.resizeTerminal()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
resizeTerminal() {
|
||||||
|
if (!this.terminalActive) return;
|
||||||
|
|
||||||
|
fitAddon.fit();
|
||||||
|
const height = $refs.terminalWrapper.clientHeight;
|
||||||
|
const rows = height / term._core._renderService._charSizeService.height - 1;
|
||||||
|
var termWidth = term.cols;
|
||||||
|
var termHeight = parseInt(rows.toString(), 10);
|
||||||
|
term.resize(termWidth, termHeight);
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
resize: {
|
||||||
|
cols: termWidth,
|
||||||
|
rows: termHeight
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
initializeWebSocket();
|
||||||
|
</script>
|
||||||
@endscript
|
@endscript
|
||||||
</div>
|
</div>
|
Reference in New Issue
Block a user