import { URLSearchParams } from 'url'; import { getContainerStats, streamContainerLogs, readServerProperties, writeServerProperties, updateMods, createBackup } from './docker.js'; import { checkConnectionStatus, checkGeyserStatus, checkSftpStatus } from './status.js'; import { apiRequest } from './api.js'; const clients = new Map(); const staticEndpoints = ['log', 'website', 'map', 'my-link-cache', 'my-geyser-cache', 'my-sftp-cache', 'my-link', 'my-geyser-link', 'my-sftp']; const dynamicEndpoints = ['hello', 'time', 'mod-list']; // Helper function to start Docker stats interval function startDockerStatsInterval(ws, client, user, docker) { if (!client.subscriptions.has('docker') || user === 'Unknown') { console.log(`Skipping docker stats interval: ${user === 'Unknown' ? 'User unknown' : 'Not subscribed to docker'}`); return null; } // Send initial stats immediately (async () => { try { const initialStats = await getContainerStats(docker, user); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: 'docker', data: { ...initialStats, user } })); } } catch (error) { console.error(`Error sending initial docker stats for ${user}:`, error.message); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: 'docker', error: `Failed to fetch initial stats: ${error.message}` })); } } })(); // Start interval for periodic stats const intervalId = setInterval(async () => { try { if (ws.readyState !== ws.OPEN) { console.warn(`WebSocket not open (state: ${ws.readyState}) for ${user}, clearing docker stats interval ${intervalId}`); clearInterval(intervalId); client.intervals = client.intervals.filter(id => id !== intervalId); return; } const container = docker.getContainer(user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { ws.send(JSON.stringify({ type: 'docker', error: `Container ${user} is not running` })); return; } const stats = await getContainerStats(docker, user); if (stats.error) { console.error(`Error fetching stats for ${user}: ${stats.error}`); ws.send(JSON.stringify({ type: 'docker', error: stats.error })); } else { ws.send(JSON.stringify({ type: 'docker', data: { ...stats, user } })); } } catch (error) { console.error(`Error in docker stats interval for ${user}:`, error.message); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: 'docker', error: `Failed to fetch stats: ${error.message}` })); } } }, 1000); // 1-second interval client.intervals.push(intervalId); console.log(`Docker stats interval ID ${intervalId} added for ${user}`); // Test interval to verify setInterval execution const testIntervalId = setInterval(() => { // console.log(`Test interval tick for ${user} at ${new Date().toISOString()}, Interval ID: ${testIntervalId}`); }, 2000); // 2-second test interval client.intervals.push(testIntervalId); // console.log(`Test interval ID ${testIntervalId} added for ${user}`); return intervalId; } async function fetchAndSendUpdate(ws, endpoint, client, docker) { if (['mod-list', 'list-players'].includes(endpoint) && client.user !== 'Unknown') { try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { ws.send(JSON.stringify({ type: endpoint, error: `Container ${client.user} is not running` })); return; } } catch (error) { ws.send(JSON.stringify({ type: endpoint, error: `Failed to check container status: ${error.message}` })); return; } } if (endpoint === 'time' && client.cache['time']) { ws.send(JSON.stringify({ type: endpoint, data: client.cache['time'] })); return; } const response = await apiRequest(`/${endpoint}`, client.apiKey); if (!response.error) { if (endpoint === 'time') client.cache['time'] = response; if (endpoint === 'my-link-cache') { client.cache['my-link-cache'] = response; if (client.subscriptions.has('my-link-cache') && client.user !== 'Unknown') { try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running' && response.hostname && response.port) { const status = await checkConnectionStatus(response.hostname, response.port); ws.send(JSON.stringify({ type: 'connection-status', data: { isOnline: status.isOnline } })); } else { ws.send(JSON.stringify({ type: 'connection-status', error: `Container ${client.user} is not running` })); } } catch (error) { ws.send(JSON.stringify({ type: 'connection-status', error: `Failed to check container status: ${error.message}` })); } } } if (endpoint === 'my-geyser-cache') { client.cache['my-geyser-cache'] = response; if (client.subscriptions.has('my-geyser-cache') && client.user !== 'Unknown') { try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running' && response.hostname && response.port) { const status = await checkGeyserStatus(response.hostname, response.port); ws.send(JSON.stringify({ type: 'geyser-status', data: { isOnline: status.isOnline } })); } else { ws.send(JSON.stringify({ type: 'geyser-status', error: `Container ${client.user} is not running` })); } } catch (error) { ws.send(JSON.stringify({ type: 'geyser-status', error: `Failed to check container status: ${error.message}` })); } } } if (endpoint === 'my-sftp-cache') { client.cache['my-sftp-cache'] = response; if (client.subscriptions.has('my-sftp-cache') && client.user !== 'Unknown') { try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); const ipAddress = inspect.NetworkSettings.Networks?.minecraft_network?.IPAddress || 'N/A'; if (inspect.State.Status === 'running' && response.hostname && response.port) { const status = await checkSftpStatus(response.hostname, response.port); ws.send(JSON.stringify({ type: 'sftp-status', data: { isOnline: status.isOnline, ipAddress } })); } else { ws.send(JSON.stringify({ type: 'sftp-status', error: `Container ${client.user} is not running`, ipAddress })); } // Add IP address to my-sftp-cache response response.ipAddress = ipAddress; } catch (error) { ws.send(JSON.stringify({ type: 'sftp-status', error: `Failed to check container status: ${error.message}` })); } } } if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: endpoint, data: response })); } } else { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: endpoint, error: response.error })); } } } async function manageStatusChecks(ws, client, user, docker) { try { const container = docker.getContainer(user); const inspect = await container.inspect(); const isRunning = inspect.State.Status === 'running'; // Clear only status check intervals to prevent duplicates ['connectionStatusInterval', 'geyserStatusInterval', 'sftpStatusInterval', 'statusCheckMonitorInterval'].forEach((key) => { if (client[key]) { console.log(`Clearing ${key} for ${user}`); clearInterval(client[key]); client[key] = null; client.intervals = client.intervals.filter(id => id !== client[key]); } }); if (!isRunning || user === 'Unknown') { ['my-link-cache', 'my-geyser-cache', 'my-sftp-cache'].forEach((sub) => { if (client.subscriptions.has(sub)) { ws.send(JSON.stringify({ type: sub.replace('-cache', '-status'), error: `Container ${user} is not running or user unknown` })); } }); if (!isRunning && (client.subscriptions.has('my-link-cache') || client.subscriptions.has('my-geyser-cache') || client.subscriptions.has('my-sftp-cache')) && user !== 'Unknown') { console.log(`Starting container status monitor for ${user}`); client.statusCheckMonitorInterval = setInterval(async () => { try { const monitorContainer = docker.getContainer(user); const monitorInspect = await monitorContainer.inspect(); if (monitorInspect.State.Status === 'running') { console.log(`Container ${user} is running, restarting status checks`); await manageStatusChecks(ws, client, user, docker); clearInterval(client.statusCheckMonitorInterval); client.statusCheckMonitorInterval = null; } } catch (error) { console.error(`Error monitoring container ${user}:`, error.message); } }, parseInt(process.env.CONTAINER_STATUS_MONITOR_INTERVAL_MS, 10)); client.intervals.push(client.statusCheckMonitorInterval); } return; } const statusChecks = [ { subscription: 'my-link-cache', intervalKey: 'connectionStatusInterval', intervalMs: process.env.CONNECTION_STATUS_INTERVAL_MS, checkFn: checkConnectionStatus, cacheKey: 'my-link-cache', statusType: 'connection-status' }, { subscription: 'my-geyser-cache', intervalKey: 'geyserStatusInterval', intervalMs: process.env.GEYSER_STATUS_INTERVAL_MS, checkFn: checkGeyserStatus, cacheKey: 'my-geyser-cache', statusType: 'geyser-status' }, { subscription: 'my-sftp-cache', intervalKey: 'sftpStatusInterval', intervalMs: process.env.SFTP_STATUS_INTERVAL_MS, checkFn: checkSftpStatus, cacheKey: 'my-sftp-cache', statusType: 'sftp-status' } ]; for (const { subscription, intervalKey, intervalMs, checkFn, cacheKey, statusType } of statusChecks) { if (client.subscriptions.has(subscription)) { console.log(`Starting ${statusType} check for ${user}`); client[intervalKey] = setInterval(async () => { try { const containerCheck = docker.getContainer(user); const inspectCheck = await containerCheck.inspect(); if (inspectCheck.State.Status !== 'running') { console.log(`Container ${user} stopped, clearing ${statusType} interval`); clearInterval(client[intervalKey]); client[intervalKey] = null; client.intervals = client.intervals.filter(id => id !== client[intervalKey]); return; } const data = client.cache[cacheKey]; if (data && data.hostname && data.port) { const status = await checkFn(data.hostname, data.port); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); } } } catch (error) { console.error(`Error in ${statusType} check for ${user}:`, error.message); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: statusType, data: { isOnline: false, error: error.message } })); } } }, parseInt(intervalMs, 10)); client.intervals.push(client[intervalKey]); const data = client.cache[cacheKey]; if (data && data.hostname && data.port) { console.log(`Performing initial ${statusType} check for ${user}`); const status = await checkFn(data.hostname, data.port); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); } } } } } catch (error) { console.error(`Error managing status checks for ${user}:`, error.message); } } export function handleWebSocket(ws, req, docker) { const urlParams = new URLSearchParams(req.url.split('?')[1]); const apiKey = urlParams.get('apiKey'); if (!apiKey) { console.error('WebSocket connection rejected: Missing API key'); ws.send(JSON.stringify({ error: 'API key required' })); ws.close(); return; } const client = { apiKey, subscriptions: new Set(), user: null, intervals: [], logStream: null, cache: {}, connectionStatusInterval: null, geyserStatusInterval: null, sftpStatusInterval: null, statusCheckMonitorInterval: null, lastUpdateUserTime: 0 // For debouncing updateUser }; clients.set(ws, client); console.log('WebSocket client registered with API key'); // Heartbeat to keep WebSocket alive const heartbeatInterval = setInterval(() => { if (ws.readyState === ws.OPEN) { ws.ping(); console.log(`Sent ping to client for user ${client.user || 'unknown'}`); } else { console.log(`WebSocket not open (state: ${ws.readyState}), stopping heartbeat for user ${client.user || 'unknown'}`); clearInterval(heartbeatInterval); } }, 30000); ws.on('pong', () => { console.log(`Received pong from client for user ${client.user || 'unknown'}`); }); ws.on('message', async (message) => { try { const data = JSON.parse(message.toString()); console.log('WebSocket message received:', data.type); if (data.type === 'subscribe') { data.endpoints.forEach(endpoint => { client.subscriptions.add(endpoint); console.log(`Client subscribed to ${endpoint}`); }); console.log(`Client subscriptions: ${Array.from(client.subscriptions)}`); let hello = client.cache['hello'] || await apiRequest('/hello', client.apiKey); if (!client.cache['hello'] && !hello.error) client.cache['hello'] = hello; if (hello.error) { console.error('Failed to fetch /hello:', hello.error); ws.send(JSON.stringify({ type: 'hello', error: hello.error })); return; } if (hello.message && typeof hello.message === 'string') { const user = hello.message.split(', ')[1]?.replace('!', '').trim() || 'Unknown'; client.user = user; console.log(`User identified: ${user}`); ws.send(JSON.stringify({ type: 'hello', data: hello })); // Start Docker stats interval startDockerStatsInterval(ws, client, user, docker); if (client.subscriptions.has('docker-logs') && user !== 'Unknown') { console.log(`Starting docker logs stream for ${user}`); await streamContainerLogs(docker, ws, user, client); } else if (user === 'Unknown') { console.warn('Cannot start docker logs stream: User is Unknown'); ws.send(JSON.stringify({ type: 'docker-logs', error: 'User not identified' })); } await manageStatusChecks(ws, client, user, docker); await Promise.all([ ...staticEndpoints.filter(e => client.subscriptions.has(e)).map(e => fetchAndSendUpdate(ws, e, client, docker)), ...dynamicEndpoints.filter(e => client.subscriptions.has(e)).map(async (e) => { if (e === 'hello' && client.cache['hello']) { ws.send(JSON.stringify({ type: 'hello', data: client.cache['hello'] })); return; } if (e === 'time' && client.cache['time']) { ws.send(JSON.stringify({ type: 'time', data: client.cache['time'] })); return; } await fetchAndSendUpdate(ws, e, client, docker); }), client.subscriptions.has('list-players') ? fetchAndSendUpdate(ws, 'list-players', client, docker) : null ].filter(Boolean)); client.intervals.push(setInterval(async () => { try { for (const endpoint of dynamicEndpoints) { if (client.subscriptions.has(endpoint) && !(endpoint === 'hello' && client.cache['hello'] || endpoint === 'time' && client.cache['time'])) { await fetchAndSendUpdate(ws, endpoint, client, docker); } } } catch (error) { console.error('Error in dynamic endpoints interval:', error.message); } }, parseInt(process.env.DYNAMIC_ENDPOINTS_INTERVAL_MS, 10))); client.intervals.push(setInterval(async () => { try { for (const endpoint of staticEndpoints) { if (client.subscriptions.has(endpoint)) { await fetchAndSendUpdate(ws, endpoint, client, docker); } } } catch (error) { console.error('Error in static endpoints interval:', error.message); } }, parseInt(process.env.STATIC_ENDPOINTS_INTERVAL_MS, 10))); if (client.subscriptions.has('list-players') && user !== 'Unknown') { try { const container = docker.getContainer(user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { console.log(`Starting list-players interval for ${user}`); client.intervals.push(setInterval(() => fetchAndSendUpdate(ws, 'list-players', client, docker), parseInt(process.env.LIST_PLAYERS_INTERVAL_MS, 10))); } else { console.log(`Container ${user} is not running, skipping list-players interval`); ws.send(JSON.stringify({ type: 'list-players', error: `Container ${user} is not running` })); } } catch (error) { console.error(`Error checking container status for list-players for ${user}:`, error.message); ws.send(JSON.stringify({ type: 'list-players', error: `Failed to check container status: ${error.message}` })); } } } else { console.error('Invalid /hello response:', hello); ws.send(JSON.stringify({ type: 'hello', error: 'Invalid hello response' })); } } else if (data.type === 'updateUser') { // Debounce updateUser to prevent rapid successive calls const now = Date.now(); if (now - client.lastUpdateUserTime < 1000) { console.log(`Debouncing updateUser for ${data.user}, last update: ${client.lastUpdateUserTime}`); return; } client.lastUpdateUserTime = now; client.user = data.user; console.log(`Updated user to: ${client.user}`); if (client.user !== 'Unknown') { // Clear only specific intervals, preserve others ['connectionStatusInterval', 'geyserStatusInterval', 'sftpStatusInterval', 'statusCheckMonitorInterval'].forEach((key) => { if (client[key]) { console.log(`Clearing ${key} for ${client.user}`); clearInterval(client[key]); client[key] = null; client.intervals = client.intervals.filter(id => id !== client[key]); } }); // Start Docker stats interval for new user startDockerStatsInterval(ws, client, client.user, docker); if (client.subscriptions.has('list-players')) { try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { console.log(`Starting list-players interval for new user ${client.user}`); client.intervals.push(setInterval(() => fetchAndSendUpdate(ws, 'list-players', client, docker), parseInt(process.env.LIST_PLAYERS_NEW_USER_INTERVAL_MS, 10))); } else { console.log(`Container ${client.user} is not running, skipping list-players interval`); ws.send(JSON.stringify({ type: 'list-players', error: `Container ${client.user} is not running` })); } } catch (error) { console.error(`Error checking container status for list-players for ${client.user}:`, error.message); ws.send(JSON.stringify({ type: 'list-players', error: `Failed to check container status: ${error.message}` })); } } await manageStatusChecks(ws, client, client.user, docker); if (client.subscriptions.has('docker-logs')) { if (client.logStream) { client.logStream.destroy(); client.logStream = null; } console.log(`Starting docker logs stream for new user ${client.user}`); await streamContainerLogs(docker, ws, client.user, client); } } } else if (data.type === 'request') { const { requestId, endpoint, method, body } = data; let response; if (endpoint.startsWith('/docker') || endpoint === '/docker') { response = client.user === 'Unknown' ? { error: 'User not identified' } : await getContainerStats(docker, client.user); console.log(`Docker stats request response for ${client.user}:`, response); } else if (endpoint === '/search' && method === 'POST' && body) { response = await apiRequest(endpoint, client.apiKey, method, body); response.totalResults = response.totalResults || (response.results ? response.results.length : 0); } else if (endpoint === '/server-properties' && method === 'GET') { response = client.user === 'Unknown' ? { error: 'User not identified' } : await readServerProperties(docker, client.user); } else if (endpoint === '/server-properties' && method === 'POST' && body && body.content) { response = client.user === 'Unknown' ? { error: 'User not identified' } : await writeServerProperties(docker, client.user, body.content); } else if (endpoint === '/update-mods' && method === 'POST') { response = client.user === 'Unknown' ? { error: 'User not identified' } : await updateMods(docker, client.user); } else if (endpoint === '/backup' && method === 'POST') { response = client.user === 'Unknown' ? { error: 'User not identified' } : await createBackup(docker, client.user); } else { response = await apiRequest(endpoint, client.apiKey, method, body); } if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ requestId, ...response })); } if (['my-link', 'my-geyser-link', 'my-sftp'].includes(endpoint) && !response.error) { await fetchAndSendUpdate(ws, endpoint, client, docker); if (endpoint === 'my-link') { const linkData = await apiRequest('/my-link-cache', client.apiKey); if (!linkData.error) { client.cache['my-link-cache'] = linkData; try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { console.log(`Performing status check after my-link request for ${client.user}`); const status = await checkConnectionStatus(linkData.hostname, linkData.port); ws.send(JSON.stringify({ type: 'connection-status', data: { isOnline: status.isOnline } })); } else { ws.send(JSON.stringify({ type: 'connection-status', error: `Container ${client.user} is not running` })); } } catch (error) { console.error(`Error checking container status for ${client.user}:`, error.message); ws.send(JSON.stringify({ type: 'connection-status', error: `Failed to check container status: ${error.message}` })); } } } else if (endpoint === 'my-geyser-link') { const geyserData = await apiRequest('/my-geyser-cache', client.apiKey); if (!geyserData.error) { client.cache['my-geyser-cache'] = geyserData; try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { console.log(`Performing status check after my-geyser-link request for ${client.user}`); const status = await checkGeyserStatus(geyserData.hostname, geyserData.port); ws.send(JSON.stringify({ type: 'geyser-status', data: { isOnline: status.isOnline } })); } else { ws.send(JSON.stringify({ type: 'geyser-status', error: `Container ${client.user} is not running` })); } } catch (error) { console.error(`Error checking container status for ${client.user}:`, error.message); ws.send(JSON.stringify({ type: 'geyser-status', error: `Failed to check container status: ${error.message}` })); } } } else if (endpoint === 'my-sftp') { const sftpData = await apiRequest('/my-sftp-cache', client.apiKey); if (!sftpData.error) { client.cache['my-sftp-cache'] = sftpData; try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { console.log(`Performing status check after my-sftp request for ${client.user}`); const status = await checkSftpStatus(sftpData.hostname, sftpData.port); ws.send(JSON.stringify({ type: 'sftp-status', data: { isOnline: status.isOnline } })); } else { ws.send(JSON.stringify({ type: 'sftp-status', error: `Container ${client.user} is not running` })); } } catch (error) { console.error(`Error checking container status for ${client.user}:`, error.message); ws.send(JSON.stringify({ type: 'sftp-status', error: `Failed to check container status: ${error.message}` })); } } } } } else if (['kick-player', 'ban-player', 'op-player', 'deop-player'].includes(data.type)) { const { requestId, player } = data; if (!player) { ws.send(JSON.stringify({ requestId, error: 'Player name is required' })); return; } if (client.user === 'Unknown') { ws.send(JSON.stringify({ requestId, error: 'User not identified' })); return; } try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { ws.send(JSON.stringify({ requestId, error: `Container ${client.user} is not running` })); return; } const command = { 'kick-player': `kick ${player}`, 'ban-player': `ban ${player}`, 'op-player': `op ${player}`, 'deop-player': `deop ${player}` }[data.type]; const response = await apiRequest('/console', client.apiKey, 'POST', { command }); if (!response.error) { const playerListResponse = await apiRequest('/list-players', client.apiKey); if (!playerListResponse.error) { ws.send(JSON.stringify({ type: 'list-players', data: playerListResponse })); } } if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ requestId, ...response })); } } catch (error) { console.error(`Error processing ${data.type} for ${player}:`, error.message); ws.send(JSON.stringify({ requestId, error: `Failed to process command: ${error.message}` })); } } else if (data.type === 'tell-player') { const { requestId, player, message } = data; if (!player || !message) { ws.send(JSON.stringify({ requestId, error: 'Player name and message are required' })); return; } if (client.user === 'Unknown') { ws.send(JSON.stringify({ requestId, error: 'User not identified' })); return; } try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { ws.send(JSON.stringify({ requestId, error: `Container ${client.user} is not running` })); return; } const response = await apiRequest('/tell', client.apiKey, 'POST', { username: player, message }); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ requestId, ...response })); } } catch (error) { console.error(`Error sending message to ${player}:`, error.message); ws.send(JSON.stringify({ requestId, error: `Failed to send message: ${error.message}` })); } } else if (data.type === 'give-player') { const { requestId, player, item, amount } = data; if (!player || !item || !amount) { ws.send(JSON.stringify({ requestId, error: 'Player name, item, and amount are required' })); return; } if (client.user === 'Unknown') { ws.send(JSON.stringify({ requestId, error: 'User not identified' })); return; } try { const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { ws.send(JSON.stringify({ requestId, error: `Container ${client.user} is not running` })); return; } const response = await apiRequest('/give', client.apiKey, 'POST', { username: player, item, amount }); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ requestId, ...response })); } } catch (error) { console.error(`Error giving item to ${player}:`, error.message); ws.send(JSON.stringify({ requestId, error: `Failed to give item: ${error.message}` })); } } else if (data.type === 'refresh') { console.log('Processing refresh request'); delete client.cache['hello']; delete client.cache['time']; await Promise.all([ ...staticEndpoints.filter(e => client.subscriptions.has(e)).map(e => fetchAndSendUpdate(ws, e, client, docker)), ...dynamicEndpoints.filter(e => client.subscriptions.has(e)).map(e => fetchAndSendUpdate(ws, e, client, docker)), client.subscriptions.has('list-players') ? fetchAndSendUpdate(ws, 'list-players', client, docker) : null ].filter(Boolean)); if (client.user && client.user !== 'Unknown') { try { const stats = await getContainerStats(docker, client.user); console.log(`Sending refreshed docker stats for ${client.user}:`, stats); if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: 'docker', data: { ...stats, user: client.user } })); } const container = docker.getContainer(client.user); const inspect = await container.inspect(); if (inspect.State.Status === 'running') { const linkData = client.cache['my-link-cache']; if (linkData && linkData.hostname && linkData.port && client.subscriptions.has('my-link-cache')) { console.log(`Performing refresh connection status check for ${client.user}`); const status = await checkConnectionStatus(linkData.hostname, linkData.port); ws.send(JSON.stringify({ type: 'connection-status', data: { isOnline: status.isOnline } })); } const geyserData = client.cache['my-geyser-cache']; if (geyserData && geyserData.hostname && geyserData.port && client.subscriptions.has('my-geyser-cache')) { console.log(`Performing refresh Geyser status check for ${client.user}`); const status = await checkGeyserStatus(geyserData.hostname, geyserData.port); ws.send(JSON.stringify({ type: 'geyser-status', data: { isOnline: status.isOnline } })); } const sftpData = client.cache['my-sftp-cache']; if (sftpData && sftpData.hostname && sftpData.port && client.subscriptions.has('my-sftp-cache')) { console.log(`Performing refresh SFTP status check for ${client.user}`); const status = await checkSftpStatus(sftpData.hostname, sftpData.port); ws.send(JSON.stringify({ type: 'sftp-status', data: { isOnline: status.isOnline } })); } } else { if (client.subscriptions.has('my-link-cache')) { ws.send(JSON.stringify({ type: 'connection-status', error: `Container ${client.user} is not running` })); } if (client.subscriptions.has('my-geyser-cache')) { ws.send(JSON.stringify({ type: 'geyser-status', error: `Container ${client.user} is not running` })); } if (client.subscriptions.has('my-sftp-cache')) { ws.send(JSON.stringify({ type: 'sftp-status', error: `Container ${client.user} is not running` })); } } } catch (error) { console.error(`Error during refresh for ${client.user}:`, error.message); ws.send(JSON.stringify({ type: 'docker', error: `Failed to refresh stats: ${error.message}` })); } } } } catch (error) { console.error('WebSocket message error:', error.message); ws.send(JSON.stringify({ error: `Invalid message: ${error.message}` })); } }); ws.on('close', () => { try { const client = clients.get(ws); client.intervals.forEach(intervalId => { console.log(`Clearing interval ID ${intervalId} for client ${client.user || 'unknown'}`); clearInterval(intervalId); }); client.intervals = []; if (client.logStream) { client.logStream.destroy(); client.logStream = null; } ['connectionStatusInterval', 'geyserStatusInterval', 'sftpStatusInterval', 'statusCheckMonitorInterval'].forEach((key) => { if (client[key]) { console.log(`Clearing ${key} for client ${client.user || 'unknown'}`); clearInterval(client[key]); client[key] = null; } }); clearInterval(heartbeatInterval); clients.delete(ws); console.log(`WebSocket client disconnected for user ${client.user || 'unknown'}`); } catch (error) { console.error('Error on WebSocket close:', error.message); } }); ws.on('error', (error) => console.error(`WebSocket error for user ${client.user || 'unknown'}:`, error.message)); }