testando ui otimista
Deploy / Deploy (push) Successful in 1m27s
Details
Deploy / Deploy (push) Successful in 1m27s
Details
This commit is contained in:
parent
e08d80c223
commit
ba26dc1fe3
|
|
@ -12,12 +12,14 @@ const pino = require("pino");
|
|||
const os = require("os");
|
||||
const crypto = require("crypto");
|
||||
const { spawn } = require("child_process");
|
||||
const pendingSaves = new Set();
|
||||
|
||||
// --- RASTREAMENTO DE USUÁRIOS ---
|
||||
const activeUsers = {}; // Mapeia socket.id -> { username, room }
|
||||
|
||||
//import { LOG_SERVER } from "../utils.js"
|
||||
const LOG_SERVER = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/server`
|
||||
const SESSION_JSON = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/sessions`
|
||||
const LOG_SERVER = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/server`;
|
||||
const SESSION_JSON = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/sessions`;
|
||||
|
||||
const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
||||
<!DOCTYPE lmms-project>
|
||||
|
|
@ -99,7 +101,6 @@ const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
|||
</song>
|
||||
</lmms-project>`;
|
||||
|
||||
|
||||
// -------------------------
|
||||
// LOGGER DINÂMICO (GERENCIADOR)
|
||||
// -------------------------
|
||||
|
|
@ -153,20 +154,26 @@ const app = express();
|
|||
const PORT = process.env.PORT || 33001;
|
||||
|
||||
// ====== HTTPS (Opção B) ======
|
||||
const CERT_FULLCHAIN = process.env.SSL_FULLCHAIN || "/etc/letsencrypt/live/alice.ufsj.edu.br/fullchain.pem";
|
||||
const CERT_PRIVKEY = process.env.SSL_PRIVKEY || "/etc/letsencrypt/live/alice.ufsj.edu.br/privkey.pem";
|
||||
const CERT_FULLCHAIN =
|
||||
process.env.SSL_FULLCHAIN ||
|
||||
"/etc/letsencrypt/live/alice.ufsj.edu.br/fullchain.pem";
|
||||
const CERT_PRIVKEY =
|
||||
process.env.SSL_PRIVKEY ||
|
||||
"/etc/letsencrypt/live/alice.ufsj.edu.br/privkey.pem";
|
||||
|
||||
if (!fs.existsSync(CERT_FULLCHAIN) || !fs.existsSync(CERT_PRIVKEY)) {
|
||||
console.error("[HTTPS] Certificados não encontrados.\n" +
|
||||
` fullchain: ${CERT_FULLCHAIN}\n` +
|
||||
` privkey : ${CERT_PRIVKEY}\n` +
|
||||
"Defina SSL_FULLCHAIN/SSL_PRIVKEY ou instale os certificados no caminho padrão.");
|
||||
console.error(
|
||||
"[HTTPS] Certificados não encontrados.\n" +
|
||||
` fullchain: ${CERT_FULLCHAIN}\n` +
|
||||
` privkey : ${CERT_PRIVKEY}\n` +
|
||||
"Defina SSL_FULLCHAIN/SSL_PRIVKEY ou instale os certificados no caminho padrão.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const httpsOptions = {
|
||||
cert: fs.readFileSync(CERT_FULLCHAIN),
|
||||
key : fs.readFileSync(CERT_PRIVKEY),
|
||||
key: fs.readFileSync(CERT_PRIVKEY),
|
||||
};
|
||||
|
||||
const httpsServer = https.createServer(httpsOptions, app);
|
||||
|
|
@ -194,14 +201,14 @@ function dataFile(roomName) {
|
|||
function ensureRoom(roomName) {
|
||||
if (roomStates[roomName]) return roomStates[roomName];
|
||||
|
||||
// tenta carregar do disco
|
||||
try {
|
||||
const p = dataFile(roomName);
|
||||
if (fs.existsSync(p)) {
|
||||
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
roomStates[roomName] = {
|
||||
// 🔥 CORREÇÃO: Fallback para DEFAULT_PROJECT_XML se o JSON salvo estiver sem XML
|
||||
projectXml: j.projectXml || DEFAULT_PROJECT_XML,
|
||||
// ✅ Mantém o patternState em vez do projectXml gigante
|
||||
patternState: j.patternState || null,
|
||||
projectXml: j.projectXml || DEFAULT_PROJECT_XML, // Fallback para renderização
|
||||
audio: j.audio || { tracks: [], clips: [] },
|
||||
seq: j.seq || 0,
|
||||
tokensSeen: new Set(),
|
||||
|
|
@ -212,9 +219,8 @@ function ensureRoom(roomName) {
|
|||
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
||||
}
|
||||
|
||||
// Cria sala NOVA na memória
|
||||
roomStates[roomName] = {
|
||||
// 🔥 CORREÇÃO: Inicia com o XML do LMMS completo, não mais null
|
||||
patternState: null,
|
||||
projectXml: DEFAULT_PROJECT_XML,
|
||||
audio: { tracks: [], clips: [] },
|
||||
seq: 0,
|
||||
|
|
@ -225,28 +231,54 @@ function ensureRoom(roomName) {
|
|||
|
||||
function saveRoom(roomName) {
|
||||
const r = ensureRoom(roomName);
|
||||
try {
|
||||
fs.mkdirSync(SESSION_JSON, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
dataFile(roomName),
|
||||
JSON.stringify({ projectXml: r.projectXml, audio: r.audio, seq: r.seq })
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(`[persist] falha ao salvar estado da sala ${roomName}:`, e);
|
||||
}
|
||||
|
||||
// Se já tem um salvamento agendado para esta sala, ignora
|
||||
if (pendingSaves.has(roomName)) return;
|
||||
pendingSaves.add(roomName);
|
||||
|
||||
// Aguarda 2 segundos antes de salvar (Debounce)
|
||||
setTimeout(() => {
|
||||
try {
|
||||
fs.mkdirSync(SESSION_JSON, { recursive: true });
|
||||
// Extraia a stringificação do Event Loop principal usando promessas ou apenas aceite o custo mitigado pelo debounce
|
||||
const payload = JSON.stringify({
|
||||
patternState: r.patternState,
|
||||
projectXml: r.projectXml, // Mantemos o backup antigo se existir
|
||||
audio: r.audio,
|
||||
seq: r.seq,
|
||||
});
|
||||
|
||||
fs.writeFile(dataFile(roomName), payload, (err) => {
|
||||
pendingSaves.delete(roomName); // Libera para futuros salvamentos
|
||||
if (err)
|
||||
console.warn(
|
||||
`[persist] Erro assíncrono ao salvar sala ${roomName}:`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
pendingSaves.delete(roomName);
|
||||
console.warn(
|
||||
`[persist] falha ao preparar salvamento da sala ${roomName}:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}, 2000); // 2 segundos de respiro para o servidor
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Server / IO
|
||||
// Server / IO
|
||||
// -------------------------
|
||||
const io = new Server(httpsServer, {
|
||||
cors: {
|
||||
// CORREÇÃO: Simplificado para a origem real do cliente, que é onde o HTML é servido.
|
||||
// O cliente provavelmente é servido a partir de https://alice.ufsj.edu.br (porta 443).
|
||||
origin: "https://alice.ufsj.edu.br",
|
||||
origin: "https://alice.ufsj.edu.br",
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true,
|
||||
},
|
||||
// Evita que o servidor chute o usuário ao receber XMLs pesados!
|
||||
maxHttpBufferSize: 5e7,
|
||||
});
|
||||
|
||||
console.log("Servidor Backend do MMP-Search está no ar!");
|
||||
|
|
@ -269,15 +301,10 @@ function basenameNoExt(p) {
|
|||
function applyAuthoritativeAction(roomName, action) {
|
||||
const room = ensureRoom(roomName);
|
||||
const state = room.audio;
|
||||
|
||||
// idempotência por token (se fornecido)
|
||||
if (action.__token && room.tokensSeen.has(action.__token)) {
|
||||
return null;
|
||||
}
|
||||
if (action.__token) room.tokensSeen.add(action.__token);
|
||||
|
||||
let mutated = false;
|
||||
|
||||
// REMOVIDO: checagem de tokensSeen e if (action.__token)
|
||||
|
||||
switch (action.type) {
|
||||
case "ADD_AUDIO_LANE": {
|
||||
// Usa o trackId enviado pelo cliente, se existir
|
||||
|
|
@ -304,7 +331,7 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
) {
|
||||
console.warn(
|
||||
`[Server] Ação ADD_AUDIO_CLIP rejeitada (dados inválidos):`,
|
||||
action
|
||||
action,
|
||||
);
|
||||
return null; // Rejeita a ação, não faz broadcast
|
||||
}
|
||||
|
|
@ -340,7 +367,7 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
if (!action.clipId || !action.props) {
|
||||
console.warn(
|
||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
||||
action
|
||||
action,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -352,7 +379,7 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
) {
|
||||
console.warn(
|
||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (trackId inválido):`,
|
||||
action
|
||||
action,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -362,7 +389,7 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
) {
|
||||
console.warn(
|
||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (startTimeInSeconds inválido):`,
|
||||
action
|
||||
action,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -377,7 +404,7 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
|
||||
case "REMOVE_AUDIO_CLIP": {
|
||||
const i = state.clips.findIndex(
|
||||
(x) => String(x.id) === String(action.clipId)
|
||||
(x) => String(x.id) === String(action.clipId),
|
||||
);
|
||||
if (i >= 0) {
|
||||
state.clips.splice(i, 1);
|
||||
|
|
@ -398,13 +425,13 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
// Se removemos a track, os clips dela têm que sumir.
|
||||
const prevCount = state.clips.length;
|
||||
state.clips = state.clips.filter((c) => {
|
||||
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
|
||||
const parentTrackExists = state.tracks.some(t => t.id === c.trackId);
|
||||
return c.trackId && parentTrackExists;
|
||||
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
|
||||
const parentTrackExists = state.tracks.some((t) => t.id === c.trackId);
|
||||
return c.trackId && parentTrackExists;
|
||||
});
|
||||
|
||||
if (state.clips.length !== prevCount) {
|
||||
mutated = true;
|
||||
mutated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -416,9 +443,9 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
|
||||
if (!mutated) return null;
|
||||
|
||||
room.seq += 1; // numeração autoritativa
|
||||
saveRoom(roomName); // persistência em disco
|
||||
return { ...action, __seq: room.seq }; // devolve ação com seq para broadcast
|
||||
// REMOVIDO: room.seq += 1; e atribuição de __seq
|
||||
saveRoom(roomName);
|
||||
return action; // Apenas retorna a ação modificada
|
||||
}
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
|
|
@ -439,61 +466,83 @@ io.on("connection", (socket) => {
|
|||
activeUsers[socket.id] = { username: finalUserName, room: roomName };
|
||||
|
||||
await socket.join(roomName);
|
||||
console.log(`[ROOM] 🟢 Usuário ${finalUserName} (${socket.id}) entrou na sala: ${roomName}`);
|
||||
console.log(
|
||||
`[ROOM] 🟢 Usuário ${finalUserName} (${socket.id}) entrou na sala: ${roomName}`,
|
||||
);
|
||||
|
||||
// Avisa os outros para mostrar o Popup Verde
|
||||
socket.to(roomName).emit("user_joined", { username: finalUserName, id: socket.id });
|
||||
socket
|
||||
.to(roomName)
|
||||
.emit("user_joined", { username: finalUserName, id: socket.id });
|
||||
|
||||
// Atualiza o monitor de console
|
||||
const clientsInRoom = Array.from(io.sockets.adapter.rooms.get(roomName) || []);
|
||||
const clientsInRoom = Array.from(
|
||||
io.sockets.adapter.rooms.get(roomName) || [],
|
||||
);
|
||||
io.to(roomName).emit("room_status", {
|
||||
room: roomName, count: clientsInRoom.length, users: clientsInRoom
|
||||
room: roomName,
|
||||
count: clientsInRoom.length,
|
||||
users: clientsInRoom,
|
||||
});
|
||||
logMyRooms(socket, "JOIN");
|
||||
|
||||
const room = ensureRoom(roomName);
|
||||
if (room.projectXml) {
|
||||
socket.emit("load_project_state", room.projectXml);
|
||||
// Envia o JSON se existir, senão envia o XML de fallback
|
||||
if (room.patternState) {
|
||||
socket.emit("load_project_state", { patternState: room.patternState });
|
||||
} else if (room.projectXml) {
|
||||
socket.emit("load_project_state", { xml: room.projectXml });
|
||||
}
|
||||
|
||||
socket.emit("action_broadcast", {
|
||||
action: { type: "AUDIO_SNAPSHOT", snapshot: room.audio, __seq: room.seq, __target: socket.id },
|
||||
action: {
|
||||
type: "AUDIO_SNAPSHOT",
|
||||
snapshot: room.audio,
|
||||
__seq: room.seq,
|
||||
__target: socket.id,
|
||||
},
|
||||
});
|
||||
socket.to(roomName).emit("feedback", `Usuário ${finalUserName} entrou na sala.`);
|
||||
socket
|
||||
.to(roomName)
|
||||
.emit("feedback", `Usuário ${finalUserName} entrou na sala.`);
|
||||
});
|
||||
|
||||
// --- GATILHO DE DESPERTAR ÚNICO ---
|
||||
socket.on("request_full_sync", ({ room }) => {
|
||||
const targetRoom = ensureRoom(room);
|
||||
if (targetRoom && targetRoom.projectXml) {
|
||||
console.log(`[SYNC] Cliente ${socket.id} acordou e pediu Full Sync da sala ${room}`);
|
||||
socket.emit("load_project_state", targetRoom.projectXml);
|
||||
}
|
||||
});
|
||||
// --- GATILHO DE DESPERTAR ÚNICO ---
|
||||
socket.on("request_full_sync", ({ room }) => {
|
||||
const targetRoom = ensureRoom(room);
|
||||
if (targetRoom && targetRoom.projectXml) {
|
||||
console.log(`[SYNC] Cliente ${socket.id} acordou e pediu Full Sync`);
|
||||
// 🔥 Manda o XML dentro de um objeto avisando que é um pedido FORÇADO
|
||||
socket.emit("load_project_state", {
|
||||
xml: targetRoom.projectXml,
|
||||
forceSync: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================
|
||||
// SAÍDA E LIMPEZA UNIFICADA (Aviso e Desconexão)
|
||||
// =========================================================
|
||||
socket.on("disconnecting", () => {
|
||||
const user = activeUsers[socket.id];
|
||||
if (user) {
|
||||
// =========================================================
|
||||
// SAÍDA E LIMPEZA UNIFICADA (Aviso e Desconexão)
|
||||
// =========================================================
|
||||
socket.on("disconnecting", () => {
|
||||
const user = activeUsers[socket.id];
|
||||
if (user) {
|
||||
console.log(`[ROOM] 🔴 ${user.username} saiu da sala ${user.room}`);
|
||||
socket.to(user.room).emit("user_left", { username: user.username });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
delete activeUsers[socket.id]; // Limpa a identidade
|
||||
console.log(`Cliente desconectado: ${socket.id}`);
|
||||
|
||||
// Remove locks de renderização
|
||||
for (const [roomName, lockId] of renderLocks.entries()) {
|
||||
if (lockId === socket.id) {
|
||||
renderLocks.delete(roomName);
|
||||
console.log(`Lock de render da sala ${roomName} liberado.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
delete activeUsers[socket.id]; // Limpa a identidade
|
||||
console.log(`Cliente desconectado: ${socket.id}`);
|
||||
|
||||
// Remove locks de renderização
|
||||
for (const [roomName, lockId] of renderLocks.entries()) {
|
||||
if (lockId === socket.id) {
|
||||
renderLocks.delete(roomName);
|
||||
console.log(`Lock de render da sala ${roomName} liberado.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// =======================================
|
||||
// CLOCK SYNC
|
||||
|
|
@ -511,7 +560,7 @@ socket.on("disconnect", () => {
|
|||
cb && cb({ ok: false, error: "invalid_payload" });
|
||||
console.warn(
|
||||
`[broadcast_action] payload inválido de ${socket.id}:`,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -528,7 +577,7 @@ socket.on("disconnect", () => {
|
|||
socketId: socket.id,
|
||||
action: action,
|
||||
},
|
||||
"action_received"
|
||||
"action_received",
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(`[Logger] Falha ao logar ação para sala ${roomName}:`, e);
|
||||
|
|
@ -541,7 +590,7 @@ socket.on("disconnect", () => {
|
|||
// Garante que o emissor está na sala (caso o join tenha falhado/atrasado)
|
||||
if (!socket.rooms.has(roomName)) {
|
||||
console.warn(
|
||||
`[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...`
|
||||
`[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...`,
|
||||
);
|
||||
await socket.join(roomName);
|
||||
logMyRooms(socket, "FORCED_JOIN");
|
||||
|
|
@ -550,18 +599,48 @@ socket.on("disconnect", () => {
|
|||
// Carrega estado da sala
|
||||
const room = ensureRoom(roomName);
|
||||
|
||||
// ==========================================
|
||||
// 1. PREVENÇÃO DE ECO DUPLO E MEMORY LEAK
|
||||
// ==========================================
|
||||
if (action.__token) {
|
||||
if (room.tokensSeen.has(action.__token)) {
|
||||
console.log(`[Deduplicação] Ação repetida ignorada: ${action.type}`);
|
||||
return; // Mata a ação aqui, não faz broadcast!
|
||||
}
|
||||
room.tokensSeen.add(action.__token);
|
||||
|
||||
// Limpa tokens antigos (mantém apenas os últimos 1000)
|
||||
if (room.tokensSeen.size > 1000) {
|
||||
const oldestToken = room.tokensSeen.values().next().value;
|
||||
room.tokensSeen.delete(oldestToken);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. ORDEM AUTORITATIVA GLOBAL (JUIZ)
|
||||
// ==========================================
|
||||
room.seq += 1;
|
||||
action.__seq = room.seq; // TODAS as ações agora ganham um número de sequência
|
||||
|
||||
// Persiste estado do Pattern (Notas/Sequenciador)
|
||||
// Aceita tanto carregamento total quanto atualização de notas
|
||||
if ((action.type === "LOAD_PROJECT" || action.type === "SYNC_PATTERN_STATE") && action.xml) {
|
||||
// Proteção: Não salva se o XML for vazio (evita corromper a sala com o bug antigo)
|
||||
if (action.xml.trim().length > 0) {
|
||||
if (
|
||||
(action.type === "LOAD_PROJECT" ||
|
||||
action.type === "SYNC_PATTERN_STATE") &&
|
||||
action.xml
|
||||
) {
|
||||
const xmlTrimmed = action.xml.trim();
|
||||
|
||||
// Validação de Integridade: O XML chegou inteiro?
|
||||
if (xmlTrimmed.length > 0 && xmlTrimmed.endsWith("</lmms-project>")) {
|
||||
room.projectXml = action.xml;
|
||||
saveRoom(roomName);
|
||||
console.log(
|
||||
`[broadcast_action] XML da sala ${roomName} atualizado via ${action.type}.`
|
||||
`[broadcast_action] XML da sala ${roomName} salvo com segurança (${action.type}).`,
|
||||
);
|
||||
} else {
|
||||
console.warn(`[Server] Ignorando XML vazio vindo de ${action.type}`);
|
||||
console.warn(
|
||||
`[Server] ALERTA: XML corrompido/incompleto rejeitado de ${socket.id} na ação ${action.type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -573,18 +652,18 @@ socket.on("disconnect", () => {
|
|||
const idsInRoom = socketsInRoom.map((s) => s.id);
|
||||
console.log(
|
||||
`[broadcast_action] ${action.type} (token=${action.__token}) para sala "${roomName}" com ${idsInRoom.length} cliente(s):`,
|
||||
idsInRoom
|
||||
idsInRoom,
|
||||
);
|
||||
|
||||
// Se foi uma ação do editor de áudio e foi aplicada, rebroadcast com __seq
|
||||
if (maybeApplied) {
|
||||
io.to(roomName).emit("action_broadcast", { action: maybeApplied });
|
||||
socket.to(roomName).emit("action_broadcast", { action: maybeApplied });
|
||||
return;
|
||||
}
|
||||
|
||||
// Caso contrário, apenas repercute a ação como antes (sem __seq)
|
||||
io.to(roomName).emit("action_broadcast", { action });
|
||||
});
|
||||
// Caso contrário, repercute a ação UMA ÚNICA VEZ para todo mundo na sala
|
||||
socket.to(roomName).emit("action_broadcast", { action });
|
||||
}); // <--- Fim do socket.on("broadcast_action")
|
||||
|
||||
// =======================================
|
||||
// Retrocompat (daw_action)
|
||||
|
|
@ -594,7 +673,7 @@ socket.on("disconnect", () => {
|
|||
if (!room || !action) return;
|
||||
|
||||
console.warn(
|
||||
"[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK."
|
||||
"[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK.",
|
||||
);
|
||||
|
||||
// (Opcional, mas recomendado) Adicionando log dinâmico aqui também
|
||||
|
|
@ -606,7 +685,7 @@ socket.on("disconnect", () => {
|
|||
socketId: socket.id,
|
||||
action: action,
|
||||
},
|
||||
"action_received (deprecated daw_action)"
|
||||
"action_received (deprecated daw_action)",
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(`[Logger] Falha ao logar daw_action para sala ${room}:`, e);
|
||||
|
|
@ -614,7 +693,7 @@ socket.on("disconnect", () => {
|
|||
|
||||
if (!socket.rooms.has(room)) {
|
||||
console.warn(
|
||||
`[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...`
|
||||
`[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...`,
|
||||
);
|
||||
await socket.join(room);
|
||||
logMyRooms(socket, "FORCED_JOIN(daw)");
|
||||
|
|
@ -625,7 +704,7 @@ socket.on("disconnect", () => {
|
|||
r.projectXml = action.xml;
|
||||
saveRoom(room);
|
||||
console.log(
|
||||
`[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).`
|
||||
`[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -633,7 +712,7 @@ socket.on("disconnect", () => {
|
|||
const idsInRoom = socketsInRoom.map((s) => s.id);
|
||||
console.log(
|
||||
`[daw_action] ${action.type} para sala "${room}" (${idsInRoom.length} cliente(s)):`,
|
||||
idsInRoom
|
||||
idsInRoom,
|
||||
);
|
||||
|
||||
io.in(room).emit("action_broadcast", { action });
|
||||
|
|
@ -668,9 +747,7 @@ app.use(express.json({ limit: "250mb" }));
|
|||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
|
||||
const allowed = new Set([
|
||||
"https://alice.ufsj.edu.br",
|
||||
]);
|
||||
const allowed = new Set(["https://alice.ufsj.edu.br"]);
|
||||
|
||||
if (origin && allowed.has(origin)) {
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
|
|
@ -700,14 +777,15 @@ function buildLmmsCommand(inputMmp, outputAudio, ext) {
|
|||
return { cmd: LMMS_BIN, args: lmmsArgs };
|
||||
}
|
||||
|
||||
|
||||
function sanitizeFileName(name) {
|
||||
return String(name || "projeto")
|
||||
.normalize("NFKD")
|
||||
.replace(/[^\w\s.-]/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "_")
|
||||
.slice(0, 120) || "projeto";
|
||||
return (
|
||||
String(name || "projeto")
|
||||
.normalize("NFKD")
|
||||
.replace(/[^\w\s.-]/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "_")
|
||||
.slice(0, 120) || "projeto"
|
||||
);
|
||||
}
|
||||
|
||||
function isSafeZipEntry(name) {
|
||||
|
|
@ -721,12 +799,17 @@ function isSafeZipEntry(name) {
|
|||
async function safeUnzip(zipPath, destDir) {
|
||||
// lista entradas
|
||||
const list = await run("unzip", ["-Z1", zipPath], { timeoutMs: 60_000 });
|
||||
const entries = String(list.out || "").split("\n").map(x => x.trim()).filter(Boolean);
|
||||
const entries = String(list.out || "")
|
||||
.split("\n")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entries.length) throw new Error("zip sem entradas (ou unzip falhou ao listar)");
|
||||
if (!entries.length)
|
||||
throw new Error("zip sem entradas (ou unzip falhou ao listar)");
|
||||
|
||||
for (const e of entries) {
|
||||
if (!isSafeZipEntry(e)) throw new Error(`zip contém caminho inseguro: ${e}`);
|
||||
if (!isSafeZipEntry(e))
|
||||
throw new Error(`zip contém caminho inseguro: ${e}`);
|
||||
}
|
||||
|
||||
// extrai
|
||||
|
|
@ -747,7 +830,6 @@ async function findFirstMmp(dir) {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
function run(cmd, args, { timeoutMs, cwd } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
|
||||
|
|
@ -758,7 +840,9 @@ function run(cmd, args, { timeoutMs, cwd } = {}) {
|
|||
p.stderr.on("data", (d) => (err += d.toString()));
|
||||
|
||||
const t = setTimeout(() => {
|
||||
try { p.kill("SIGKILL"); } catch {}
|
||||
try {
|
||||
p.kill("SIGKILL");
|
||||
} catch {}
|
||||
reject(new Error(`${cmd} timeout (${timeoutMs}ms)\n${err || out}`));
|
||||
}, timeoutMs || LMMS_TIMEOUT_MS);
|
||||
|
||||
|
|
@ -778,26 +862,40 @@ function run(cmd, args, { timeoutMs, cwd } = {}) {
|
|||
// --- ENDPOINT DE MONITORAMENTO DE SALAS (ADMIN) ---
|
||||
app.get("/api/admin/salas", (req, res) => {
|
||||
const roomsData = {};
|
||||
|
||||
|
||||
// Itera por todas as salas ativas na memória do Socket.IO
|
||||
io.sockets.adapter.rooms.forEach((value, key) => {
|
||||
// O Socket.IO cria uma "sala" automática para cada usuário.
|
||||
// Vamos filtrar para mostrar apenas as salas reais do seu sistema.
|
||||
if (!io.sockets.sockets.has(key)) {
|
||||
roomsData[key] = {
|
||||
total_usuarios: value.size,
|
||||
ids_conectados: Array.from(value)
|
||||
};
|
||||
}
|
||||
// O Socket.IO cria uma "sala" automática para cada usuário.
|
||||
// Vamos filtrar para mostrar apenas as salas reais do seu sistema.
|
||||
if (!io.sockets.sockets.has(key)) {
|
||||
roomsData[key] = {
|
||||
total_usuarios: value.size,
|
||||
ids_conectados: Array.from(value),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
salas_ativas: Object.keys(roomsData).length,
|
||||
detalhes: roomsData
|
||||
res.json({
|
||||
salas_ativas: Object.keys(roomsData).length,
|
||||
detalhes: roomsData,
|
||||
});
|
||||
});
|
||||
// --------------------------------------------------
|
||||
|
||||
// Rota para salvar o backup do JSON sem usar o WebSocket
|
||||
app.post("/api/save_room_state", (req, res) => {
|
||||
const { roomName, patternState } = req.body;
|
||||
|
||||
if (!roomName || !patternState) {
|
||||
return res.status(400).json({ error: "Faltam dados" });
|
||||
}
|
||||
|
||||
const room = ensureRoom(roomName);
|
||||
room.patternState = patternState;
|
||||
saveRoom(roomName);
|
||||
return res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
// trava simples pra não renderizar a mesma sala em paralelo
|
||||
const renderLocks = new Map();
|
||||
|
||||
|
|
@ -807,7 +905,9 @@ app.post("/render", async (req, res) => {
|
|||
const ext = String(format || "wav").toLowerCase();
|
||||
const allowed = new Set(["wav", "ogg", "flac", "mp3"]);
|
||||
if (!allowed.has(ext)) {
|
||||
return res.status(400).json({ ok: false, error: "invalid_format", allowed: [...allowed] });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ ok: false, error: "invalid_format", allowed: [...allowed] });
|
||||
}
|
||||
|
||||
// trava sala (se aplicável)
|
||||
|
|
@ -833,7 +933,8 @@ app.post("/render", async (req, res) => {
|
|||
await safeUnzip(zipPath, tmpDir);
|
||||
|
||||
inputMmpPath = await findFirstMmp(tmpDir);
|
||||
if (!inputMmpPath) throw new Error("nenhum .mmp encontrado após extrair o pacote");
|
||||
if (!inputMmpPath)
|
||||
throw new Error("nenhum .mmp encontrado após extrair o pacote");
|
||||
|
||||
cwdForLmms = path.dirname(inputMmpPath); // isso faz o LMMS resolver samples/...
|
||||
} else {
|
||||
|
|
@ -847,7 +948,9 @@ app.post("/render", async (req, res) => {
|
|||
}
|
||||
|
||||
if (!projectXml || String(projectXml).trim().length === 0) {
|
||||
return res.status(400).json({ ok: false, error: "missing_xml_or_room" });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ ok: false, error: "missing_xml_or_room" });
|
||||
}
|
||||
|
||||
inputMmpPath = path.join(tmpDir, "project.mmp");
|
||||
|
|
@ -862,15 +965,25 @@ app.post("/render", async (req, res) => {
|
|||
const downloadName = `${fileBase}.${ext}`;
|
||||
|
||||
return res.download(outputPath, downloadName, (err) => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
if (roomName) renderLocks.delete(roomName);
|
||||
if (err) console.error("[/render] download error:", err);
|
||||
});
|
||||
} catch (e) {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
if (roomName) renderLocks.delete(roomName);
|
||||
console.error("[/render] fail:", e);
|
||||
return res.status(500).json({ ok: false, error: "render_failed", details: String(e?.message || e) });
|
||||
return res
|
||||
.status(500)
|
||||
.json({
|
||||
ok: false,
|
||||
error: "render_failed",
|
||||
details: String(e?.message || e),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -883,25 +996,29 @@ app.post("/notify-update", express.json(), (req, res) => {
|
|||
// O evento deve ser algo que ui.js entenda.
|
||||
io.emit("system_update", {
|
||||
type: "RELOAD_SAMPLES",
|
||||
message: "Novo Sample/Project adicionado. Recarregando o navegador de arquivos...",
|
||||
message:
|
||||
"Novo Sample/Project adicionado. Recarregando o navegador de arquivos...",
|
||||
});
|
||||
console.log(
|
||||
"[Notificação] Evento 'RELOAD_SAMPLES' emitido para todos os clientes."
|
||||
"[Notificação] Evento 'RELOAD_SAMPLES' emitido para todos os clientes.",
|
||||
);
|
||||
|
||||
return res.status(200).send({ success: true, message: "Notificação de Samples/Projetos enviada." });
|
||||
return res
|
||||
.status(200)
|
||||
.send({
|
||||
success: true,
|
||||
message: "Notificação de Samples/Projetos enviada.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(400).send({ success: false, message: "updateType inválido." });
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send(
|
||||
"Servidor Backend do MMP-Search está no ar novamente!"
|
||||
);
|
||||
res.send("Servidor Backend do MMP-Search está no ar novamente!");
|
||||
});
|
||||
|
||||
// ====== START ======
|
||||
httpsServer.listen(PORT, () => {
|
||||
console.log(`Servidor escutando na porta ${PORT}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue