Bug fixes
This commit is contained in:
parent
db04774fb4
commit
9ec5bf0788
120
app.js
120
app.js
@ -51,12 +51,27 @@ function addConnection(topicHex) {
|
||||
console.log(`[INFO] Adding connection with topic: ${topicHex}`);
|
||||
|
||||
const connectionItem = document.createElement('li');
|
||||
connectionItem.className = 'list-group-item d-flex align-items-center';
|
||||
connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between';
|
||||
connectionItem.dataset.topicId = topicId;
|
||||
connectionItem.innerHTML = `
|
||||
<span>
|
||||
<span class="connection-status status-disconnected"></span>${topicId}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-danger disconnect-btn">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
`;
|
||||
connectionItem.addEventListener('click', () => switchConnection(topicId));
|
||||
|
||||
// Add click event to switch connection
|
||||
connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId));
|
||||
|
||||
// Add click event to the disconnect button
|
||||
const disconnectBtn = connectionItem.querySelector('.disconnect-btn');
|
||||
disconnectBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Prevent triggering the switch connection event
|
||||
disconnectConnection(topicId, connectionItem);
|
||||
});
|
||||
|
||||
connectionList.appendChild(connectionItem);
|
||||
|
||||
connections[topicId] = { topic, peer: null, swarm: null };
|
||||
@ -80,10 +95,11 @@ function addConnection(topicHex) {
|
||||
connections[topicId].peer = peer;
|
||||
updateConnectionStatus(topicId, true);
|
||||
|
||||
peer.on('data', (data) => {
|
||||
|
||||
function handlePeerData(data, topicId, peer) {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`);
|
||||
console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`);
|
||||
|
||||
if (response.type === 'containers') {
|
||||
if (window.activePeer === peer) {
|
||||
@ -99,11 +115,16 @@ function addConnection(topicHex) {
|
||||
} else if (response.type === 'stats') {
|
||||
updateContainerStats(response.data);
|
||||
} else if (response.error) {
|
||||
console.log(`Error: ${response.error}`);
|
||||
console.error(`[ERROR] Server error: ${response.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to parse data from server: ${err.message}`);
|
||||
console.error(`[ERROR] Failed to parse data from peer (topic: ${topicId}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
peer.on('data', (data) => {
|
||||
// Handle incoming data
|
||||
handlePeerData(data, topicId, peer);
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
@ -124,7 +145,85 @@ function addConnection(topicHex) {
|
||||
switchConnection(topicId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectConnection(topicId, connectionItem) {
|
||||
const connection = connections[topicId];
|
||||
if (!connection) {
|
||||
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect the peer and destroy the swarm
|
||||
if (connection.peer) {
|
||||
connection.peer.destroy();
|
||||
connection.peer = null;
|
||||
}
|
||||
if (connection.swarm) {
|
||||
connection.swarm.destroy();
|
||||
connection.swarm = null;
|
||||
}
|
||||
|
||||
// Remove the connection from the global connections object
|
||||
delete connections[topicId];
|
||||
|
||||
// Remove the connection item from the list
|
||||
connectionList.removeChild(connectionItem);
|
||||
|
||||
console.log(`[INFO] Disconnected and removed connection: ${topicId}`);
|
||||
|
||||
// Reset active peer if it was the disconnected connection
|
||||
window.activePeer = null;
|
||||
connectionTitle.textContent = 'Choose a Connection'; // Reset title
|
||||
dashboard.classList.add('hidden');
|
||||
containerList.innerHTML = ''; // Clear the container list
|
||||
|
||||
|
||||
// Ensure the container list is cleared regardless of the active connection
|
||||
resetContainerList();
|
||||
|
||||
// Refresh the connections view
|
||||
resetConnectionsView();
|
||||
}
|
||||
|
||||
// Function to reset the container list
|
||||
function resetContainerList() {
|
||||
containerList.innerHTML = ''; // Clear the existing list
|
||||
console.log('[INFO] Container list cleared.');
|
||||
}
|
||||
|
||||
// Function to reset the connections view
|
||||
function resetConnectionsView() {
|
||||
// Clear the connection list
|
||||
connectionList.innerHTML = '';
|
||||
|
||||
// Re-populate the connection list from the `connections` object
|
||||
Object.keys(connections).forEach((topicId) => {
|
||||
const connectionItem = document.createElement('li');
|
||||
connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between';
|
||||
connectionItem.dataset.topicId = topicId;
|
||||
connectionItem.innerHTML = `
|
||||
<span>
|
||||
<span class="connection-status ${connections[topicId].peer ? 'status-connected' : 'status-disconnected'}"></span>${topicId}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-danger disconnect-btn">Disconnect</button>
|
||||
`;
|
||||
|
||||
// Add click event to switch connection
|
||||
connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId));
|
||||
|
||||
// Add click event to the disconnect button
|
||||
const disconnectBtn = connectionItem.querySelector('.disconnect-btn');
|
||||
disconnectBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Prevent triggering the switch connection event
|
||||
disconnectConnection(topicId, connectionItem);
|
||||
});
|
||||
|
||||
connectionList.appendChild(connectionItem);
|
||||
});
|
||||
|
||||
console.log('[INFO] Connections view reset.');
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
function updateConnectionStatus(topicId, isConnected) {
|
||||
@ -139,11 +238,13 @@ function switchConnection(topicId) {
|
||||
const connection = connections[topicId];
|
||||
if (!connection) {
|
||||
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
|
||||
connectionTitle.textContent = 'Choose a Connection'; // Default message
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.peer) {
|
||||
console.error('[ERROR] No active peer for this connection.');
|
||||
connectionTitle.textContent = 'Choose a Connection'; // Default message
|
||||
return;
|
||||
}
|
||||
|
||||
@ -153,7 +254,7 @@ function switchConnection(topicId) {
|
||||
|
||||
console.log('[INFO] Sending "listContainers" command');
|
||||
sendCommand('listContainers');
|
||||
}
|
||||
}
|
||||
|
||||
// Attach switchConnection to the global window object
|
||||
window.switchConnection = switchConnection;
|
||||
@ -310,9 +411,6 @@ duplicateContainerForm.addEventListener('submit', (e) => {
|
||||
// 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');
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- xterm.css for Terminal -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
||||
@ -135,6 +136,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="titlebar">
|
||||
@ -154,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<h1 id="connection-title">Select a Connection</h1>
|
||||
<h1 id="connection-title">Add a Connection</h1>
|
||||
<div id="dashboard" class="hidden">
|
||||
<h2>Containers</h2>
|
||||
<table class="table table-dark table-striped">
|
||||
@ -223,6 +225,7 @@
|
||||
<!-- 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>
|
||||
</body>
|
||||
|
273
server/server.js
273
server/server.js
@ -3,6 +3,7 @@
|
||||
import Hyperswarm from 'hyperswarm';
|
||||
import Docker from 'dockerode';
|
||||
import crypto from 'hypercore-crypto';
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
const swarm = new Hyperswarm();
|
||||
@ -80,8 +81,9 @@ swarm.on('connection', (peer) => {
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[WARN] Unknown command: ${parsedData.command}`);
|
||||
response = { error: 'Unknown command' };
|
||||
// console.warn(`[WARN] Unknown command: ${parsedData.command}`);
|
||||
// response = { error: 'Unknown command' };
|
||||
return
|
||||
}
|
||||
|
||||
// Send response if one was generated
|
||||
@ -95,9 +97,15 @@ swarm.on('connection', (peer) => {
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('error', (err) => {
|
||||
console.error(`[ERROR] Peer connection error: ${err.message}`);
|
||||
cleanupPeer(peer);
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
console.log('[INFO] Peer disconnected');
|
||||
connectedPeers.delete(peer);
|
||||
cleanupPeer(peer)
|
||||
|
||||
// Clean up any terminal session associated with this peer
|
||||
if (terminalSessions.has(peer)) {
|
||||
@ -110,6 +118,19 @@ swarm.on('connection', (peer) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to handle peer cleanup
|
||||
function cleanupPeer(peer) {
|
||||
connectedPeers.delete(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 {
|
||||
@ -143,21 +164,30 @@ async function duplicateContainer(name, config, peer) {
|
||||
// Start the new container
|
||||
await newContainer.start();
|
||||
|
||||
// Send success response
|
||||
// Send success response to the requesting peer
|
||||
peer.write(JSON.stringify({ success: true, message: `Container '${newName}' duplicated and started successfully.` }));
|
||||
|
||||
// List containers again to update the client
|
||||
// Get the updated list of containers
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
const update = { type: 'containers', data: containers };
|
||||
|
||||
// Broadcast the updated container list to all connected peers
|
||||
for (const connectedPeer of connectedPeers) {
|
||||
connectedPeer.write(JSON.stringify(update));
|
||||
}
|
||||
|
||||
// Start streaming stats for the new container
|
||||
const newContainerInfo = containers.find(c => c.Names.includes(`/${newName}`));
|
||||
if (newContainerInfo) {
|
||||
streamContainerStats(newContainerInfo);
|
||||
}
|
||||
} 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
|
||||
docker.getEvents({}, (err, stream) => {
|
||||
if (err) {
|
||||
@ -193,6 +223,145 @@ docker.listContainers({ all: true }, (err, containers) => {
|
||||
|
||||
containers.forEach((containerInfo) => {
|
||||
const container = docker.getContainer(containerInfo.Id);
|
||||
container.stats({ stream: true }, (err, stream) => {
|
||||
if (err) {
|
||||
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) {
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
// Function to handle terminal sessions
|
||||
async function handleTerminal(containerId, peer) {
|
||||
const container = docker.getContainer(containerId);
|
||||
|
||||
try {
|
||||
const exec = await container.exec({
|
||||
Cmd: ['/bin/bash'],
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
});
|
||||
|
||||
const stream = await exec.start({ hijack: true, stdin: true });
|
||||
|
||||
console.log(`[INFO] Terminal session started for container: ${containerId}`);
|
||||
|
||||
const stdout = new PassThrough();
|
||||
const stderr = new PassThrough();
|
||||
|
||||
container.modem.demuxStream(stream, stdout, stderr);
|
||||
|
||||
const onData = (input) => {
|
||||
try {
|
||||
const parsed = JSON.parse(input.toString());
|
||||
if (parsed.type === 'terminalInput' && parsed.data) {
|
||||
const inputData = parsed.encoding === 'base64'
|
||||
? Buffer.from(parsed.data, 'base64')
|
||||
: Buffer.from(parsed.data);
|
||||
stream.write(inputData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to parse terminal input: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
peer.on('data', onData);
|
||||
terminalSessions.set(peer, { containerId, exec, stream, onData });
|
||||
|
||||
stdout.on('data', (chunk) => {
|
||||
peer.write(JSON.stringify({
|
||||
type: 'terminalOutput',
|
||||
containerId,
|
||||
data: chunk.toString('base64'),
|
||||
encoding: 'base64',
|
||||
}));
|
||||
});
|
||||
|
||||
stderr.on('data', (chunk) => {
|
||||
peer.write(JSON.stringify({
|
||||
type: 'terminalErrorOutput',
|
||||
containerId,
|
||||
data: chunk.toString('base64'),
|
||||
encoding: 'base64',
|
||||
}));
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
console.log(`[INFO] Peer disconnected, ending terminal session for container: ${containerId}`);
|
||||
stream.end();
|
||||
terminalSessions.delete(peer);
|
||||
peer.removeListener('data', onData);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to start terminal for container ${containerId}: ${err.message}`);
|
||||
peer.write(JSON.stringify({ error: `Failed to start terminal: ${err.message}` }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 streamContainerStats(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}`);
|
||||
@ -227,104 +396,8 @@ docker.listContainers({ all: true }, (err, containers) => {
|
||||
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) {
|
||||
const container = docker.getContainer(containerId);
|
||||
|
||||
try {
|
||||
const exec = await container.exec({
|
||||
Cmd: ['/bin/bash'],
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
});
|
||||
|
||||
const stream = await exec.start({ hijack: true, stdin: true });
|
||||
|
||||
console.log(`[INFO] Terminal session started for container: ${containerId}`);
|
||||
|
||||
const onData = (input) => {
|
||||
try {
|
||||
const parsed = JSON.parse(input.toString());
|
||||
if (parsed.type === 'terminalInput' && parsed.data) {
|
||||
let inputData;
|
||||
if (parsed.encoding === 'base64') {
|
||||
inputData = Buffer.from(parsed.data, 'base64');
|
||||
} else {
|
||||
inputData = Buffer.from(parsed.data);
|
||||
}
|
||||
stream.write(inputData);
|
||||
}
|
||||
} catch (err) {
|
||||
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', () => {
|
||||
console.log(`[INFO] Peer disconnected, ending terminal session for container: ${containerId}`);
|
||||
stream.end();
|
||||
terminalSessions.delete(peer);
|
||||
peer.removeListener('data', onData);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to start terminal for container ${containerId}: ${err.message}`);
|
||||
peer.write(JSON.stringify({ error: `Failed to start terminal: ${err.message}` }));
|
||||
}
|
||||
}
|
||||
|
||||
// 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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user