Bug fixes

This commit is contained in:
Raven Scott 2024-11-28 02:51:47 -05:00
parent db04774fb4
commit 9ec5bf0788
4 changed files with 291 additions and 117 deletions

120
app.js
View File

@ -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');

View File

@ -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>

View File

@ -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', () => {