diff --git a/includes/websocket.js b/includes/websocket.js index 92bfef9..c16fcec 100644 --- a/includes/websocket.js +++ b/includes/websocket.js @@ -7,6 +7,74 @@ 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') { + console.log(`Container ${user} not running, sending error`); + 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 { @@ -81,9 +149,13 @@ async function fetchAndSendUpdate(ws, endpoint, client, docker) { } } } - ws.send(JSON.stringify({ type: endpoint, data: response })); + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify({ type: endpoint, data: response })); + } } else { - ws.send(JSON.stringify({ type: endpoint, error: response.error })); + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify({ type: endpoint, error: response.error })); + } } } @@ -93,11 +165,14 @@ async function manageStatusChecks(ws, client, user, docker) { const inspect = await container.inspect(); const isRunning = inspect.State.Status === 'running'; - client.intervals.forEach(clearInterval); - client.intervals = []; + // Clear only status check intervals to prevent duplicates ['connectionStatusInterval', 'geyserStatusInterval', 'sftpStatusInterval', 'statusCheckMonitorInterval'].forEach((key) => { - if (client[key]) clearInterval(client[key]); - client[key] = null; + 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') { @@ -165,16 +240,21 @@ async function manageStatusChecks(ws, client, user, docker) { 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); - ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify({ type: 'connection-status', data: { isOnline: status.isOnline } })); + } } } catch (error) { console.error(`Error in ${statusType} check for ${user}:`, error.message); - ws.send(JSON.stringify({ type: statusType, data: { isOnline: false, error: 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]); @@ -183,7 +263,9 @@ async function manageStatusChecks(ws, client, user, docker) { if (data && data.hostname && data.port) { console.log(`Performing initial ${statusType} check for ${user}`); const status = await checkFn(data.hostname, data.port); - ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); + } } } } @@ -212,14 +294,32 @@ export function handleWebSocket(ws, req, docker) { connectionStatusInterval: null, geyserStatusInterval: null, sftpStatusInterval: null, - statusCheckMonitorInterval: 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); @@ -242,45 +342,8 @@ export function handleWebSocket(ws, req, docker) { console.log(`User identified: ${user}`); ws.send(JSON.stringify({ type: 'hello', data: hello })); - if (client.subscriptions.has('docker') && user !== 'Unknown') { - try { - const container = docker.getContainer(user); - const inspect = await container.inspect(); - if (inspect.State.Status === 'running') { - console.log(`Starting docker stats interval for ${user}`); - client.intervals.push(setInterval(async () => { - try { - console.log(`Fetching docker stats for ${user}`); - 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 { - console.log(`Sending docker stats for ${user}:`, stats); - ws.send(JSON.stringify({ type: 'docker', data: { ...stats, user } })); - } - } catch (error) { - console.error(`Error in docker stats interval for ${user}:`, error.message); - ws.send(JSON.stringify({ type: 'docker', error: `Failed to fetch stats: ${error.message}` })); - } - }, parseInt(process.env.DOCKER_STATS_INTERVAL_MS, 10))); - - // Send initial stats immediately - console.log(`Sending initial docker stats for ${user}`); - const initialStats = await getContainerStats(docker, user); - ws.send(JSON.stringify({ type: 'docker', data: { ...initialStats, user } })); - } else { - console.log(`Container ${user} is not running, skipping docker stats interval`); - ws.send(JSON.stringify({ type: 'docker', error: `Container ${user} is not running` })); - } - } catch (error) { - console.error(`Error checking container status for docker stats for ${user}:`, error.message); - ws.send(JSON.stringify({ type: 'docker', error: `Failed to check container status: ${error.message}` })); - } - } else if (user === 'Unknown') { - console.warn('Cannot start docker stats interval: User is Unknown'); - ws.send(JSON.stringify({ type: 'docker', error: 'User not identified' })); - } + // 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}`); @@ -353,52 +416,29 @@ export function handleWebSocket(ws, req, docker) { 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') { - client.intervals.forEach(clearInterval); - client.intervals = []; + // Clear only specific intervals, preserve others ['connectionStatusInterval', 'geyserStatusInterval', 'sftpStatusInterval', 'statusCheckMonitorInterval'].forEach((key) => { - if (client[key]) clearInterval(client[key]); - client[key] = null; + 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]); + } }); - if (client.subscriptions.has('docker')) { - try { - const container = docker.getContainer(client.user); - const inspect = await container.inspect(); - if (inspect.State.Status === 'running') { - console.log(`Starting docker stats interval for new user ${client.user}`); - client.intervals.push(setInterval(async () => { - try { - console.log(`Fetching docker stats for ${client.user}`); - const stats = await getContainerStats(docker, client.user); - if (stats.error) { - console.error(`Error fetching stats for ${client.user}: ${stats.error}`); - ws.send(JSON.stringify({ type: 'docker', error: stats.error })); - } else { - console.log(`Sending docker stats for ${client.user}:`, stats); - ws.send(JSON.stringify({ type: 'docker', data: { ...stats, user: client.user } })); - } - } catch (error) { - console.error(`Error in docker stats interval for ${client.user}:`, error.message); - ws.send(JSON.stringify({ type: 'docker', error: `Failed to fetch stats: ${error.message}` })); - } - }, parseInt(process.env.DOCKER_STATS_INTERVAL_MS, 10))); - - // Send initial stats immediately - console.log(`Sending initial docker stats for ${client.user}`); - const initialStats = await getContainerStats(docker, client.user); - ws.send(JSON.stringify({ type: 'docker', data: { ...initialStats, user: client.user } })); - } else { - console.log(`Container ${client.user} is not running, skipping docker stats interval`); - ws.send(JSON.stringify({ type: 'docker', error: `Container ${client.user} is not running` })); - } - } catch (error) { - console.error(`Error checking container status for docker stats for ${client.user}:`, error.message); - ws.send(JSON.stringify({ type: 'docker', error: `Failed to check container status: ${error.message}` })); - } - } + // Start Docker stats interval for new user + startDockerStatsInterval(ws, client, client.user, docker); if (client.subscriptions.has('list-players')) { try { @@ -444,7 +484,9 @@ export function handleWebSocket(ws, req, docker) { } else { response = await apiRequest(endpoint, client.apiKey, method, body); } - ws.send(JSON.stringify({ requestId, ...response })); + 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') { @@ -536,7 +578,9 @@ export function handleWebSocket(ws, req, docker) { ws.send(JSON.stringify({ type: 'list-players', data: playerListResponse })); } } - ws.send(JSON.stringify({ requestId, ...response })); + 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}` })); @@ -559,7 +603,9 @@ export function handleWebSocket(ws, req, docker) { return; } const response = await apiRequest('/tell', client.apiKey, 'POST', { username: player, message }); - ws.send(JSON.stringify({ requestId, ...response })); + 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}` })); @@ -582,7 +628,9 @@ export function handleWebSocket(ws, req, docker) { return; } const response = await apiRequest('/give', client.apiKey, 'POST', { username: player, item, amount }); - ws.send(JSON.stringify({ requestId, ...response })); + 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}` })); @@ -600,7 +648,9 @@ export function handleWebSocket(ws, req, docker) { try { const stats = await getContainerStats(docker, client.user); console.log(`Sending refreshed docker stats for ${client.user}:`, stats); - ws.send(JSON.stringify({ type: 'docker', data: { ...stats, user: client.user } })); + 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') { @@ -648,20 +698,29 @@ export function handleWebSocket(ws, req, docker) { ws.on('close', () => { try { const client = clients.get(ws); - client.intervals.forEach(clearInterval); + 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]) clearInterval(client[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'); + 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:', error.message)); + ws.on('error', (error) => console.error(`WebSocket error for user ${client.user || 'unknown'}:`, error.message)); } \ No newline at end of file