// services/logService.js const WebSocket = require('ws'); const decodeURIComponentSafe = str => { try { return decodeURIComponent(str); } catch { return str; // fallback } }; const wssList = []; // all WS servers const sockets = []; // { ws, name } const originalConsole = {}; let clients = new Map(); // ws => username // ASCII check function isAscii(str) { return /^[\x00-\x7F]*$/.test(str); } const attachLogService = async function (server, basePath = '/logs') { try { const wss = new WebSocket.Server({ noServer: true }); wssList.push(wss); // Save original console methods once ["log", "error", "warn", "info", "debug"].forEach(fn => { if (!originalConsole[fn]) originalConsole[fn] = console[fn]; }); // Override console methods (only once) if (!originalConsole._overridden) { ["log", "error", "warn", "info", "debug"].forEach(fn => { console[fn] = (...args) => { originalConsole[fn].apply(console, args); const msg = args.map(a => (typeof a === "string" ? a : JSON.stringify(a))).join(" "); broadcast({ type: fn, message: msg, client: "server" }); }; }); originalConsole._overridden = true; } // Handle HTTP upgrade requests server.on("upgrade", (request, socket, head) => { const urlParts = request.url.split('/').filter(Boolean); // e.g. ['logs','😎'] if (urlParts[0] === basePath.replace(/^\/+/, '') && urlParts[1]) { // Decode URL safely, preserving emojis const clientName = decodeURIComponentSafe(urlParts[1]); wss.handleUpgrade(request, socket, head, ws => { ws.clientName = clientName; wss.emit('connection', ws, request); clients.set(ws, clientName); sendClientsUpdate(); }); } else { socket.destroy(); } }); // WS connection wss.on('connection', ws => { sockets.push(ws); ws.on("message", rawMsg => { const msg = rawMsg.toString(); broadcast({ type: "message", message: msg, client: ws.clientName }); }); ws.on("close", () => { const index = sockets.indexOf(ws); if (index !== -1) sockets.splice(index, 1); clients.delete(ws); sendClientsUpdate(); }); }); return { success: true }; } catch (e) { throw new Error(e.message); } }; const detachLogService = async function () { try { Object.keys(originalConsole).forEach(fn => { if (fn !== "_overridden") console[fn] = originalConsole[fn]; }); sockets.forEach(ws => ws.close()); sockets.length = 0; wssList.forEach(wss => wss.close()); wssList.length = 0; return { success: true }; } catch (e) { throw new Error(e.message); } }; // Broadcast helper function broadcast(obj) { // Add timestamp obj.timestamp = new Date().toISOString(); const msg = JSON.stringify(obj); sockets.forEach(ws => { if (ws.readyState === WebSocket.OPEN) ws.send(msg); }); } function sendClientsUpdate() { const clientNames = Array.from(clients.values()); broadcast({ clients: clientNames }); } module.exports = { attachLogService, detachLogService, };