random changes
This commit is contained in:
parent
f4d88e0ded
commit
db04774fb4
403
app.js
403
app.js
@ -1,49 +1,50 @@
|
|||||||
import Hyperswarm from 'hyperswarm';
|
import Hyperswarm from 'hyperswarm';
|
||||||
import b4a from 'b4a';
|
import b4a from 'b4a';
|
||||||
import { Terminal } from 'xterm';
|
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
|
||||||
|
|
||||||
const swarm = new Hyperswarm();
|
|
||||||
const connections = {};
|
|
||||||
let activePeer = null;
|
|
||||||
let terminalSessions = {}; // Track terminal states per container
|
|
||||||
let xterm = null; // The current terminal instance
|
|
||||||
let fitAddon = null; // FitAddon instance
|
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const terminalModal = document.getElementById('terminal-modal');
|
|
||||||
const terminalTitle = document.getElementById('terminal-title');
|
|
||||||
const terminalContainer = document.getElementById('terminal-container');
|
|
||||||
const tray = document.getElementById('tray');
|
|
||||||
const containerList = document.getElementById('container-list');
|
const containerList = document.getElementById('container-list');
|
||||||
|
const connectionList = document.getElementById('connection-list');
|
||||||
|
const addConnectionForm = document.getElementById('add-connection-form');
|
||||||
|
const newConnectionTopic = document.getElementById('new-connection-topic');
|
||||||
|
const connectionTitle = document.getElementById('connection-title');
|
||||||
|
const dashboard = document.getElementById('dashboard');
|
||||||
|
|
||||||
|
// Modal Elements
|
||||||
|
const duplicateModalElement = document.getElementById('duplicateModal');
|
||||||
|
const duplicateModal = new bootstrap.Modal(duplicateModalElement);
|
||||||
|
const duplicateContainerForm = document.getElementById('duplicate-container-form');
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
const connections = {};
|
||||||
|
let activePeer = null;
|
||||||
|
window.activePeer = null; // Expose to other modules
|
||||||
|
|
||||||
// Initialize the app
|
// Initialize the app
|
||||||
console.log('[INFO] Client app initialized');
|
console.log('[INFO] Client app initialized');
|
||||||
|
|
||||||
// Add Kill Terminal button functionality
|
// Collapse Sidebar Functionality
|
||||||
document.getElementById('kill-terminal-btn').onclick = () => {
|
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
|
||||||
const containerId = terminalTitle.dataset.containerId;
|
collapseSidebarBtn.addEventListener('click', () => {
|
||||||
if (containerId && terminalSessions[containerId]) {
|
const sidebar = document.getElementById('sidebar');
|
||||||
console.log(`[INFO] Killing terminal session for container: ${containerId}`);
|
sidebar.classList.toggle('collapsed');
|
||||||
|
const btn = collapseSidebarBtn;
|
||||||
// Send kill command to server
|
btn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
|
||||||
window.sendCommand('killTerminal', { containerId });
|
});
|
||||||
|
|
||||||
// Clean up terminal state
|
|
||||||
terminalModal.style.display = 'none';
|
|
||||||
delete terminalSessions[containerId];
|
|
||||||
xterm.dispose(); // Dispose of the current terminal instance
|
|
||||||
xterm = null; // Reset xterm instance
|
|
||||||
} else {
|
|
||||||
console.error('[ERROR] No terminal session found to kill.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new connection
|
// Add a new connection
|
||||||
document.getElementById('add-connection-form').addEventListener('submit', (e) => {
|
addConnectionForm.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const topicHex = document.getElementById('new-connection-topic').value;
|
const topicHex = newConnectionTopic.value.trim();
|
||||||
|
if (topicHex) {
|
||||||
|
addConnection(topicHex);
|
||||||
|
newConnectionTopic.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to add a new connection
|
||||||
|
function addConnection(topicHex) {
|
||||||
const topic = b4a.from(topicHex, 'hex');
|
const topic = b4a.from(topicHex, 'hex');
|
||||||
const topicId = topicHex.substring(0, 12);
|
const topicId = topicHex.substring(0, 12);
|
||||||
|
|
||||||
@ -55,14 +56,27 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) =>
|
|||||||
connectionItem.innerHTML = `
|
connectionItem.innerHTML = `
|
||||||
<span class="connection-status status-disconnected"></span>${topicId}
|
<span class="connection-status status-disconnected"></span>${topicId}
|
||||||
`;
|
`;
|
||||||
connectionItem.addEventListener('click', () => window.switchConnection(topicId));
|
connectionItem.addEventListener('click', () => switchConnection(topicId));
|
||||||
document.getElementById('connection-list').appendChild(connectionItem);
|
connectionList.appendChild(connectionItem);
|
||||||
|
|
||||||
connections[topicId] = { topic, peer: null };
|
connections[topicId] = { topic, peer: null, swarm: null };
|
||||||
|
|
||||||
|
// Create a new swarm for this connection
|
||||||
|
const swarm = new Hyperswarm();
|
||||||
|
connections[topicId].swarm = swarm;
|
||||||
|
|
||||||
swarm.join(topic, { client: true, server: false });
|
swarm.join(topic, { client: true, server: false });
|
||||||
|
|
||||||
swarm.on('connection', (peer) => {
|
swarm.on('connection', (peer) => {
|
||||||
console.log(`[INFO] Connected to peer for topic: ${topicHex}`);
|
console.log(`[INFO] Connected to peer for topic: ${topicHex}`);
|
||||||
|
|
||||||
|
// Prevent duplicate connections
|
||||||
|
if (connections[topicId].peer) {
|
||||||
|
console.warn(`[WARN] Duplicate connection detected for topic: ${topicId}. Closing.`);
|
||||||
|
peer.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
connections[topicId].peer = peer;
|
connections[topicId].peer = peer;
|
||||||
updateConnectionStatus(topicId, true);
|
updateConnectionStatus(topicId, true);
|
||||||
|
|
||||||
@ -72,9 +86,20 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) =>
|
|||||||
console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`);
|
console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`);
|
||||||
|
|
||||||
if (response.type === 'containers') {
|
if (response.type === 'containers') {
|
||||||
renderContainers(response.data);
|
if (window.activePeer === peer) {
|
||||||
|
renderContainers(response.data);
|
||||||
|
}
|
||||||
} else if (response.type === 'terminalOutput') {
|
} else if (response.type === 'terminalOutput') {
|
||||||
appendTerminalOutput(response.data, response.containerId);
|
appendTerminalOutput(response.data, response.containerId, response.encoding);
|
||||||
|
} else if (response.type === 'containerConfig') {
|
||||||
|
if (window.inspectContainerCallback) {
|
||||||
|
window.inspectContainerCallback(response.data);
|
||||||
|
window.inspectContainerCallback = null; // Reset the callback
|
||||||
|
}
|
||||||
|
} else if (response.type === 'stats') {
|
||||||
|
updateContainerStats(response.data);
|
||||||
|
} else if (response.error) {
|
||||||
|
console.log(`Error: ${response.error}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[ERROR] Failed to parse data from server: ${err.message}`);
|
console.error(`[ERROR] Failed to parse data from server: ${err.message}`);
|
||||||
@ -84,41 +109,50 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) =>
|
|||||||
peer.on('close', () => {
|
peer.on('close', () => {
|
||||||
console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
|
console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
|
||||||
updateConnectionStatus(topicId, false);
|
updateConnectionStatus(topicId, false);
|
||||||
});
|
connections[topicId].peer = null; // Clear the peer reference
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collapse/Expand Sidebar
|
if (window.activePeer === peer) {
|
||||||
document.getElementById('collapse-sidebar-btn').addEventListener('click', () => {
|
window.activePeer = null;
|
||||||
const sidebar = document.getElementById('sidebar');
|
connectionTitle.textContent = 'Disconnected';
|
||||||
sidebar.classList.toggle('collapsed');
|
dashboard.classList.add('hidden');
|
||||||
const btn = document.getElementById('collapse-sidebar-btn');
|
containerList.innerHTML = '';
|
||||||
btn.textContent = sidebar.classList.contains('collapsed') ? '>' : '<';
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If this is the first connection, switch to it
|
||||||
|
if (!window.activePeer) {
|
||||||
|
switchConnection(topicId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update connection status
|
// Update connection status
|
||||||
function updateConnectionStatus(topicId, isConnected) {
|
function updateConnectionStatus(topicId, isConnected) {
|
||||||
const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`);
|
const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`);
|
||||||
connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
|
if (connectionItem) {
|
||||||
|
connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch between connections
|
// Switch between connections
|
||||||
function switchConnection(topicId) {
|
function switchConnection(topicId) {
|
||||||
activePeer = connections[topicId].peer;
|
const connection = connections[topicId];
|
||||||
const connectionTitle = document.getElementById('connection-title');
|
if (!connection) {
|
||||||
if (!connectionTitle) {
|
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
|
||||||
console.error('[ERROR] Connection title element is missing.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
connectionTitle.textContent = `Connection: ${topicId}`;
|
|
||||||
document.getElementById('dashboard').classList.remove('hidden');
|
|
||||||
|
|
||||||
if (activePeer) {
|
if (!connection.peer) {
|
||||||
console.log('[INFO] Sending "listContainers" command');
|
console.error('[ERROR] No active peer for this connection.');
|
||||||
window.sendCommand('listContainers');
|
return;
|
||||||
} else {
|
|
||||||
console.error('[ERROR] No active peer to send command.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.activePeer = connection.peer;
|
||||||
|
connectionTitle.textContent = `Connection: ${topicId}`;
|
||||||
|
dashboard.classList.remove('hidden');
|
||||||
|
|
||||||
|
console.log('[INFO] Sending "listContainers" command');
|
||||||
|
sendCommand('listContainers');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach switchConnection to the global window object
|
// Attach switchConnection to the global window object
|
||||||
@ -126,10 +160,10 @@ window.switchConnection = switchConnection;
|
|||||||
|
|
||||||
// Send a command to the active peer
|
// Send a command to the active peer
|
||||||
function sendCommand(command, args = {}) {
|
function sendCommand(command, args = {}) {
|
||||||
if (activePeer) {
|
if (window.activePeer) {
|
||||||
const message = JSON.stringify({ command, args });
|
const message = JSON.stringify({ command, args });
|
||||||
console.log(`[DEBUG] Sending command to server: ${message}`);
|
console.log(`[DEBUG] Sending command to server: ${message}`);
|
||||||
activePeer.write(message);
|
window.activePeer.write(message);
|
||||||
} else {
|
} else {
|
||||||
console.error('[ERROR] No active peer to send command.');
|
console.error('[ERROR] No active peer to send command.');
|
||||||
}
|
}
|
||||||
@ -140,110 +174,165 @@ window.sendCommand = sendCommand;
|
|||||||
|
|
||||||
// Render the container list
|
// Render the container list
|
||||||
function renderContainers(containers) {
|
function renderContainers(containers) {
|
||||||
console.log(`[INFO] Rendering ${containers.length} containers`);
|
console.log(`[INFO] Rendering ${containers.length} containers`);
|
||||||
containerList.innerHTML = '';
|
containerList.innerHTML = ''; // Clear the current list
|
||||||
|
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const name = container.Names[0].replace(/^\//, ''); // Remove leading slash from container names
|
||||||
|
const image = container.Image;
|
||||||
|
const containerId = container.Id;
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.dataset.containerId = containerId; // Store container ID for reference
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${name}</td>
|
||||||
|
<td>${image}</td>
|
||||||
|
<td>${container.State}</td>
|
||||||
|
<td class="cpu">0</td>
|
||||||
|
<td class="memory">0</td>
|
||||||
|
<td class="ip-address">-</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-success btn-sm action-start" ${container.State === 'running' ? 'disabled' : ''}>Start</button>
|
||||||
|
<button class="btn btn-warning btn-sm action-stop" ${container.State !== 'running' ? 'disabled' : ''}>Stop</button>
|
||||||
|
<button class="btn btn-danger btn-sm action-remove">Remove</button>
|
||||||
|
<button class="btn btn-primary btn-sm action-terminal" ${container.State !== 'running' ? 'disabled' : ''}>Terminal</button>
|
||||||
|
<button class="btn btn-secondary btn-sm action-duplicate">Duplicate</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
containerList.appendChild(row);
|
||||||
|
|
||||||
|
// Add event listeners for action buttons
|
||||||
|
addActionListeners(row, container);
|
||||||
|
|
||||||
|
// Add event listener for duplicate button
|
||||||
|
const duplicateBtn = row.querySelector('.action-duplicate');
|
||||||
|
duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
containers.forEach((container) => {
|
// Add event listeners to action buttons
|
||||||
const name = container.Names[0].replace(/^\//, ''); // Remove leading slash from container names
|
function addActionListeners(row, container) {
|
||||||
const row = document.createElement('tr');
|
const startBtn = row.querySelector('.action-start');
|
||||||
row.innerHTML = `
|
const stopBtn = row.querySelector('.action-stop');
|
||||||
<td>${name}</td>
|
const removeBtn = row.querySelector('.action-remove');
|
||||||
<td>${container.State}</td>
|
const terminalBtn = row.querySelector('.action-terminal');
|
||||||
<td>
|
|
||||||
<button class="btn btn-success btn-sm" onclick="window.sendCommand('startContainer', { id: '${container.Id}' })">Start</button>
|
startBtn.addEventListener('click', () => {
|
||||||
<button class="btn btn-warning btn-sm" onclick="window.sendCommand('stopContainer', { id: '${container.Id}' })">Stop</button>
|
sendCommand('startContainer', { id: container.Id });
|
||||||
<button class="btn btn-danger btn-sm" onclick="window.sendCommand('removeContainer', { id: '${container.Id}' })">Remove</button>
|
});
|
||||||
<button class="btn btn-info btn-sm" onclick="window.startTerminal('${container.Id}', '${name}')">Terminal</button>
|
|
||||||
</td>
|
stopBtn.addEventListener('click', () => {
|
||||||
`;
|
sendCommand('stopContainer', { id: container.Id });
|
||||||
containerList.appendChild(row);
|
});
|
||||||
|
|
||||||
|
removeBtn.addEventListener('click', () => {
|
||||||
|
sendCommand('removeContainer', { id: container.Id });
|
||||||
|
});
|
||||||
|
|
||||||
|
terminalBtn.addEventListener('click', () => {
|
||||||
|
startTerminal(container.Id, container.Names[0] || container.Id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Xterm.js terminal
|
// Function to update container statistics
|
||||||
function initializeTerminal() {
|
function updateContainerStats(stats) {
|
||||||
if (xterm) {
|
console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`);
|
||||||
xterm.dispose(); // Dispose existing terminal instance
|
let row = containerList.querySelector(`tr[data-container-id="${stats.id}"]`);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
console.warn(`[WARN] No row found for container ID: ${stats.id}. Adding a placeholder.`);
|
||||||
|
// Create a placeholder row if it doesn't exist
|
||||||
|
row = document.createElement('tr');
|
||||||
|
row.dataset.containerId = stats.id;
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>Unknown</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td class="cpu">0</td>
|
||||||
|
<td class="memory">0</td>
|
||||||
|
<td class="ip-address">-</td>
|
||||||
|
<td>-</td>
|
||||||
|
`;
|
||||||
|
containerList.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.querySelector('.cpu').textContent = stats.cpu.toFixed(2);
|
||||||
|
row.querySelector('.memory').textContent = (stats.memory / (1024 * 1024)).toFixed(2);
|
||||||
|
row.querySelector('.ip-address').textContent = stats.ip || '-';
|
||||||
}
|
}
|
||||||
xterm = new Terminal({ cursorBlink: true, theme: { background: '#000000', foreground: '#ffffff' } });
|
|
||||||
fitAddon = new FitAddon(); // Create a new FitAddon instance
|
|
||||||
xterm.loadAddon(fitAddon);
|
|
||||||
|
|
||||||
xterm.open(terminalContainer);
|
// Function to open the Duplicate Modal with container configurations
|
||||||
setTimeout(() => fitAddon.fit(), 10); // Ensure terminal fits properly after rendering
|
function openDuplicateModal(container) {
|
||||||
xterm.write('\x1b[1;32mWelcome to the Docker Terminal\x1b[0m\r\n');
|
console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`);
|
||||||
|
|
||||||
|
// Send a command to get container configurations
|
||||||
|
sendCommand('inspectContainer', { id: container.Id });
|
||||||
|
|
||||||
|
// Listen for the response
|
||||||
|
window.inspectContainerCallback = (config) => {
|
||||||
|
if (!config) {
|
||||||
|
alert('Failed to retrieve container configuration.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the modal fields with the current configurations
|
||||||
|
document.getElementById('container-name').value = config.Name.replace(/^\//, '');
|
||||||
|
document.getElementById('container-image').value = config.Config.Image;
|
||||||
|
document.getElementById('container-config').value = JSON.stringify(config, null, 2);
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
duplicateModal.show();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Adjust terminal size dynamically when the window is resized
|
// Handle the Duplicate Container Form Submission
|
||||||
window.addEventListener('resize', () => {
|
duplicateContainerForm.addEventListener('submit', (e) => {
|
||||||
fitAddon.fit();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const name = document.getElementById('container-name').value.trim();
|
||||||
|
const image = document.getElementById('container-image').value.trim();
|
||||||
|
const configJSON = document.getElementById('container-config').value.trim();
|
||||||
|
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = JSON.parse(configJSON);
|
||||||
|
} catch (err) {
|
||||||
|
alert('Invalid JSON in configuration.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[INFO] Sending duplicateContainer command for name: ${name}`);
|
||||||
|
|
||||||
|
// Send the duplicate command to the server
|
||||||
|
sendCommand('duplicateContainer', { name, config });
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
duplicateModal.hide();
|
||||||
|
|
||||||
|
// Notify the user
|
||||||
|
alert('Duplicate command sent.');
|
||||||
|
|
||||||
|
// Trigger container list update after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[INFO] Fetching updated container list after duplication');
|
||||||
|
sendCommand('listContainers');
|
||||||
|
}, 2000); // Wait for duplication to complete
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Start terminal session
|
|
||||||
function startTerminal(containerId, containerName) {
|
|
||||||
if (!activePeer) {
|
|
||||||
console.error('[ERROR] No active peer for terminal.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!terminalSessions[containerId]) {
|
|
||||||
terminalSessions[containerId] = { output: '' }; // Initialize terminal state
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = terminalSessions[containerId];
|
|
||||||
initializeTerminal();
|
|
||||||
xterm.write(session.output);
|
|
||||||
|
|
||||||
terminalModal.style.display = 'flex';
|
|
||||||
terminalTitle.dataset.containerId = containerId;
|
|
||||||
terminalTitle.textContent = `Container Terminal: ${containerName}`;
|
|
||||||
|
|
||||||
console.log(`[INFO] Starting terminal for container: ${containerId}`);
|
|
||||||
activePeer.write(JSON.stringify({ command: 'startTerminal', args: { containerId } }));
|
|
||||||
|
|
||||||
xterm.onData((data) => {
|
|
||||||
console.log(`[DEBUG] Sending terminal input: ${data}`);
|
|
||||||
|
|
||||||
activePeer.write(JSON.stringify({ type: 'terminalInput', data }));
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('minimize-terminal-btn').onclick = () => {
|
|
||||||
terminalModal.style.display = 'none';
|
|
||||||
addToTray(containerId); // Minimize to tray
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach startTerminal to the global window object
|
// Attach startTerminal to the global window object
|
||||||
window.startTerminal = startTerminal;
|
window.startTerminal = startTerminal;
|
||||||
|
|
||||||
// Append terminal output
|
// Handle window unload to clean up swarms and peers
|
||||||
function appendTerminalOutput(data, containerId) {
|
window.addEventListener('beforeunload', () => {
|
||||||
if (!terminalSessions[containerId]) {
|
for (const topicId in connections) {
|
||||||
console.error(`[ERROR] No terminal session found for container: ${containerId}`);
|
const connection = connections[topicId];
|
||||||
return;
|
if (connection.peer) {
|
||||||
|
connection.peer.destroy();
|
||||||
|
}
|
||||||
|
if (connection.swarm) {
|
||||||
|
connection.swarm.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
console.log(`[DEBUG] Appending terminal output: ${data}`);
|
|
||||||
const session = terminalSessions[containerId];
|
|
||||||
session.output += data;
|
|
||||||
if (terminalTitle.dataset.containerId === containerId) {
|
|
||||||
xterm.write(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add terminal to tray
|
|
||||||
function addToTray(containerId) {
|
|
||||||
let trayItem = document.querySelector(`.tray-item[data-id="${containerId}"]`);
|
|
||||||
if (!trayItem) {
|
|
||||||
trayItem = document.createElement('div');
|
|
||||||
trayItem.className = 'tray-item';
|
|
||||||
trayItem.dataset.id = containerId;
|
|
||||||
trayItem.textContent = `Terminal: ${containerId}`;
|
|
||||||
trayItem.onclick = () => {
|
|
||||||
terminalModal.style.display = 'flex';
|
|
||||||
xterm.write(terminalSessions[containerId].output);
|
|
||||||
setTimeout(() => fitAddon.fit(), 10); // Ensure proper resize
|
|
||||||
};
|
|
||||||
tray.appendChild(trayItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
58
index.html
58
index.html
@ -2,10 +2,15 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
|
||||||
<title>Docker P2P Manager</title>
|
<title>Docker P2P Manager</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- xterm.css for Terminal -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -135,6 +140,7 @@
|
|||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
<pear-ctrl></pear-ctrl>
|
<pear-ctrl></pear-ctrl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<button id="collapse-sidebar-btn"><</button>
|
<button id="collapse-sidebar-btn"><</button>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -146,6 +152,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1 id="connection-title">Select a Connection</h1>
|
<h1 id="connection-title">Select a Connection</h1>
|
||||||
<div id="dashboard" class="hidden">
|
<div id="dashboard" class="hidden">
|
||||||
@ -154,7 +161,11 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Image</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>CPU (%)</th>
|
||||||
|
<th>Memory (MB)</th>
|
||||||
|
<th>IP Address</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -162,18 +173,57 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Duplicate Container Modal -->
|
||||||
|
<div class="modal fade" id="duplicateModal" tabindex="-1" aria-labelledby="duplicateModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content bg-dark text-white">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="duplicateModalLabel">Duplicate Container</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="duplicate-container-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="container-name" class="form-label">Container Name</label>
|
||||||
|
<input type="text" class="form-control" id="container-name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="container-image" class="form-label">Image</label>
|
||||||
|
<input type="text" class="form-control" id="container-image" required>
|
||||||
|
</div>
|
||||||
|
<!-- Container Configuration as JSON -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="container-config" class="form-label">Container Configuration (JSON)</label>
|
||||||
|
<textarea class="form-control" id="container-config" rows="10" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Deploy Duplicate</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terminal Modal -->
|
||||||
<div id="terminal-modal">
|
<div id="terminal-modal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span id="terminal-title"></span>
|
<span id="terminal-title"></span>
|
||||||
<div>
|
<div>
|
||||||
<button id="kill-terminal-btn" class="btn btn-sm btn-danger">Kill Terminal</button>
|
<button id="kill-terminal-btn" class="btn btn-sm btn-danger">Kill Terminal</button>
|
||||||
<button id="minimize-terminal-btn" class="btn btn-sm btn-secondary">Minimize</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="terminal-container"></div>
|
<div id="terminal-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tray"></div>
|
<div id="tray"></div>
|
||||||
|
|
||||||
|
<!-- xterm.js -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS for Modal Functionality -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Your App JS -->
|
||||||
<script type="module" src="app.js"></script>
|
<script type="module" src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
203
libs/terminal.js
Normal file
203
libs/terminal.js
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// terminal.js
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const terminalModal = document.getElementById('terminal-modal');
|
||||||
|
const terminalTitle = document.getElementById('terminal-title');
|
||||||
|
const terminalContainer = document.getElementById('terminal-container');
|
||||||
|
const tray = document.getElementById('tray');
|
||||||
|
|
||||||
|
// Terminal variables
|
||||||
|
let terminalSessions = {}; // Track terminal sessions per containerId
|
||||||
|
let activeContainerId = null; // Currently displayed containerId
|
||||||
|
|
||||||
|
// Kill Terminal button functionality
|
||||||
|
document.getElementById('kill-terminal-btn').onclick = () => {
|
||||||
|
const containerId = activeContainerId;
|
||||||
|
|
||||||
|
if (containerId && terminalSessions[containerId]) {
|
||||||
|
console.log(`[INFO] Killing terminal session for container: ${containerId}`);
|
||||||
|
|
||||||
|
// Send kill command to server
|
||||||
|
window.sendCommand('killTerminal', { containerId });
|
||||||
|
|
||||||
|
// Clean up terminal session
|
||||||
|
const session = terminalSessions[containerId];
|
||||||
|
session.xterm.dispose();
|
||||||
|
session.onDataDisposable.dispose();
|
||||||
|
if (session.resizeListener) {
|
||||||
|
window.removeEventListener('resize', session.resizeListener);
|
||||||
|
}
|
||||||
|
// Remove the terminal's container div from DOM
|
||||||
|
session.container.parentNode.removeChild(session.container);
|
||||||
|
|
||||||
|
delete terminalSessions[containerId];
|
||||||
|
|
||||||
|
// Remove from tray if exists
|
||||||
|
removeFromTray(containerId);
|
||||||
|
|
||||||
|
// Hide the terminal modal if this was the active session
|
||||||
|
if (activeContainerId === containerId) {
|
||||||
|
terminalModal.style.display = 'none';
|
||||||
|
activeContainerId = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('[ERROR] No terminal session found to kill.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start terminal session
|
||||||
|
function startTerminal(containerId, containerName) {
|
||||||
|
if (!window.activePeer) {
|
||||||
|
console.error('[ERROR] No active peer for terminal.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminalSessions[containerId]) {
|
||||||
|
// Terminal session already exists
|
||||||
|
console.log(`[INFO] Terminal session already exists for container: ${containerId}`);
|
||||||
|
|
||||||
|
// If terminal is minimized or not active, switch to it
|
||||||
|
if (activeContainerId !== containerId || terminalModal.style.display === 'none') {
|
||||||
|
switchTerminal(containerId);
|
||||||
|
} else {
|
||||||
|
console.log(`[INFO] Terminal for container ${containerId} is already active`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new terminal session
|
||||||
|
console.log(`[INFO] Creating new terminal session for container: ${containerId}`);
|
||||||
|
|
||||||
|
// Initialize new terminal session
|
||||||
|
const xterm = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
theme: { background: '#000000', foreground: '#ffffff' },
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
xterm.loadAddon(fitAddon);
|
||||||
|
|
||||||
|
// Create a container div for this terminal
|
||||||
|
const terminalDiv = document.createElement('div');
|
||||||
|
terminalDiv.style.width = '100%';
|
||||||
|
terminalDiv.style.height = '100%';
|
||||||
|
terminalDiv.style.display = 'none'; // Initially hidden
|
||||||
|
terminalContainer.appendChild(terminalDiv);
|
||||||
|
|
||||||
|
// Open the terminal in the container div
|
||||||
|
xterm.open(terminalDiv);
|
||||||
|
|
||||||
|
// Set up event listener for terminal input using onData
|
||||||
|
const onDataDisposable = xterm.onData((data) => {
|
||||||
|
console.log(`[DEBUG] Sending terminal input for container ${containerId}: ${data}`);
|
||||||
|
|
||||||
|
// Base64 encode the data
|
||||||
|
window.activePeer.write(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'terminalInput',
|
||||||
|
containerId,
|
||||||
|
data: btoa(data),
|
||||||
|
encoding: 'base64',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the terminal session
|
||||||
|
terminalSessions[containerId] = {
|
||||||
|
xterm,
|
||||||
|
fitAddon,
|
||||||
|
onDataDisposable,
|
||||||
|
output: '',
|
||||||
|
name: containerName,
|
||||||
|
resizeListener: null,
|
||||||
|
container: terminalDiv,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send startTerminal command to server
|
||||||
|
console.log(`[INFO] Starting terminal for container: ${containerId}`);
|
||||||
|
window.activePeer.write(
|
||||||
|
JSON.stringify({ command: 'startTerminal', args: { containerId } })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch to the requested terminal session
|
||||||
|
switchTerminal(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to a terminal session
|
||||||
|
function switchTerminal(containerId) {
|
||||||
|
const session = terminalSessions[containerId];
|
||||||
|
if (!session) {
|
||||||
|
console.error(`[ERROR] No terminal session found for container: ${containerId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the current terminal if any
|
||||||
|
if (activeContainerId && activeContainerId !== containerId) {
|
||||||
|
const currentSession = terminalSessions[activeContainerId];
|
||||||
|
if (currentSession) {
|
||||||
|
// Remove resize listener for current session
|
||||||
|
if (currentSession.resizeListener) {
|
||||||
|
window.removeEventListener('resize', currentSession.resizeListener);
|
||||||
|
currentSession.resizeListener = null;
|
||||||
|
}
|
||||||
|
// Hide current terminal's container
|
||||||
|
currentSession.container.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the terminal
|
||||||
|
session.container.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(() => session.fitAddon.fit(), 10); // Ensure terminal fits properly after rendering
|
||||||
|
|
||||||
|
// Update modal title
|
||||||
|
terminalTitle.dataset.containerId = containerId;
|
||||||
|
terminalTitle.textContent = `Container Terminal: ${session.name}`;
|
||||||
|
|
||||||
|
// Show the terminal modal
|
||||||
|
terminalModal.style.display = 'flex';
|
||||||
|
activeContainerId = containerId;
|
||||||
|
|
||||||
|
// Remove from tray if it exists
|
||||||
|
removeFromTray(containerId);
|
||||||
|
|
||||||
|
console.log(`[INFO] Switched to terminal for container: ${containerId}`);
|
||||||
|
|
||||||
|
// Adjust resize event listener
|
||||||
|
if (session.resizeListener) {
|
||||||
|
window.removeEventListener('resize', session.resizeListener);
|
||||||
|
}
|
||||||
|
session.resizeListener = () => session.fitAddon.fit();
|
||||||
|
window.addEventListener('resize', session.resizeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append terminal output
|
||||||
|
function appendTerminalOutput(data, containerId, encoding) {
|
||||||
|
const session = terminalSessions[containerId];
|
||||||
|
if (!session) {
|
||||||
|
console.error(`[ERROR] No terminal session found for container: ${containerId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputData;
|
||||||
|
if (encoding === 'base64') {
|
||||||
|
outputData = atob(data);
|
||||||
|
} else {
|
||||||
|
outputData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the decoded data to the terminal
|
||||||
|
session.xterm.write(outputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove terminal from tray
|
||||||
|
function removeFromTray(containerId) {
|
||||||
|
const trayItem = document.querySelector(`.tray-item[data-id="${containerId}"]`);
|
||||||
|
if (trayItem) {
|
||||||
|
trayItem.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose functions to app.js
|
||||||
|
export { startTerminal, appendTerminalOutput };
|
222
server/server.js
222
server/server.js
@ -1,3 +1,5 @@
|
|||||||
|
// server.js
|
||||||
|
|
||||||
import Hyperswarm from 'hyperswarm';
|
import Hyperswarm from 'hyperswarm';
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import crypto from 'hypercore-crypto';
|
import crypto from 'hypercore-crypto';
|
||||||
@ -5,15 +7,18 @@ import crypto from 'hypercore-crypto';
|
|||||||
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||||
const swarm = new Hyperswarm();
|
const swarm = new Hyperswarm();
|
||||||
const connectedPeers = new Set();
|
const connectedPeers = new Set();
|
||||||
|
const terminalSessions = new Map(); // Map to track terminal sessions per peer
|
||||||
|
|
||||||
// Generate a topic for the server
|
// Generate a topic for the server
|
||||||
const topic = crypto.randomBytes(32);
|
const topic = crypto.randomBytes(32);
|
||||||
console.log(`[INFO] Server started with topic: ${topic.toString('hex')}`);
|
console.log(`[INFO] Server started with topic: ${topic.toString('hex')}`);
|
||||||
|
|
||||||
|
// Join the swarm with the generated topic
|
||||||
swarm.join(topic, { server: true, client: false });
|
swarm.join(topic, { server: true, client: false });
|
||||||
|
|
||||||
|
// Handle incoming peer connections
|
||||||
swarm.on('connection', (peer) => {
|
swarm.on('connection', (peer) => {
|
||||||
console.log(`[INFO] Peer connected`);
|
console.log('[INFO] Peer connected');
|
||||||
connectedPeers.add(peer);
|
connectedPeers.add(peer);
|
||||||
|
|
||||||
peer.on('data', async (data) => {
|
peer.on('data', async (data) => {
|
||||||
@ -24,11 +29,24 @@ swarm.on('connection', (peer) => {
|
|||||||
|
|
||||||
switch (parsedData.command) {
|
switch (parsedData.command) {
|
||||||
case 'listContainers':
|
case 'listContainers':
|
||||||
console.log(`[INFO] Handling 'listContainers' command`);
|
console.log('[INFO] Handling \'listContainers\' command');
|
||||||
const containers = await docker.listContainers({ all: true });
|
const containers = await docker.listContainers({ all: true });
|
||||||
response = { type: 'containers', data: containers };
|
response = { type: 'containers', data: containers };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'inspectContainer':
|
||||||
|
console.log(`[INFO] Handling 'inspectContainer' command for container: ${parsedData.args.id}`);
|
||||||
|
const container = docker.getContainer(parsedData.args.id);
|
||||||
|
const config = await container.inspect();
|
||||||
|
response = { type: 'containerConfig', data: config };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'duplicateContainer':
|
||||||
|
console.log('[INFO] Handling \'duplicateContainer\' command');
|
||||||
|
const { name, config: dupConfig } = parsedData.args;
|
||||||
|
await duplicateContainer(name, dupConfig, peer);
|
||||||
|
return; // Response is handled within the duplicateContainer function
|
||||||
|
|
||||||
case 'startContainer':
|
case 'startContainer':
|
||||||
console.log(`[INFO] Handling 'startContainer' command for container: ${parsedData.args.id}`);
|
console.log(`[INFO] Handling 'startContainer' command for container: ${parsedData.args.id}`);
|
||||||
await docker.getContainer(parsedData.args.id).start();
|
await docker.getContainer(parsedData.args.id).start();
|
||||||
@ -50,15 +68,27 @@ swarm.on('connection', (peer) => {
|
|||||||
case 'startTerminal':
|
case 'startTerminal':
|
||||||
console.log(`[INFO] Starting terminal for container: ${parsedData.args.containerId}`);
|
console.log(`[INFO] Starting terminal for container: ${parsedData.args.containerId}`);
|
||||||
handleTerminal(parsedData.args.containerId, peer);
|
handleTerminal(parsedData.args.containerId, peer);
|
||||||
return; // No response needed for streaming
|
return; // No immediate response needed for streaming commands
|
||||||
|
|
||||||
|
case 'killTerminal':
|
||||||
|
console.log(`[INFO] Handling 'killTerminal' command for container: ${parsedData.args.containerId}`);
|
||||||
|
handleKillTerminal(parsedData.args.containerId, peer);
|
||||||
|
response = {
|
||||||
|
success: true,
|
||||||
|
message: `Terminal for container ${parsedData.args.containerId} killed`,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`[WARN] Unknown command: ${parsedData.command}`);
|
console.warn(`[WARN] Unknown command: ${parsedData.command}`);
|
||||||
response = { error: 'Unknown command' };
|
response = { error: 'Unknown command' };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DEBUG] Sending response to peer: ${JSON.stringify(response)}`);
|
// Send response if one was generated
|
||||||
peer.write(JSON.stringify(response));
|
if (response) {
|
||||||
|
console.log(`[DEBUG] Sending response to peer: ${JSON.stringify(response)}`);
|
||||||
|
peer.write(JSON.stringify(response));
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[ERROR] Failed to handle data from peer: ${err.message}`);
|
console.error(`[ERROR] Failed to handle data from peer: ${err.message}`);
|
||||||
peer.write(JSON.stringify({ error: err.message }));
|
peer.write(JSON.stringify({ error: err.message }));
|
||||||
@ -66,11 +96,68 @@ swarm.on('connection', (peer) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
peer.on('close', () => {
|
peer.on('close', () => {
|
||||||
console.log(`[INFO] Peer disconnected`);
|
console.log('[INFO] Peer disconnected');
|
||||||
connectedPeers.delete(peer);
|
connectedPeers.delete(peer);
|
||||||
|
|
||||||
|
// Clean up any terminal session associated with this peer
|
||||||
|
if (terminalSessions.has(peer)) {
|
||||||
|
const session = terminalSessions.get(peer);
|
||||||
|
console.log(`[INFO] Cleaning up terminal session for container: ${session.containerId}`);
|
||||||
|
session.stream.end();
|
||||||
|
peer.removeListener('data', session.onData);
|
||||||
|
terminalSessions.delete(peer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function to duplicate a container
|
||||||
|
async function duplicateContainer(name, config, peer) {
|
||||||
|
try {
|
||||||
|
// Remove non-essential fields from the configuration
|
||||||
|
const sanitizedConfig = { ...config };
|
||||||
|
delete sanitizedConfig.Id;
|
||||||
|
delete sanitizedConfig.State;
|
||||||
|
delete sanitizedConfig.Created;
|
||||||
|
delete sanitizedConfig.NetworkSettings;
|
||||||
|
delete sanitizedConfig.Mounts;
|
||||||
|
delete sanitizedConfig.Path;
|
||||||
|
delete sanitizedConfig.Args;
|
||||||
|
delete sanitizedConfig.Image;
|
||||||
|
|
||||||
|
// Ensure the container has a unique name
|
||||||
|
const newName = name;
|
||||||
|
const existingContainers = await docker.listContainers({ all: true });
|
||||||
|
const nameExists = existingContainers.some(c => c.Names.includes(`/${newName}`));
|
||||||
|
|
||||||
|
if (nameExists) {
|
||||||
|
peer.write(JSON.stringify({ error: `Container name '${newName}' already exists.` }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new container with the provided configuration
|
||||||
|
const newContainer = await docker.createContainer({
|
||||||
|
...sanitizedConfig.Config,
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the new container
|
||||||
|
await newContainer.start();
|
||||||
|
|
||||||
|
// Send success response
|
||||||
|
peer.write(JSON.stringify({ success: true, message: `Container '${newName}' duplicated and started successfully.` }));
|
||||||
|
|
||||||
|
// List containers again to update the client
|
||||||
|
const containers = await docker.listContainers({ all: true });
|
||||||
|
const update = { type: 'containers', data: containers };
|
||||||
|
for (const connectedPeer of connectedPeers) {
|
||||||
|
connectedPeer.write(JSON.stringify(update));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR] Failed to duplicate container: ${err.message}`);
|
||||||
|
peer.write(JSON.stringify({ error: `Failed to duplicate container: ${err.message}` }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stream Docker events to all peers
|
// Stream Docker events to all peers
|
||||||
docker.getEvents({}, (err, stream) => {
|
docker.getEvents({}, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -81,9 +168,10 @@ docker.getEvents({}, (err, stream) => {
|
|||||||
stream.on('data', async (chunk) => {
|
stream.on('data', async (chunk) => {
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(chunk.toString());
|
const event = JSON.parse(chunk.toString());
|
||||||
|
if (event.status === "undefined") return
|
||||||
console.log(`[INFO] Docker event received: ${event.status} - ${event.id}`);
|
console.log(`[INFO] Docker event received: ${event.status} - ${event.id}`);
|
||||||
|
|
||||||
// Get updated container list and broadcast
|
// Get updated container list and broadcast it to all connected peers
|
||||||
const containers = await docker.listContainers({ all: true });
|
const containers = await docker.listContainers({ all: true });
|
||||||
const update = { type: 'containers', data: containers };
|
const update = { type: 'containers', data: containers };
|
||||||
|
|
||||||
@ -96,12 +184,70 @@ docker.getEvents({}, (err, stream) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collect and stream container stats
|
||||||
|
docker.listContainers({ all: true }, (err, containers) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`[ERROR] Failed to list containers for stats: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
containers.forEach((containerInfo) => {
|
||||||
|
const container = docker.getContainer(containerInfo.Id);
|
||||||
|
container.stats({ stream: true }, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`[ERROR] Failed to get stats for container ${containerInfo.Id}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('data', (data) => {
|
||||||
|
try {
|
||||||
|
const stats = JSON.parse(data.toString());
|
||||||
|
const cpuUsage = calculateCPUPercent(stats);
|
||||||
|
const memoryUsage = stats.memory_stats.usage;
|
||||||
|
const networks = stats.networks;
|
||||||
|
const ipAddress = networks ? Object.values(networks)[0].IPAddress : '-';
|
||||||
|
|
||||||
|
const statsData = {
|
||||||
|
id: containerInfo.Id,
|
||||||
|
cpu: cpuUsage,
|
||||||
|
memory: memoryUsage,
|
||||||
|
ip: ipAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcast stats to all connected peers
|
||||||
|
for (const peer of connectedPeers) {
|
||||||
|
peer.write(JSON.stringify({ type: 'stats', data: statsData }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR] Failed to parse stats for container ${containerInfo.Id}: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
console.error(`[ERROR] Stats stream error for container ${containerInfo.Id}: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to calculate CPU usage percentage
|
||||||
|
function calculateCPUPercent(stats) {
|
||||||
|
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||||
|
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||||||
|
const cpuCount = stats.cpu_stats.online_cpus || stats.cpu_stats.cpu_usage.percpu_usage.length;
|
||||||
|
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||||
|
return (cpuDelta / systemDelta) * cpuCount * 100.0;
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle terminal sessions
|
||||||
async function handleTerminal(containerId, peer) {
|
async function handleTerminal(containerId, peer) {
|
||||||
const container = docker.getContainer(containerId);
|
const container = docker.getContainer(containerId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exec = await container.exec({
|
const exec = await container.exec({
|
||||||
Cmd: ['/bin/sh'],
|
Cmd: ['/bin/bash'],
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
@ -112,26 +258,43 @@ async function handleTerminal(containerId, peer) {
|
|||||||
|
|
||||||
console.log(`[INFO] Terminal session started for container: ${containerId}`);
|
console.log(`[INFO] Terminal session started for container: ${containerId}`);
|
||||||
|
|
||||||
stream.on('data', (chunk) => {
|
const onData = (input) => {
|
||||||
console.log(`[DEBUG] Terminal output: ${chunk.toString()}`);
|
|
||||||
peer.write(JSON.stringify({ type: 'terminalOutput', containerId, data: chunk.toString() }));
|
|
||||||
});
|
|
||||||
|
|
||||||
peer.on('data', (input) => {
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(input.toString());
|
const parsed = JSON.parse(input.toString());
|
||||||
if (parsed.type === 'terminalInput' && parsed.data) {
|
if (parsed.type === 'terminalInput' && parsed.data) {
|
||||||
console.log(`[DEBUG] Terminal input: ${parsed.data}`);
|
let inputData;
|
||||||
stream.write(parsed.data);
|
if (parsed.encoding === 'base64') {
|
||||||
|
inputData = Buffer.from(parsed.data, 'base64');
|
||||||
|
} else {
|
||||||
|
inputData = Buffer.from(parsed.data);
|
||||||
|
}
|
||||||
|
stream.write(inputData);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[ERROR] Failed to parse terminal input: ${err.message}`);
|
console.error(`[ERROR] Failed to parse terminal input: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peer.on('data', onData);
|
||||||
|
|
||||||
|
// Store the session along with the onData listener
|
||||||
|
terminalSessions.set(peer, { containerId, exec, stream, onData });
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
const dataBase64 = chunk.toString('base64');
|
||||||
|
peer.write(JSON.stringify({
|
||||||
|
type: 'terminalOutput',
|
||||||
|
containerId,
|
||||||
|
data: dataBase64,
|
||||||
|
encoding: 'base64',
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
peer.on('close', () => {
|
peer.on('close', () => {
|
||||||
console.log(`[INFO] Peer disconnected, ending terminal session for container: ${containerId}`);
|
console.log(`[INFO] Peer disconnected, ending terminal session for container: ${containerId}`);
|
||||||
stream.end();
|
stream.end();
|
||||||
|
terminalSessions.delete(peer);
|
||||||
|
peer.removeListener('data', onData);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[ERROR] Failed to start terminal for container ${containerId}: ${err.message}`);
|
console.error(`[ERROR] Failed to start terminal for container ${containerId}: ${err.message}`);
|
||||||
@ -139,8 +302,33 @@ async function handleTerminal(containerId, peer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to handle killing terminal sessions
|
||||||
|
function handleKillTerminal(containerId, peer) {
|
||||||
|
const session = terminalSessions.get(peer);
|
||||||
|
|
||||||
|
if (session && session.containerId === containerId) {
|
||||||
|
console.log(`[INFO] Killing terminal session for container: ${containerId}`);
|
||||||
|
|
||||||
|
// Close the stream and exec session
|
||||||
|
session.stream.end();
|
||||||
|
terminalSessions.delete(peer);
|
||||||
|
|
||||||
|
// Remove the specific 'data' event listener for terminal input
|
||||||
|
peer.removeListener('data', session.onData);
|
||||||
|
|
||||||
|
console.log(`[INFO] Terminal session for container ${containerId} terminated`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[WARN] No terminal session found for container: ${containerId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to duplicate a container (already defined above)
|
||||||
|
|
||||||
|
// Stream Docker events to all peers (already defined above)
|
||||||
|
|
||||||
|
// Handle process termination
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log(`[INFO] Server shutting down`);
|
console.log('[INFO] Server shutting down');
|
||||||
swarm.destroy();
|
swarm.destroy();
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user