Fix memory leaks and optimize performance

- Fix memory leaks: cleanup stats/logs streams, event listeners, peer handlers
- Fix bugs: duplicate listeners/modals, race conditions
- Optimize: increase polling intervals, cache DOM queries, localStorage fallback
- Improve: error handling, code cleanup, remove debug statements
This commit is contained in:
Raven Scott
2025-11-24 07:03:35 -05:00
parent 0cf82495e9
commit d3216afb64
7 changed files with 400 additions and 197 deletions
Vendored
BIN
View File
Binary file not shown.
+197 -83
View File
@@ -4,13 +4,17 @@ import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js';
// DOM Elements
// DOM Elements - Cache frequently accessed elements
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');
const welcomePage = document.getElementById('welcome-page');
const sidebar = document.getElementById('sidebar');
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
const alertContainer = document.getElementById('alert-container');
// Modal Elements
const duplicateModalElement = document.getElementById('duplicateModal');
@@ -56,21 +60,27 @@ document.addEventListener('DOMContentLoaded', () => {
});
function startStatsInterval() {
if (statsInterval) {
clearInterval(statsInterval);
// Guard: stop existing interval before starting a new one
stopStatsInterval();
// Only start if there's an active peer
if (!window.activePeer) {
console.warn('[WARN] No active peer; not starting stats interval.');
return;
}
// Increased interval to 500ms for better performance (was 100ms)
statsInterval = setInterval(() => {
if (window.activePeer) {
const now = Date.now();
if (now - lastStatsUpdate >= 500) { // Ensure at least 500ms between updates
// sendCommand('allStats', {}); // Adjust command if necessary
lastStatsUpdate = now;
}
} else {
console.warn('[WARN] No active peer; skipping stats request.');
stopStatsInterval(); // Stop interval if peer is no longer active
}
}, 100); // Poll every 100ms for better reactivity
}, 500); // Poll every 500ms for better performance (reduced from 100ms)
}
const smoothedStats = {}; // Container-specific smoothing storage
@@ -126,12 +136,32 @@ function waitForPeerResponse(expectedMessageFragment, timeout = 900000) {
});
}
// Utility functions for managing cookies
// Utility functions for managing cookies and localStorage
const COOKIE_SIZE_LIMIT = 4000; // 4KB limit for cookies
const CONNECTIONS_STORAGE_KEY = 'peardock_connections';
const USE_LOCALSTORAGE_KEY = 'peardock_use_localstorage';
function setCookie(name, value, days = 365) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${encodeURIComponent(value)};${expires};path=/`;
const cookieValue = `${name}=${encodeURIComponent(value)};${expires};path=/`;
// Check cookie size (approximate)
if (cookieValue.length > COOKIE_SIZE_LIMIT) {
console.warn(`[WARN] Cookie size (${cookieValue.length} bytes) exceeds limit. Using localStorage instead.`);
// Mark that we should use localStorage
try {
localStorage.setItem(USE_LOCALSTORAGE_KEY, 'true');
localStorage.setItem(CONNECTIONS_STORAGE_KEY, value);
return;
} catch (err) {
console.error(`[ERROR] Failed to save to localStorage: ${err.message}`);
// Fall through to try cookie anyway
}
}
document.cookie = cookieValue;
}
function getCookie(name) {
@@ -147,9 +177,23 @@ function deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
// Load connections from cookies
// Load connections from cookies or localStorage
function loadConnections() {
const savedConnections = getCookie('connections');
let savedConnections = null;
// Check if we should use localStorage
try {
const useLocalStorage = localStorage.getItem(USE_LOCALSTORAGE_KEY);
if (useLocalStorage === 'true') {
savedConnections = localStorage.getItem(CONNECTIONS_STORAGE_KEY);
} else {
savedConnections = getCookie('connections');
}
} catch (err) {
console.warn(`[WARN] localStorage not available, falling back to cookies: ${err.message}`);
savedConnections = getCookie('connections');
}
const connections = savedConnections ? JSON.parse(savedConnections) : {};
// Recreate the topic Buffer from the hex string
@@ -167,7 +211,7 @@ function loadConnections() {
}
// Save connections to cookies
// Save connections to cookies or localStorage
function saveConnections() {
const serializableConnections = {};
@@ -179,7 +223,30 @@ function saveConnections() {
};
}
setCookie('connections', JSON.stringify(serializableConnections));
const serialized = JSON.stringify(serializableConnections);
// Check size and use appropriate storage
if (serialized.length > COOKIE_SIZE_LIMIT) {
// Use localStorage for large data
try {
localStorage.setItem(USE_LOCALSTORAGE_KEY, 'true');
localStorage.setItem(CONNECTIONS_STORAGE_KEY, serialized);
console.log('[INFO] Saved connections to localStorage (data too large for cookies)');
} catch (err) {
console.error(`[ERROR] Failed to save to localStorage: ${err.message}`);
// Try cookie as fallback (may fail but we try)
setCookie('connections', serialized);
}
} else {
// Use cookies for small data
try {
localStorage.removeItem(USE_LOCALSTORAGE_KEY);
localStorage.removeItem(CONNECTIONS_STORAGE_KEY);
} catch (err) {
// Ignore localStorage errors
}
setCookie('connections', serialized);
}
}
@@ -197,47 +264,31 @@ function toggleResetButtonVisibility() {
const resetConnectionsBtn = document.createElement('button');
resetConnectionsBtn.textContent = 'Reset Connections';
resetConnectionsBtn.className = 'btn btn-danger w-100 mt-2';
resetConnectionsBtn.addEventListener('click', () => {
console.log('[INFO] Resetting connections and clearing cookies.');
resetConnectionsBtn.addEventListener('click', () => {
console.log('[INFO] Resetting connections and clearing storage.');
Object.keys(connections).forEach((topicId) => {
disconnectConnection(topicId);
});
deleteCookie('connections');
// Also clear localStorage
try {
localStorage.removeItem(USE_LOCALSTORAGE_KEY);
localStorage.removeItem(CONNECTIONS_STORAGE_KEY);
} catch (err) {
console.warn(`[WARN] Failed to clear localStorage: ${err.message}`);
}
resetConnectionsView();
showWelcomePage();
toggleResetButtonVisibility(); // Ensure button visibility is updated
});
document.getElementById('sidebar').appendChild(resetConnectionsBtn);
if (sidebar) {
sidebar.appendChild(resetConnectionsBtn);
}
// Initialize the app
console.log('[INFO] Client app initialized');
// Load connections from cookies and restore them
document.addEventListener('DOMContentLoaded', () => {
const savedConnections = loadConnections();
console.log('[INFO] Restoring saved connections:', savedConnections);
// Restore saved connections
Object.keys(savedConnections).forEach((topicId) => {
let topicHex = savedConnections[topicId].topic;
// Ensure topicHex is a string
if (typeof topicHex !== 'string') {
topicHex = b4a.toString(topicHex, 'hex');
}
addConnection(topicHex);
});
if (Object.keys(connections).length > 0) {
hideWelcomePage();
} else {
showWelcomePage();
}
assertVisibility(); // Ensure visibility reflects the restored connections
});
// Show Status Indicator
// Modify showStatusIndicator to recreate it dynamically
@@ -272,12 +323,14 @@ function showAlert(type, message) {
alertBox.className = `alert alert-${type}`;
alertBox.textContent = message;
const container = document.querySelector('#alert-container');
if (container) {
container.appendChild(alertBox);
// Use cached DOM element
if (alertContainer) {
alertContainer.appendChild(alertBox);
setTimeout(() => {
container.removeChild(alertBox);
if (alertContainer.contains(alertBox)) {
alertContainer.removeChild(alertBox);
}
}, 5000);
} else {
console.warn('[WARN] Alert container not found.');
@@ -287,17 +340,21 @@ function showAlert(type, message) {
// Collapse Sidebar Functionality
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
collapseSidebarBtn.addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('collapsed');
const btn = collapseSidebarBtn;
btn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
if (collapseSidebarBtn) {
collapseSidebarBtn.addEventListener('click', () => {
// Use cached DOM elements
if (sidebar) {
sidebar.classList.toggle('collapsed');
collapseSidebarBtn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
// Toggle Reset Connections Button Visibility
const resetConnectionsBtn = document.querySelector('#sidebar .btn-danger');
resetConnectionsBtn.style.display = sidebar.classList.contains('collapsed') ? 'none' : 'block';
});
// Toggle Reset Connections Button Visibility
const resetConnectionsBtn = sidebar.querySelector('.btn-danger');
if (resetConnectionsBtn) {
resetConnectionsBtn.style.display = sidebar.classList.contains('collapsed') ? 'none' : 'block';
}
}
});
}
function handlePeerData(data, topicId, peer) {
try {
@@ -498,9 +555,18 @@ function addConnection(topicHex) {
connections[topicId].peer = peer;
updateConnectionStatus(topicId, true);
peer.on('data', (data) => handlePeerData(data, topicId, peer));
// Store peer data handler reference for cleanup
const peerDataHandler = (data) => handlePeerData(data, topicId, peer);
peer.on('data', peerDataHandler);
connections[topicId].peerDataHandler = peerDataHandler; // Store for cleanup
peer.on('close', () => {
updateConnectionStatus(topicId, false);
// Remove peer data handler
if (connections[topicId] && connections[topicId].peerDataHandler) {
peer.removeListener('data', connections[topicId].peerDataHandler);
delete connections[topicId].peerDataHandler;
}
if (window.activePeer === peer) {
window.activePeer = null;
dashboard.classList.add('hidden');
@@ -515,11 +581,12 @@ function addConnection(topicHex) {
});
// Collapse the sidebar after adding a connection
const sidebar = document.getElementById('sidebar');
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
if (!sidebar.classList.contains('collapsed')) {
// Use cached DOM elements
if (sidebar && !sidebar.classList.contains('collapsed')) {
sidebar.classList.add('collapsed');
collapseSidebarBtn.innerHTML = '>';
if (collapseSidebarBtn) {
collapseSidebarBtn.innerHTML = '>';
}
console.log('[DEBUG] Sidebar collapsed after adding connection');
}
}
@@ -541,22 +608,38 @@ function openTemplateDeployModal(topicId) {
// Initialize connections from cookies on page load
document.addEventListener('DOMContentLoaded', () => {
const savedConnections = loadConnections();
console.log('[INFO] Loading saved connections:', savedConnections);
try {
const savedConnections = loadConnections();
console.log('[INFO] Loading saved connections:', savedConnections);
Object.keys(savedConnections).forEach((topicId) => {
const topicHex = savedConnections[topicId].topic;
addConnection(topicHex);
});
// Restore saved connections with error handling
Object.keys(savedConnections).forEach((topicId) => {
try {
let topicHex = savedConnections[topicId].topic;
if (Object.keys(connections).length > 0) {
hideWelcomePage();
startStatsInterval(); // Start stats polling for active peers
} else {
showWelcomePage();
// Ensure topicHex is a string
if (typeof topicHex !== 'string') {
topicHex = b4a.toString(topicHex, 'hex');
}
addConnection(topicHex);
} catch (err) {
console.error(`[ERROR] Failed to restore connection ${topicId}: ${err.message}`);
}
});
if (Object.keys(connections).length > 0) {
hideWelcomePage();
startStatsInterval(); // Start stats polling for active peers
} else {
showWelcomePage();
}
assertVisibility(); // Ensure visibility reflects the restored connections
} catch (err) {
console.error(`[ERROR] Failed to initialize connections: ${err.message}`);
showWelcomePage(); // Show welcome page on error
}
assertVisibility();
});
@@ -582,6 +665,10 @@ function disconnectConnection(topicId, connectionItem) {
// Destroy the peer and swarm
if (connection.peer) {
// Remove peer data handler before destroying
if (connection.peerDataHandler) {
connection.peer.removeListener('data', connection.peerDataHandler);
}
connection.peer.destroy();
}
if (connection.swarm) {
@@ -628,6 +715,10 @@ function disconnectConnection(topicId, connectionItem) {
// Function to reset the container list
function resetContainerList() {
containerList.innerHTML = ''; // Clear the existing list
// Clean up smoothedStats for all containers when list is reset
Object.keys(smoothedStats).forEach(containerId => {
delete smoothedStats[containerId];
});
console.log('[INFO] Container list cleared.');
}
@@ -723,6 +814,17 @@ function renderContainers(containers, topicId) {
}
console.log(`[INFO] Rendering ${containers.length} containers for topic: ${topicId}`);
// Get current container IDs before clearing
const currentContainerIds = new Set(containers.map(c => c.Id));
// Clean up smoothedStats for containers that no longer exist
Object.keys(smoothedStats).forEach(containerId => {
if (!currentContainerIds.has(containerId)) {
delete smoothedStats[containerId];
}
});
containerList.innerHTML = ''; // Clear the current list
containers.forEach((container) => {
@@ -993,11 +1095,27 @@ function updateContainerStats(stats) {
}
function updateStatsUI(row, stats) {
requestIdleCallback(() => {
row.querySelector('.cpu').textContent = stats.cpu.toFixed(2) || '0.00';
row.querySelector('.memory').textContent = (stats.memory / (1024 * 1024)).toFixed(2) || '0.00';
row.querySelector('.ip-address').textContent = stats.ip;
});
// Use requestAnimationFrame for smoother UI updates
if (window.requestAnimationFrame) {
requestAnimationFrame(() => {
const cpuEl = row.querySelector('.cpu');
const memoryEl = row.querySelector('.memory');
const ipEl = row.querySelector('.ip-address');
if (cpuEl) cpuEl.textContent = stats.cpu.toFixed(2) || '0.00';
if (memoryEl) memoryEl.textContent = (stats.memory / (1024 * 1024)).toFixed(2) || '0.00';
if (ipEl) ipEl.textContent = stats.ip;
});
} else {
// Fallback for browsers without requestAnimationFrame
const cpuEl = row.querySelector('.cpu');
const memoryEl = row.querySelector('.memory');
const ipEl = row.querySelector('.ip-address');
if (cpuEl) cpuEl.textContent = stats.cpu.toFixed(2) || '0.00';
if (memoryEl) memoryEl.textContent = (stats.memory / (1024 * 1024)).toFixed(2) || '0.00';
if (ipEl) ipEl.textContent = stats.ip;
}
}
@@ -1082,9 +1200,7 @@ duplicateContainerForm.addEventListener('submit', (e) => {
function showWelcomePage() {
const welcomePage = document.getElementById('welcome-page');
const dashboard = document.getElementById('dashboard');
const connectionTitle = document.getElementById('connection-title');
// Use cached DOM elements
if (welcomePage) {
welcomePage.classList.remove('hidden');
@@ -1102,8 +1218,7 @@ function showWelcomePage() {
}
function hideWelcomePage() {
const welcomePage = document.getElementById('welcome-page');
const dashboard = document.getElementById('dashboard');
// Use cached DOM elements
if (welcomePage) {
console.log('[DEBUG] Hiding welcome page');
@@ -1121,8 +1236,7 @@ function hideWelcomePage() {
}
function assertVisibility() {
const welcomePage = document.getElementById('welcome-page');
const dashboard = document.getElementById('dashboard');
// Use cached DOM elements
if (Object.keys(connections).length === 0) {
console.assert(!welcomePage.classList.contains('hidden'), '[ASSERTION FAILED] Welcome page should be visible.');
console.assert(dashboard.classList.contains('hidden'), '[ASSERTION FAILED] Dashboard should be hidden.');
-44
View File
@@ -257,27 +257,10 @@
left: 50%;
transform: translateX(-50%);
z-index: 1055;
/* Ensure it overlays important elements only */
display: flex;
flex-direction: column-reverse;
/* Stack alerts upwards */
gap: 10px;
/* Add space between alerts */
pointer-events: none;
/* Prevent container from blocking clicks */
}
#alert-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1055;
display: flex;
flex-direction: column-reverse;
gap: 10px;
pointer-events: none;
/* Prevent container from blocking clicks */
}
.alert {
@@ -390,17 +373,6 @@
}
.list-group-item {
position: relative;
display: block;
padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);
color: var(--bs-list-group-color);
text-decoration: none;
background-color: #2c2c2c
;
}
.list-group-item {
position: relative;
display: block;
@@ -613,22 +585,6 @@
<!-- Search Input -->
<!-- Deploy Modal -->
<div class="modal fade" id="templateDeployModal" tabindex="-1" aria-labelledby="deployModalLabel" 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="deploy-title"></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" id="template-search-input" class="form-control my-3" placeholder="Search templates...">
<ul id="template-list" class="list-group"></ul>
</div>
</div>
</div>
</div>
<!-- Template Deploy Modal -->
<div class="modal fade" id="templateDeployModal" tabindex="-1" aria-labelledby="templateDeployModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
+25 -20
View File
@@ -50,7 +50,27 @@ function startDockerTerminal(connectionId, peer) {
xterm.open(dockerTerminalContainer);
fitAddon.fit();
dockerTerminalSession = { xterm, fitAddon, connectionId, peer };
// Handle peer data - store handler reference for cleanup
const peerDataHandler = (data) => {
console.log('[DEBUG] Received data event');
try {
const response = JSON.parse(data.toString());
if (response.connectionId === connectionId) {
const decodedData = decodeResponseData(response.data, response.encoding);
if (response.type === 'dockerOutput') {
xterm.write(`${decodedData.trim()}\r\n`);
} else if (response.type === 'terminalErrorOutput') {
xterm.write(`\r\n[ERROR] ${decodedData.trim()}\r\n`);
}
}
} catch (error) {
console.error(`[ERROR] Failed to parse response from peer: ${error.message}`);
}
};
peer.on('data', peerDataHandler);
dockerTerminalSession = { xterm, fitAddon, connectionId, peer, peerDataHandler };
// Buffer to accumulate user input
let inputBuffer = '';
@@ -87,25 +107,6 @@ function startDockerTerminal(connectionId, peer) {
}
});
// Handle peer data
peer.on('data', (data) => {
console.log('[DEBUG] Received data event');
try {
const response = JSON.parse(data.toString());
if (response.connectionId === connectionId) {
const decodedData = decodeResponseData(response.data, response.encoding);
if (response.type === 'dockerOutput') {
xterm.write(`${decodedData.trim()}\r\n`);
} else if (response.type === 'terminalErrorOutput') {
xterm.write(`\r\n[ERROR] ${decodedData.trim()}\r\n`);
}
}
} catch (error) {
console.error(`[ERROR] Failed to parse response from peer: ${error.message}`);
}
});
// Update the terminal modal and title
dockerTerminalTitle.textContent = `Docker CLI Terminal: ${connectionId}`;
const modalInstance = new bootstrap.Modal(dockerTerminalModal);
@@ -174,6 +175,10 @@ function cleanUpDockerTerminal() {
if (dockerTerminalSession.xterm) {
dockerTerminalSession.xterm.dispose();
}
// Remove peer data handler if it exists
if (dockerTerminalSession.peer && dockerTerminalSession.peerDataHandler) {
dockerTerminalSession.peer.removeListener('data', dockerTerminalSession.peerDataHandler);
}
dockerTerminalSession = null; // Reset the session object
}
-10
View File
@@ -101,16 +101,6 @@ function displayTemplateList(templates) {
});
}
// Filter templates by search input
templateSearchInput.addEventListener('input', () => {
const searchQuery = templateSearchInput.value.toLowerCase();
const filteredTemplates = templates.filter(template =>
template.title.toLowerCase().includes(searchQuery) ||
template.description.toLowerCase().includes(searchQuery)
);
displayTemplateList(filteredTemplates);
});
// Open deploy modal and populate the form dynamically
function openDeployModal(template) {
console.log('[DEBUG] Opening deploy modal for:', template);
+21 -4
View File
@@ -17,6 +17,10 @@ let isResizing = false;
let startY = 0;
let startHeight = 0;
// Store event listener references for cleanup
let mousemoveHandler = null;
let mouseupHandler = null;
// Kill Terminal button functionality
document.getElementById('kill-terminal-btn').onclick = () => {
killActiveTerminal();
@@ -55,7 +59,7 @@ terminalHeader.addEventListener('mousedown', (e) => {
});
// Resize the modal while dragging
document.addEventListener('mousemove', (e) => {
mousemoveHandler = (e) => {
if (isResizing) {
const deltaY = startY - e.clientY; // Calculate how much the mouse moved
const newHeight = Math.min(
@@ -71,15 +75,17 @@ document.addEventListener('mousemove', (e) => {
setTimeout(() => activeSession.fitAddon.fit(), 10); // Adjust terminal content
}
}
});
};
document.addEventListener('mousemove', mousemoveHandler);
// Stop resizing when the mouse is released
document.addEventListener('mouseup', () => {
mouseupHandler = () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = 'default'; // Reset cursor
}
});
};
document.addEventListener('mouseup', mouseupHandler);
// Start terminal session
function startTerminal(containerId, containerName) {
@@ -231,6 +237,17 @@ function cleanUpAllTerminals() {
Object.keys(terminalSessions).forEach(cleanUpTerminal);
terminalModal.style.display = 'none';
activeContainerId = null;
// Remove document-level event listeners
if (mousemoveHandler) {
document.removeEventListener('mousemove', mousemoveHandler);
mousemoveHandler = null;
}
if (mouseupHandler) {
document.removeEventListener('mouseup', mouseupHandler);
mouseupHandler = null;
}
console.log('[INFO] All terminal sessions cleaned up.');
}
+157 -36
View File
@@ -18,6 +18,7 @@ const docker = new Docker({
const swarm = new Hyperswarm();
const connectedPeers = new Set();
const terminalSessions = new Map(); // Map to track terminal sessions per peer
const logsStreams = new Map(); // Map to track logs streams: key = `${peerId}:${containerId}`
// Function to generate a new key
function generateNewKey() {
@@ -165,7 +166,22 @@ swarm.on('connection', (peer) => {
case 'logs':
console.log(`[INFO] Handling 'logs' command for container: ${parsedData.args.id}`);
const logsContainer = docker.getContainer(parsedData.args.id);
const containerId = parsedData.args.id;
const logsKey = `${peer.remotePublicKey?.toString('hex') || 'unknown'}:${containerId}`;
// Clean up existing logs stream for this peer/container if it exists
if (logsStreams.has(logsKey)) {
const existingStream = logsStreams.get(logsKey);
try {
existingStream.destroy();
console.log(`[INFO] Destroyed existing logs stream for container: ${containerId}`);
} catch (err) {
console.error(`[ERROR] Failed to destroy existing logs stream: ${err.message}`);
}
logsStreams.delete(logsKey);
}
const logsContainer = docker.getContainer(containerId);
const logsStream = await logsContainer.logs({
stdout: true,
stderr: true,
@@ -173,6 +189,9 @@ swarm.on('connection', (peer) => {
follow: true, // Stream live logs
});
// Store stream reference
logsStreams.set(logsKey, logsStream);
logsStream.on('data', (chunk) => {
peer.write(
JSON.stringify({
@@ -183,12 +202,14 @@ swarm.on('connection', (peer) => {
});
logsStream.on('end', () => {
console.log(`[INFO] Log stream ended for container: ${parsedData.args.id}`);
console.log(`[INFO] Log stream ended for container: ${containerId}`);
logsStreams.delete(logsKey);
});
logsStream.on('error', (err) => {
console.error(`[ERROR] Log stream error for container ${parsedData.args.id}: ${err.message}`);
console.error(`[ERROR] Log stream error for container ${containerId}: ${err.message}`);
peer.write(JSON.stringify({ error: `Log stream error: ${err.message}` }));
logsStreams.delete(logsKey);
});
break;
@@ -197,7 +218,6 @@ swarm.on('connection', (peer) => {
console.log('[INFO] Handling \'duplicateContainer\' command');
const { name, image, hostname, netmode, cpu, memory, config: dupConfig } = parsedData.args;
const memoryInMB = memory * 1024 * 1024;
console.log("MEMEMMEMEMEMEMEMEMMEME " + memoryInMB)
await duplicateContainer(name, image, hostname, netmode, cpu, memoryInMB, dupConfig, peer);
return; // Response is handled within the duplicateContainer function
@@ -206,9 +226,7 @@ swarm.on('connection', (peer) => {
await docker.getContainer(parsedData.args.id).start();
response = { success: true, message: `Container ${parsedData.args.id} started` };
break;
// case 'allStats':
// await handleallStatsRequest(peer);
// return; // No further response needed
case 'stopContainer':
console.log(`[INFO] Handling 'stopContainer' command for container: ${parsedData.args.id}`);
await docker.getContainer(parsedData.args.id).stop();
@@ -223,8 +241,25 @@ swarm.on('connection', (peer) => {
case 'removeContainer':
console.log(`[INFO] Handling 'removeContainer' command for container: ${parsedData.args.id}`);
await docker.getContainer(parsedData.args.id).remove({ force: true });
response = { success: true, message: `Container ${parsedData.args.id} removed` };
const removedContainerId = parsedData.args.id;
// Clean up all logs streams for this container
const logsKeysToDelete = [];
for (const [key, stream] of logsStreams.entries()) {
if (key.endsWith(`:${removedContainerId}`)) {
try {
stream.destroy();
console.log(`[INFO] Destroyed logs stream for removed container: ${key}`);
} catch (err) {
console.error(`[ERROR] Failed to destroy logs stream ${key}: ${err.message}`);
}
logsKeysToDelete.push(key);
}
}
logsKeysToDelete.forEach(key => logsStreams.delete(key));
await docker.getContainer(removedContainerId).remove({ force: true });
response = { success: true, message: `Container ${removedContainerId} removed` };
break;
case 'deployContainer':
@@ -356,9 +391,8 @@ swarm.on('connection', (peer) => {
break;
default:
// console.warn(`[WARN] Unknown command: ${parsedData.command}`);
// response = { error: 'Unknown command' };
return
console.warn(`[WARN] Unknown command: ${parsedData.command}`);
return;
}
// Send response if one was generated
@@ -404,6 +438,22 @@ function cleanupPeer(peer) {
peer.removeListener('data', session.onData);
terminalSessions.delete(peer);
}
// Clean up all logs streams for this peer
const peerId = peer.remotePublicKey?.toString('hex') || 'unknown';
const logsKeysToDelete = [];
for (const [key, stream] of logsStreams.entries()) {
if (key.startsWith(`${peerId}:`)) {
try {
stream.destroy();
console.log(`[INFO] Destroyed logs stream: ${key}`);
} catch (err) {
console.error(`[ERROR] Failed to destroy logs stream ${key}: ${err.message}`);
}
logsKeysToDelete.push(key);
}
}
logsKeysToDelete.forEach(key => logsStreams.delete(key));
}
// Function to duplicate a container
@@ -484,12 +534,15 @@ async function duplicateContainer(name, image, hostname, netmode, cpu, memory, c
// Stream Docker events to all peers
let dockerEventStream = null;
docker.getEvents({}, (err, stream) => {
if (err) {
console.error(`[ERROR] Failed to get Docker events: ${err.message}`);
return;
}
dockerEventStream = stream; // Store reference for cleanup
stream.on('data', async (chunk) => {
try {
const event = JSON.parse(chunk.toString());
@@ -501,12 +554,25 @@ docker.getEvents({}, (err, stream) => {
const update = { type: 'containers', data: containers };
for (const peer of connectedPeers) {
peer.write(JSON.stringify(update));
try {
peer.write(JSON.stringify(update));
} catch (peerErr) {
console.error(`[ERROR] Failed to send update to peer: ${peerErr.message}`);
}
}
} catch (err) {
console.error(`[ERROR] Failed to process Docker event: ${err.message}`);
}
});
stream.on('error', (err) => {
console.error(`[ERROR] Docker event stream error: ${err.message}`);
});
stream.on('end', () => {
console.log('[INFO] Docker event stream ended');
dockerEventStream = null;
});
});
// Collect and stream container stats
@@ -637,26 +703,45 @@ function handleKillTerminal(containerId, peer) {
}
async function collectContainerStats(containerStats) {
const currentContainers = await docker.listContainers({ all: true });
const currentIds = currentContainers.map((c) => c.Id);
try {
const currentContainers = await docker.listContainers({ all: true });
const currentIds = currentContainers.map((c) => c.Id);
// Collect stats for all containers, including newly added ones
for (const containerInfo of currentContainers) {
if (!containerStats[containerInfo.Id]) {
console.log(`[INFO] Found new container: ${containerInfo.Names[0]?.replace(/^\//, '')}`);
containerStats[containerInfo.Id] = await initializeContainerStats(containerInfo);
// Collect stats for all containers, including newly added ones
for (const containerInfo of currentContainers) {
if (!containerStats[containerInfo.Id]) {
try {
console.log(`[INFO] Found new container: ${containerInfo.Names[0]?.replace(/^\//, '')}`);
containerStats[containerInfo.Id] = await initializeContainerStats(containerInfo);
} catch (err) {
console.error(`[ERROR] Failed to initialize stats for container ${containerInfo.Id}: ${err.message}`);
}
}
}
// Remove containers that no longer exist
Object.keys(containerStats).forEach((id) => {
if (!currentIds.includes(id)) {
console.log(`[INFO] Removing stats tracking for container: ${id}`);
const statsData = containerStats[id];
// Clean up stats stream if it exists
if (statsData && statsData.stream) {
try {
statsData.stream.destroy();
console.log(`[INFO] Destroyed stats stream for container: ${id}`);
} catch (err) {
console.error(`[ERROR] Failed to destroy stats stream for container ${id}: ${err.message}`);
}
}
delete containerStats[id];
}
});
return containerStats;
} catch (err) {
console.error(`[ERROR] Failed to collect container stats: ${err.message}`);
return containerStats; // Return existing stats on error
}
// Remove containers that no longer exist
Object.keys(containerStats).forEach((id) => {
if (!currentIds.includes(id)) {
console.log(`[INFO] Removing stats tracking for container: ${id}`);
delete containerStats[id];
}
});
return containerStats;
}
async function initializeContainerStats(containerInfo) {
@@ -678,11 +763,14 @@ async function initializeContainerStats(containerInfo) {
cpu: 0,
memory: 0,
ip: ipAddress,
stream: null, // Store stream reference for cleanup
};
// Start streaming stats for the container
try {
const statsStream = await container.stats({ stream: true });
statsData.stream = statsStream; // Store reference
statsStream.on('data', (data) => {
try {
const stats = JSON.parse(data.toString());
@@ -699,6 +787,7 @@ async function initializeContainerStats(containerInfo) {
statsStream.on('close', () => {
console.log(`[INFO] Stats stream closed for container ${containerInfo.Id}`);
statsData.stream = null; // Clear reference when closed
});
} catch (err) {
console.error(`[ERROR] Failed to start stats stream for container ${containerInfo.Id}: ${err.message}`);
@@ -711,15 +800,28 @@ async function handleStatsBroadcast() {
const containerStats = {};
// Periodically update stats and broadcast
// Increased interval to 2000ms (2 seconds) for better performance
setInterval(async () => {
await collectContainerStats(containerStats);
const aggregatedStats = Object.values(containerStats);
const response = { type: 'allStats', data: aggregatedStats };
try {
await collectContainerStats(containerStats);
const aggregatedStats = Object.values(containerStats);
// Only broadcast if there are stats to send
if (aggregatedStats.length > 0) {
const response = { type: 'allStats', data: aggregatedStats };
for (const peer of connectedPeers) {
peer.write(JSON.stringify(response));
for (const peer of connectedPeers) {
try {
peer.write(JSON.stringify(response));
} catch (err) {
console.error(`[ERROR] Failed to send stats to peer: ${err.message}`);
}
}
}
} catch (err) {
console.error(`[ERROR] Failed to collect/broadcast stats: ${err.message}`);
}
}, 1000); // Send stats every 500ms
}, 2000); // Send stats every 2 seconds (reduced frequency for better performance)
}
// Start the stats broadcast
@@ -730,6 +832,25 @@ handleStatsBroadcast();
// Handle process termination
process.on('SIGINT', () => {
console.log('[INFO] Server shutting down');
// Clean up Docker event stream
if (dockerEventStream) {
try {
dockerEventStream.destroy();
} catch (err) {
console.error(`[ERROR] Failed to destroy Docker event stream: ${err.message}`);
}
}
// Clean up all peer connections
for (const peer of connectedPeers) {
try {
cleanupPeer(peer);
} catch (err) {
console.error(`[ERROR] Failed to cleanup peer: ${err.message}`);
}
}
swarm.destroy();
process.exit();
});