diff --git a/meet/server/lib/interactiveServer.js b/meet/server/lib/interactiveServer.js index 847278ec..19dfca2c 100644 --- a/meet/server/lib/interactiveServer.js +++ b/meet/server/lib/interactiveServer.js @@ -1,610 +1,472 @@ const os = require('os'); const path = require('path'); const repl = require('repl'); const readline = require('readline'); const net = require('net'); const fs = require('fs'); const mediasoup = require('mediasoup'); const colors = require('colors/safe'); const pidusage = require('pidusage'); const SOCKET_PATH_UNIX = '/tmp/kolabmeet-server.sock'; const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'kolabmeet-server'); const SOCKET_PATH = os.platform() === 'win32' ? SOCKET_PATH_WIN : SOCKET_PATH_UNIX; // Maps to store all mediasoup objects. const workers = new Map(); const routers = new Map(); const transports = new Map(); const producers = new Map(); const consumers = new Map(); const dataProducers = new Map(); const dataConsumers = new Map(); class Interactive { constructor(socket) { this._socket = socket; this._isTerminalOpen = false; } + async dump(name, params, entries) { + const list = params[0] ? [entries.get(params[0])] : entries.values(); + + for (const entry of list) { + if (!entry) { + this.error(`${name} not found`); + break; + } + + try { + const dump = await entry.dump(); + this.log(`${name}.dump():\n${JSON.stringify(dump, null, ' ')}`); + } catch (error) { + this.error(`${name}.dump() failed: ${error}`); + } + } + } + + async dumpStats(name, params, entries) { + const list = params[0] ? [entries.get(params[0])] : entries.values(); + + for (const entry of list) { + if (!entry) { + this.error(`${name} not found`); + break; + } + + try { + const stats = await entry.getStats(); + this.log(`${name}.getStats():\n${JSON.stringify(stats, null, ' ')}`); + } catch (error) { + this.error(`${name}.getStats() failed: ${error}`); + } + } + } + openCommandConsole() { const cmd = readline.createInterface( { input : this._socket, output : this._socket, terminal : true }); cmd.on('close', () => { if (this._isTerminalOpen) return; this.log('\nexiting...'); this._socket.end(); }); const readStdin = () => { cmd.question('cmd> ', async (input) => { const params = input.split(/[\s\t]+/); const command = params.shift(); switch (command) { case '': { readStdin(); break; } case 'h': case 'help': { this.log(''); this.log('available commands:'); this.log('- h, help : show this message'); this.log('- usage : show CPU and memory usage of the Node.js and mediasoup-worker processes'); this.log('- logLevel level : changes logLevel in all mediasoup Workers'); this.log('- logTags [tag] [tag] : changes logTags in all mediasoup Workers (values separated by space)'); this.log('- dumpRooms : dump all rooms'); this.log('- dumpPeers : dump all peers'); this.log('- dw, dumpWorkers : dump mediasoup Workers'); this.log('- dr, dumpRouter [id] : dump mediasoup Router with given id (or the latest created one)'); this.log('- dt, dumpTransport [id] : dump mediasoup Transport with given id (or the latest created one)'); this.log('- dp, dumpProducer [id] : dump mediasoup Producer with given id (or the latest created one)'); this.log('- dc, dumpConsumer [id] : dump mediasoup Consumer with given id (or the latest created one)'); this.log('- st, statsTransport [id] : get stats for mediasoup Transport with given id (or all)'); this.log('- sp, statsProducer [id] : get stats for mediasoup Producer with given id (or all)'); this.log('- sc, statsConsumer [id] : get stats for mediasoup Consumer with given id (or all)'); this.log('- ddp, dumpDataProducer [id] : dump mediasoup DataProducer with given id (or the latest created one)'); this.log('- ddc, dumpDataConsumer [id] : dump mediasoup DataConsumer with given id (or the latest created one)'); this.log('- sdp, statsDataProducer [id] : get stats for mediasoup DataProducer with given id (or the latest created one)'); this.log('- sdc, statsDataConsumer [id] : get stats for mediasoup DataConsumer with given id (or the latest created one)'); this.log('- t, terminal : open Node REPL Terminal'); this.log(''); readStdin(); break; } case 'u': case 'usage': { let usage = await pidusage(process.pid); this.log(`Node.js process [pid:${process.pid}]:\n${JSON.stringify(usage, null, ' ')}`); for (const worker of workers.values()) { usage = await pidusage(worker.pid); this.log(`mediasoup-worker process [pid:${worker.pid}]:\n${JSON.stringify(usage, null, ' ')}`); } break; } case 'logLevel': { const level = params[0]; const promises = []; for (const worker of workers.values()) { promises.push(worker.updateSettings({ logLevel: level })); } try { await Promise.all(promises); this.log('done'); } catch (error) { this.error(String(error)); } break; } case 'logTags': { const tags = params; const promises = []; for (const worker of workers.values()) { promises.push(worker.updateSettings({ logTags: tags })); } try { await Promise.all(promises); this.log('done'); } catch (error) { this.error(String(error)); } break; } case 'stats': { this.log(`rooms:${global.rooms.size}\npeers:${global.peers.size}`); break; } case 'dumpRooms': { for (const room of global.rooms.values()) { try { const dump = await room.dump(); this.log(`room.dump():\n${JSON.stringify(dump, null, ' ')}`); } catch (error) { this.error(`room.dump() failed: ${error}`); } } break; } case 'dumpPeers': { for (const peer of global.peers.values()) { try { const dump = await peer.peerInfo; this.log(`peer.peerInfo():\n${JSON.stringify(dump, null, ' ')}`); } catch (error) { this.error(`peer.peerInfo() failed: ${error}`); } } break; } case 'dw': case 'dumpWorkers': { for (const worker of workers.values()) { try { const dump = await worker.dump(); this.log(`worker.dump():\n${JSON.stringify(dump, null, ' ')}`); } catch (error) { this.error(`worker.dump() failed: ${error}`); } } break; } case 'dr': case 'dumpRouter': { - const id = params[0] || Array.from(routers.keys()).pop(); - const router = routers.get(id); - - if (!router) { - this.error('Router not found'); - - break; - } - - try { - const dump = await router.dump(); - - this.log(`router.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`router.dump() failed: ${error}`); - } - + await this.dump('router', params, routers); break; } case 'dt': case 'dumpTransport': { - const id = params[0] || Array.from(transports.keys()).pop(); - const transport = transports.get(id); - - if (!transport) { - this.error('Transport not found'); - - break; - } - - try { - const dump = await transport.dump(); - - this.log(`transport.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`transport.dump() failed: ${error}`); - } - + await this.dump('transport', params, transports); break; } case 'dp': case 'dumpProducer': { - const id = params[0] || Array.from(producers.keys()).pop(); - const producer = producers.get(id); - - if (!producer) { - this.error('Producer not found'); - - break; - } - - try { - const dump = await producer.dump(); - - this.log(`producer.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`producer.dump() failed: ${error}`); - } - + await this.dump('producer', params, producers); break; } case 'dc': case 'dumpConsumer': { - const id = params[0] || Array.from(consumers.keys()).pop(); - const consumer = consumers.get(id); - - if (!consumer) { - this.error('Consumer not found'); - - break; - } - - try { - const dump = await consumer.dump(); - - this.log(`consumer.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`consumer.dump() failed: ${error}`); - } - + await this.dump('consumer', params, consumers); break; } case 'ddp': case 'dumpDataProducer': { - const id = params[0] || Array.from(dataProducers.keys()).pop(); - const dataProducer = dataProducers.get(id); - - if (!dataProducer) { - this.error('DataProducer not found'); - - break; - } - - try { - const dump = await dataProducer.dump(); - - this.log(`dataProducer.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`dataProducer.dump() failed: ${error}`); - } - + await this.dump('dataProducer', params, dataProducers); break; } case 'ddc': case 'dumpDataConsumer': { - const id = params[0] || Array.from(dataConsumers.keys()).pop(); - const dataConsumer = dataConsumers.get(id); - - if (!dataConsumer) { - this.error('DataConsumer not found'); - - break; - } - - try { - const dump = await dataConsumer.dump(); - - this.log(`dataConsumer.dump():\n${JSON.stringify(dump, null, ' ')}`); - } catch (error) { - this.error(`dataConsumer.dump() failed: ${error}`); - } - + await this.dump('dataConsumer', params, dataConsumers); break; } case 'st': case 'statsTransport': { - const list = params[0] ? [transports.get(params[0])] : transports.values(); - - for (const transport of list) { - if (!transport) { - this.error('Producer not found'); - break; - } - try { - const stats = await transport.getStats(); - - this.log(`transport.getStats():\n${JSON.stringify(stats, null, ' ')}`); - } catch (error) { - this.error(`transport.getStats() failed: ${error}`); - } - } - + await this.dumpStats('transport', params, transports); break; } case 'sp': case 'statsProducer': { - const list = params[0] ? [producers.get(params[0])] : producers.values(); - - for (const producer of list) { - if (!producer) { - this.error('Producer not found'); - break; - } - - try { - const stats = await producer.getStats(); - - this.log(`producer.getStats():\n${JSON.stringify(stats, null, ' ')}`); - } catch (error) { - this.error(`producer.getStats() failed: ${error}`); - } - } - + await this.dumpStats('producer', params, producers); break; } case 'sc': case 'statsConsumer': { - const list = params[0] ? [consumers.get(params[0])] : consumers.values(); - - for (const consumer of list) { - if (!consumer) { - this.error('consumerr not found'); - break; - } - - try { - const stats = await consumer.getStats(); - this.log(`consumer.getStats():\n${JSON.stringify(stats, null, ' ')}`); - } catch (error) { - this.error(`consumer.getStats() failed: ${error}`); - } - } - + await this.dumpStats('consumer', params, consumers); break; } case 'sdp': case 'statsDataProducer': { - const id = params[0] || Array.from(dataProducers.keys()).pop(); - const dataProducer = dataProducers.get(id); - - if (!dataProducer) { - this.error('DataProducer not found'); - - break; - } - - try { - const stats = await dataProducer.getStats(); - - this.log(`dataProducer.getStats():\n${JSON.stringify(stats, null, ' ')}`); - } catch (error) { - this.error(`dataProducer.getStats() failed: ${error}`); - } - + await this.dumpStats('dataProducer', params, dataProducers); break; } case 'sdc': case 'statsDataConsumer': { - const id = params[0] || Array.from(dataConsumers.keys()).pop(); - const dataConsumer = dataConsumers.get(id); - - if (!dataConsumer) { - this.error('DataConsumer not found'); - - break; - } - - try { - const stats = await dataConsumer.getStats(); - - this.log(`dataConsumer.getStats():\n${JSON.stringify(stats, null, ' ')}`); - } catch (error) { - this.error(`dataConsumer.getStats() failed: ${error}`); - } - + await this.dumpStats('dataConsumer', params, dataConsumers); break; } case 't': case 'terminal': { this._isTerminalOpen = true; cmd.close(); this.openTerminal(); return; } default: { this.error(`unknown command '${command}'`); this.log('press \'h\' or \'help\' to get the list of available commands'); } } readStdin(); }); }; readStdin(); } openTerminal() { this.log('\n[opening Node REPL Terminal...]'); this.log('here you have access to workers, routers, transports, producers, consumers, dataProducers and dataConsumers ES6 maps'); const terminal = repl.start( { input : this._socket, output : this._socket, terminal : true, prompt : 'terminal> ', useColors : true, useGlobal : true, ignoreUndefined : false }); this._isTerminalOpen = true; terminal.on('exit', () => { this.log('\n[exiting Node REPL Terminal...]'); this._isTerminalOpen = false; this.openCommandConsole(); }); } log(msg) { try { this._socket.write(`${colors.green(msg)}\n`); } catch (error) { //Do nothing } } error(msg) { try { this._socket.write(`${colors.red.bold('ERROR: ')}${colors.red(msg)}\n`); } catch (error) { //Do nothing } } } function runMediasoupObserver() { mediasoup.observer.on('newworker', (worker) => { // Store the latest worker in a global variable. global.worker = worker; workers.set(worker.pid, worker); worker.observer.on('close', () => workers.delete(worker.pid)); worker.observer.on('newrouter', (router) => { // Store the latest router in a global variable. global.router = router; routers.set(router.id, router); router.observer.on('close', () => routers.delete(router.id)); router.observer.on('newtransport', (transport) => { // Store the latest transport in a global variable. global.transport = transport; transports.set(transport.id, transport); transport.observer.on('close', () => transports.delete(transport.id)); transport.observer.on('newproducer', (producer) => { // Store the latest producer in a global variable. global.producer = producer; producers.set(producer.id, producer); producer.observer.on('close', () => producers.delete(producer.id)); }); transport.observer.on('newconsumer', (consumer) => { // Store the latest consumer in a global variable. global.consumer = consumer; consumers.set(consumer.id, consumer); consumer.observer.on('close', () => consumers.delete(consumer.id)); }); transport.observer.on('newdataproducer', (dataProducer) => { // Store the latest dataProducer in a global variable. global.dataProducer = dataProducer; dataProducers.set(dataProducer.id, dataProducer); dataProducer.observer.on('close', () => dataProducers.delete(dataProducer.id)); }); transport.observer.on('newdataconsumer', (dataConsumer) => { // Store the latest dataConsumer in a global variable. global.dataConsumer = dataConsumer; dataConsumers.set(dataConsumer.id, dataConsumer); dataConsumer.observer.on('close', () => dataConsumers.delete(dataConsumer.id)); }); }); }); }); } module.exports = async function(rooms, peers) { try { // Run the mediasoup observer API. runMediasoupObserver(); // Make maps global so they can be used during the REPL terminal. global.rooms = rooms; global.peers = peers; global.workers = workers; global.routers = routers; global.transports = transports; global.producers = producers; global.consumers = consumers; global.dataProducers = dataProducers; global.dataConsumers = dataConsumers; const server = net.createServer((socket) => { const interactive = new Interactive(socket); interactive.openCommandConsole(); }); await new Promise((resolve) => { try { fs.unlinkSync(SOCKET_PATH); } catch (error) { //Do nothing } server.listen(SOCKET_PATH, resolve); }); } catch (error) { //Do nothing } };