Spaces:
Paused
Paused
| /** | |
| * minecraft.js β Monitor del servidor MC | |
| * v2.9 β: cache de 30s en fetchStatus para evitar conexiones TCP masivas | |
| */ | |
| import net from 'net'; | |
| import { readConfig } from './utils.js'; | |
| const config = readConfig(); | |
| let lastStatus = { online: false, players: 0, max: 0, version: '', motd: '', sample: [], failCount: 0 }; | |
| let monitorTimer = null; | |
| // ββ Cache de 30s para fetchStatus βββββββββββββββββββββββββββββββββββββββββββββ | |
| // FIX: sin esto, 10 requests simultΓ‘neas en tools.js hacen 10 conexiones TCP al MC | |
| let _statusCache = null; | |
| let _statusCacheTime = 0; | |
| const STATUS_CACHE_TTL = 30_000; // 30 segundos | |
| function encodeVI(v) { | |
| const b = []; let val = v >>> 0; | |
| do { let byte = val & 0x7F; val >>>= 7; if (val) byte |= 0x80; b.push(byte); } while (val); | |
| return Buffer.from(b); | |
| } | |
| function decodeVI(buf, off) { | |
| let r = 0, sh = 0, byte, pos = off; | |
| do { byte = buf[pos++]; r |= (byte & 0x7F) << sh; sh += 7; } while (byte & 0x80); | |
| return { value: r, next: pos }; | |
| } | |
| function stripColor(s) { return typeof s === 'string' ? s.replace(/\u00a7./g,'').trim() : ''; } | |
| function motdStr(d) { | |
| if (!d) return ''; | |
| if (typeof d === 'string') return d; | |
| const t = d.text || (d.translate) || ''; | |
| const extra = d.extra ? d.extra.map(e => e.text||'').join('') : ''; | |
| return t + extra; | |
| } | |
| export function fetchServerStatus(host, port, timeoutMs) { | |
| if (!timeoutMs) timeoutMs = 8000; | |
| return new Promise(resolve => { | |
| const sock = new net.Socket(); | |
| let buf = Buffer.alloc(0), done = false; | |
| const fail = () => { if(done)return; done=true; sock.destroy(); resolve({online:false,players:0,max:0,version:'',motd:'',sample:[]}); }; | |
| const succeed = obj => { if(done)return; done=true; sock.destroy(); resolve(Object.assign({online:true},obj)); }; | |
| sock.setTimeout(timeoutMs); | |
| sock.on('timeout', fail); sock.on('error', fail); sock.on('close', ()=>{ if(!done)fail(); }); | |
| sock.connect(port, host, () => { | |
| const hb = Buffer.from(host,'utf8'); | |
| const payload = Buffer.concat([encodeVI(0),encodeVI(763),encodeVI(hb.length),hb,Buffer.from([port>>8,port&0xFF]),encodeVI(1)]); | |
| sock.write(Buffer.concat([encodeVI(payload.length),payload])); | |
| sock.write(Buffer.from([1,0])); | |
| }); | |
| sock.on('data', chunk => { | |
| buf = Buffer.concat([buf,chunk]); | |
| try { | |
| const plen = decodeVI(buf,0); | |
| if (buf.length < plen.next + plen.value) return; | |
| const pid = decodeVI(buf, plen.next); | |
| if (pid.value !== 0) return; | |
| const slen = decodeVI(buf, pid.next); | |
| const j = JSON.parse(buf.slice(slen.next, slen.next+slen.value).toString('utf8')); | |
| succeed({ | |
| players: j.players?.online||0, max: j.players?.max||0, | |
| version: j.version?.name||'', motd: stripColor(motdStr(j.description)), | |
| sample: (j.players?.sample||[]).map(p=>p.name), | |
| }); | |
| } catch(_) {} | |
| }); | |
| }); | |
| } | |
| function tcpPing(host, port, t) { | |
| return new Promise(resolve => { | |
| const s = new net.Socket(); let d = false; | |
| const fin = r => { if(d)return; d=true; s.destroy(); resolve(r); }; | |
| s.setTimeout(t||5000); | |
| s.on('connect',()=>fin(true)); s.on('timeout',()=>fin(false)); s.on('error',()=>fin(false)); | |
| try { s.connect(port, host); } catch(_) { fin(false); } | |
| }); | |
| } | |
| export async function fetchStatus() { | |
| // FIX: devolver cache si es reciente | |
| if (_statusCache && Date.now() - _statusCacheTime < STATUS_CACHE_TTL) { | |
| return _statusCache; | |
| } | |
| const { ip, port } = config.server; | |
| try { | |
| const s = await fetchServerStatus(ip, port, 8000); | |
| const result = s.online ? s : { | |
| online: await tcpPing(ip, port, 5000), | |
| players: 0, max: 0, version: '', motd: '', sample: [], | |
| }; | |
| _statusCache = result; | |
| _statusCacheTime = Date.now(); | |
| return result; | |
| } catch(_) { | |
| const result = { online: false, players: 0, max: 0, version: '', motd: '', sample: [] }; | |
| _statusCache = result; | |
| _statusCacheTime = Date.now(); | |
| return result; | |
| } | |
| } | |
| export function startMinecraftMonitor(onOffline, onOnline) { | |
| const { ip, port, checkInterval } = config.server; | |
| console.log(`[MC] Monitor ${ip}:${port}`); | |
| async function check() { | |
| // Invalidar cache para que el monitor siempre compruebe de verdad | |
| _statusCache = null; | |
| const was = lastStatus.online; | |
| const s = await fetchStatus().catch(()=>({ online: false })); | |
| const fc = s.online ? 0 : (lastStatus.failCount||0)+1; | |
| lastStatus = { ...s, failCount: fc }; | |
| if (!s.online && fc >= 3 && was) { console.warn('[MC] OFFLINE'); if (onOffline) await onOffline().catch(()=>{}); } | |
| else if (s.online && !was) { console.log(`[MC] ONLINE ${s.players}/${s.max}`); if (onOnline) await onOnline().catch(()=>{}); } | |
| else if (s.online) { console.log(`[MC] ${s.players}/${s.max} jugadores`); } | |
| } | |
| setTimeout(check, 10000); | |
| monitorTimer = setInterval(check, checkInterval); | |
| } | |
| export function stopMinecraftMonitor() { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } } | |
| export function getServerStatus() { return lastStatus; } | |