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

286
app.js
View File

@ -45,86 +45,185 @@ addConnectionForm.addEventListener('submit', (e) => {
// Function to add a new connection // Function to add a new connection
function addConnection(topicHex) { 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);
console.log(`[INFO] Adding connection with topic: ${topicHex}`);
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 status-disconnected"></span>${topicId}
</span>
<button class="btn btn-sm btn-danger disconnect-btn">
<i class="fas fa-plug"></i>
</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);
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.on('connection', (peer) => {
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;
updateConnectionStatus(topicId, true);
console.log(`[INFO] Adding connection with topic: ${topicHex}`); function handlePeerData(data, topicId, peer) {
try {
const response = JSON.parse(data.toString());
console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`);
if (response.type === 'containers') {
if (window.activePeer === peer) {
renderContainers(response.data);
}
} else if (response.type === 'terminalOutput') {
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.error(`[ERROR] Server error: ${response.error}`);
}
} catch (err) {
console.error(`[ERROR] Failed to parse data from peer (topic: ${topicId}): ${err.message}`);
}
}
const connectionItem = document.createElement('li'); peer.on('data', (data) => {
connectionItem.className = 'list-group-item d-flex align-items-center'; // Handle incoming data
connectionItem.dataset.topicId = topicId; handlePeerData(data, topicId, peer);
connectionItem.innerHTML = ` });
<span class="connection-status status-disconnected"></span>${topicId}
`; peer.on('close', () => {
connectionItem.addEventListener('click', () => switchConnection(topicId)); console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
connectionList.appendChild(connectionItem); updateConnectionStatus(topicId, false);
connections[topicId].peer = null; // Clear the peer reference
if (window.activePeer === peer) {
window.activePeer = null;
connectionTitle.textContent = 'Disconnected';
dashboard.classList.add('hidden');
containerList.innerHTML = '';
}
});
// If this is the first connection, switch to it
if (!window.activePeer) {
switchConnection(topicId);
}
});
}
connections[topicId] = { topic, peer: null, swarm: null }; function disconnectConnection(topicId, connectionItem) {
const connection = connections[topicId];
// Create a new swarm for this connection if (!connection) {
const swarm = new Hyperswarm(); console.error(`[ERROR] No connection found for topicId: ${topicId}`);
connections[topicId].swarm = swarm;
swarm.join(topic, { client: true, server: false });
swarm.on('connection', (peer) => {
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; return;
} }
connections[topicId].peer = peer; // Disconnect the peer and destroy the swarm
updateConnectionStatus(topicId, true); if (connection.peer) {
connection.peer.destroy();
peer.on('data', (data) => { connection.peer = null;
try {
const response = JSON.parse(data.toString());
console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`);
if (response.type === 'containers') {
if (window.activePeer === peer) {
renderContainers(response.data);
}
} else if (response.type === 'terminalOutput') {
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) {
console.error(`[ERROR] Failed to parse data from server: ${err.message}`);
}
});
peer.on('close', () => {
console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
updateConnectionStatus(topicId, false);
connections[topicId].peer = null; // Clear the peer reference
if (window.activePeer === peer) {
window.activePeer = null;
connectionTitle.textContent = 'Disconnected';
dashboard.classList.add('hidden');
containerList.innerHTML = '';
}
});
// If this is the first connection, switch to it
if (!window.activePeer) {
switchConnection(topicId);
} }
}); 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 // Update connection status
function updateConnectionStatus(topicId, isConnected) { function updateConnectionStatus(topicId, isConnected) {
@ -136,25 +235,27 @@ function updateConnectionStatus(topicId, isConnected) {
// Switch between connections // Switch between connections
function switchConnection(topicId) { function switchConnection(topicId) {
const connection = connections[topicId]; const connection = connections[topicId];
if (!connection) { if (!connection) {
console.error(`[ERROR] No connection found for topicId: ${topicId}`); console.error(`[ERROR] No connection found for topicId: ${topicId}`);
return; 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;
}
window.activePeer = connection.peer;
connectionTitle.textContent = `Connection: ${topicId}`;
dashboard.classList.remove('hidden');
console.log('[INFO] Sending "listContainers" command');
sendCommand('listContainers');
} }
if (!connection.peer) {
console.error('[ERROR] No active peer for this connection.');
return;
}
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
window.switchConnection = switchConnection; window.switchConnection = switchConnection;
@ -310,9 +411,6 @@ duplicateContainerForm.addEventListener('submit', (e) => {
// Close the modal // Close the modal
duplicateModal.hide(); duplicateModal.hide();
// Notify the user
alert('Duplicate command sent.');
// Trigger container list update after a short delay // Trigger container list update after a short delay
setTimeout(() => { setTimeout(() => {
console.log('[INFO] Fetching updated container list after duplication'); console.log('[INFO] Fetching updated container list after duplication');

View File

@ -7,7 +7,8 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <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 --> <!-- xterm.css for Terminal -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
@ -135,6 +136,7 @@
cursor: pointer; cursor: pointer;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="titlebar"> <div id="titlebar">
@ -154,7 +156,7 @@
</div> </div>
<div id="content"> <div id="content">
<h1 id="connection-title">Select a Connection</h1> <h1 id="connection-title">Add a Connection</h1>
<div id="dashboard" class="hidden"> <div id="dashboard" class="hidden">
<h2>Containers</h2> <h2>Containers</h2>
<table class="table table-dark table-striped"> <table class="table table-dark table-striped">
@ -223,6 +225,7 @@
<!-- Bootstrap JS for Modal Functionality --> <!-- Bootstrap JS for Modal Functionality -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Your App JS --> <!-- Your App JS -->
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</body> </body>

View File

@ -200,4 +200,4 @@ function removeFromTray(containerId) {
} }
// Expose functions to app.js // Expose functions to app.js
export { startTerminal, appendTerminalOutput }; export { startTerminal, appendTerminalOutput };

View File

@ -3,6 +3,7 @@
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';
import { PassThrough } from 'stream';
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();
@ -80,8 +81,9 @@ swarm.on('connection', (peer) => {
break; 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' };
return
} }
// Send response if one was generated // 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', () => { peer.on('close', () => {
console.log('[INFO] Peer disconnected'); console.log('[INFO] Peer disconnected');
connectedPeers.delete(peer); connectedPeers.delete(peer);
cleanupPeer(peer)
// Clean up any terminal session associated with this peer // Clean up any terminal session associated with this peer
if (terminalSessions.has(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 // Function to duplicate a container
async function duplicateContainer(name, config, peer) { async function duplicateContainer(name, config, peer) {
try { try {
@ -143,21 +164,30 @@ async function duplicateContainer(name, config, peer) {
// Start the new container // Start the new container
await newContainer.start(); 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.` })); 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 containers = await docker.listContainers({ all: true });
const update = { type: 'containers', data: containers }; const update = { type: 'containers', data: containers };
// Broadcast the updated container list to all connected peers
for (const connectedPeer of connectedPeers) { for (const connectedPeer of connectedPeers) {
connectedPeer.write(JSON.stringify(update)); 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) { } catch (err) {
console.error(`[ERROR] Failed to duplicate container: ${err.message}`); console.error(`[ERROR] Failed to duplicate container: ${err.message}`);
peer.write(JSON.stringify({ 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) {
@ -195,7 +225,6 @@ docker.listContainers({ all: true }, (err, containers) => {
const container = docker.getContainer(containerInfo.Id); const container = docker.getContainer(containerInfo.Id);
container.stats({ stream: true }, (err, stream) => { container.stats({ stream: true }, (err, stream) => {
if (err) { if (err) {
console.error(`[ERROR] Failed to get stats for container ${containerInfo.Id}: ${err.message}`);
return; return;
} }
@ -219,12 +248,10 @@ docker.listContainers({ all: true }, (err, containers) => {
peer.write(JSON.stringify({ type: 'stats', data: statsData })); peer.write(JSON.stringify({ type: 'stats', data: statsData }));
} }
} catch (err) { } catch (err) {
console.error(`[ERROR] Failed to parse stats for container ${containerInfo.Id}: ${err.message}`);
} }
}); });
stream.on('error', (err) => { stream.on('error', (err) => {
console.error(`[ERROR] Stats stream error for container ${containerInfo.Id}: ${err.message}`);
}); });
}); });
}); });
@ -241,6 +268,7 @@ function calculateCPUPercent(stats) {
return 0.0; return 0.0;
} }
// Function to handle terminal sessions
// Function to handle terminal sessions // 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);
@ -258,16 +286,18 @@ async function handleTerminal(containerId, peer) {
console.log(`[INFO] Terminal session started for container: ${containerId}`); 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) => { const onData = (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) {
let inputData; const inputData = parsed.encoding === 'base64'
if (parsed.encoding === 'base64') { ? Buffer.from(parsed.data, 'base64')
inputData = Buffer.from(parsed.data, 'base64'); : Buffer.from(parsed.data);
} else {
inputData = Buffer.from(parsed.data);
}
stream.write(inputData); stream.write(inputData);
} }
} catch (err) { } catch (err) {
@ -276,16 +306,22 @@ async function handleTerminal(containerId, peer) {
}; };
peer.on('data', onData); peer.on('data', onData);
// Store the session along with the onData listener
terminalSessions.set(peer, { containerId, exec, stream, onData }); terminalSessions.set(peer, { containerId, exec, stream, onData });
stream.on('data', (chunk) => { stdout.on('data', (chunk) => {
const dataBase64 = chunk.toString('base64');
peer.write(JSON.stringify({ peer.write(JSON.stringify({
type: 'terminalOutput', type: 'terminalOutput',
containerId, containerId,
data: dataBase64, data: chunk.toString('base64'),
encoding: 'base64',
}));
});
stderr.on('data', (chunk) => {
peer.write(JSON.stringify({
type: 'terminalErrorOutput',
containerId,
data: chunk.toString('base64'),
encoding: 'base64', encoding: 'base64',
})); }));
}); });
@ -302,6 +338,7 @@ async function handleTerminal(containerId, peer) {
} }
} }
// Function to handle killing terminal sessions // Function to handle killing terminal sessions
function handleKillTerminal(containerId, peer) { function handleKillTerminal(containerId, peer) {
const session = terminalSessions.get(peer); const session = terminalSessions.get(peer);
@ -322,9 +359,45 @@ function handleKillTerminal(containerId, peer) {
} }
} }
// Function to duplicate a container (already defined above) 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}`);
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}`);
});
});
}
// Stream Docker events to all peers (already defined above)
// Handle process termination // Handle process termination
process.on('SIGINT', () => { process.on('SIGINT', () => {