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:
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user