diff --git a/utilities/package-lock.json b/utilities/package-lock.json index 5ff7612c2..8fe161f1a 100644 --- a/utilities/package-lock.json +++ b/utilities/package-lock.json @@ -15,7 +15,8 @@ "peer": "^0.6.1", "socket.io": "^4.4.1", "source-map": "^0.7.3", - "ua-parser-js": "^1.0.2" + "ua-parser-js": "^1.0.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.6.0" } }, "node_modules/@maxmind/geoip2-node": { @@ -1287,6 +1288,10 @@ "uuid": "bin/uuid" } }, + "node_modules/uWebSockets.js": { + "version": "20.6.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#a58e810e47a23696410f6073c8c905dc38f75da5" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2350,6 +2355,10 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, + "uWebSockets.js": { + "version": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#a58e810e47a23696410f6073c8c905dc38f75da5", + "from": "uWebSockets.js@github:uNetworking/uWebSockets.js#v20.6.0" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/utilities/package.json b/utilities/package.json index 452c3fe00..79c8513a4 100644 --- a/utilities/package.json +++ b/utilities/package.json @@ -24,6 +24,7 @@ "peer": "^0.6.1", "socket.io": "^4.4.1", "source-map": "^0.7.3", - "ua-parser-js": "^1.0.2" + "ua-parser-js": "^1.0.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.6.0" } } diff --git a/utilities/server.js b/utilities/server.js index 661ef081c..cbae8f5ff 100644 --- a/utilities/server.js +++ b/utilities/server.js @@ -8,7 +8,6 @@ const HOST = '0.0.0.0'; const PORT = 9000; var app = express(); -var wsapp = express(); let debug = process.env.debug === "1" || false; const request_logger = (identity) => { return (req, res, next) => { @@ -23,20 +22,15 @@ const request_logger = (identity) => { } }; app.use(request_logger("[app]")); -wsapp.use(request_logger("[wsapp]")); app.use('/sourcemaps', sourcemapsReaderServer); app.use('/assist', peerRouter); -wsapp.use('/assist', socket.wsRouter); const server = app.listen(PORT, HOST, () => { console.log(`App listening on http://${HOST}:${PORT}`); console.log('Press Ctrl+C to quit.'); }); -const wsserver = wsapp.listen(PORT + 1, HOST, () => { - console.log(`WS App listening on http://${HOST}:${PORT + 1}`); - console.log('Press Ctrl+C to quit.'); -}); + const peerServer = ExpressPeerServer(server, { debug: true, path: '/', @@ -48,6 +42,38 @@ peerServer.on('disconnect', peerDisconnect); peerServer.on('error', peerError); app.use('/', peerServer); app.enable('trust proxy'); -wsapp.enable('trust proxy'); -socket.start(wsserver); -module.exports = {wsserver, server}; + + +const {App} = require("uWebSockets.js"); +const PREFIX = process.env.prefix || '/assist' + +const uapp = new App(); + +const healthFn = (res, req) => { + res.writeStatus('200 OK').end('ok!'); +} +uapp.get(PREFIX, healthFn); +uapp.get(`${PREFIX}/`, healthFn); + +const uWrapper = function (fn) { + return (res, req) => fn(req, res); +} +uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-list`, uWrapper(socket.handlers.socketsList)); +uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-list/:projectKey`, uWrapper(socket.handlers.socketsListByProject)); + +uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-live`, uWrapper(socket.handlers.socketsLive)); +uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-live/:projectKey`, uWrapper(socket.handlers.socketsLiveByProject)); + + +socket.start(uapp); + +uapp.listen(HOST, PORT + 1, (token) => { + if (!token) { + console.warn("port already in use"); + } + console.log(`WS App listening on http://${HOST}:${PORT + 1}`); + console.log('Press Ctrl+C to quit.'); +}); + + +module.exports = {uapp, server}; diff --git a/utilities/server_back.js b/utilities/server_back.js new file mode 100644 index 000000000..661ef081c --- /dev/null +++ b/utilities/server_back.js @@ -0,0 +1,53 @@ +var sourcemapsReaderServer = require('./servers/sourcemaps-server'); +var {peerRouter, peerConnection, peerDisconnect, peerError} = require('./servers/peerjs-server'); +var express = require('express'); +const {ExpressPeerServer} = require('peer'); +const socket = require("./servers/websocket"); + +const HOST = '0.0.0.0'; +const PORT = 9000; + +var app = express(); +var wsapp = express(); +let debug = process.env.debug === "1" || false; +const request_logger = (identity) => { + return (req, res, next) => { + debug && console.log(identity, new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl); + res.on('finish', function () { + if (this.statusCode !== 200 || debug) { + console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode); + } + }) + + next(); + } +}; +app.use(request_logger("[app]")); +wsapp.use(request_logger("[wsapp]")); + +app.use('/sourcemaps', sourcemapsReaderServer); +app.use('/assist', peerRouter); +wsapp.use('/assist', socket.wsRouter); + +const server = app.listen(PORT, HOST, () => { + console.log(`App listening on http://${HOST}:${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); +const wsserver = wsapp.listen(PORT + 1, HOST, () => { + console.log(`WS App listening on http://${HOST}:${PORT + 1}`); + console.log('Press Ctrl+C to quit.'); +}); +const peerServer = ExpressPeerServer(server, { + debug: true, + path: '/', + proxied: true, + allow_discovery: false +}); +peerServer.on('connection', peerConnection); +peerServer.on('disconnect', peerDisconnect); +peerServer.on('error', peerError); +app.use('/', peerServer); +app.enable('trust proxy'); +wsapp.enable('trust proxy'); +socket.start(wsserver); +module.exports = {wsserver, server}; diff --git a/utilities/servers/websocket.js b/utilities/servers/websocket.js index ab6a2c4d5..ddc40ff0d 100644 --- a/utilities/servers/websocket.js +++ b/utilities/servers/websocket.js @@ -1,9 +1,7 @@ const _io = require('socket.io'); -const express = require('express'); const uaParser = require('ua-parser-js'); const geoip2Reader = require('@maxmind/geoip2-node').Reader; var {extractPeerId} = require('./peerjs-server'); -var wsRouter = express.Router(); const IDENTITIES = {agent: 'agent', session: 'session'}; const NEW_AGENT = "NEW_AGENT"; const NO_AGENTS = "NO_AGENT"; @@ -11,11 +9,11 @@ const AGENT_DISCONNECT = "AGENT_DISCONNECTED"; const AGENTS_CONNECTED = "AGENTS_CONNECTED"; const NO_SESSIONS = "SESSION_DISCONNECTED"; const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED"; -// const wsReconnectionTimeout = process.env.wsReconnectionTimeout | 10 * 1000; let io; + let debug = process.env.debug === "1" || false; -wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) { +const socketsList = function (req, res) { debug && console.log("[WS]looking for all available sessions"); let liveSessions = {}; for (let peerId of io.sockets.adapter.rooms.keys()) { @@ -25,11 +23,10 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) { liveSessions[projectKey].push(sessionId); } } - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"data": liveSessions})); -}); -wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, res) { + res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions})); +} +const socketsListByProject = function (req, res) { + req.params = {projectKey: req.getParameter(0)}; debug && console.log(`[WS]looking for available sessions for ${req.params.projectKey}`); let liveSessions = {}; for (let peerId of io.sockets.adapter.rooms.keys()) { @@ -39,12 +36,9 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, r liveSessions[projectKey].push(sessionId); } } - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); -}); - -wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) { + res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); +} +const socketsLive = async function (req, res) { debug && console.log("[WS]looking for all available LIVE sessions"); let liveSessions = {}; for (let peerId of io.sockets.adapter.rooms.keys()) { @@ -59,12 +53,10 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) { } } } - - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"data": liveSessions})); -}); -wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function (req, res) { + res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions})); +} +const socketsLiveByProject = async function (req, res) { + req.params = {projectKey: req.getParameter(0)}; debug && console.log(`[WS]looking for available LIVE sessions for ${req.params.projectKey}`); let liveSessions = {}; for (let peerId of io.sockets.adapter.rooms.keys()) { @@ -79,10 +71,8 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function ( } } } - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); -}); + res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); +} const findSessionSocketId = async (io, peerId) => { const connected_sockets = await io.in(peerId).fetchSockets(); @@ -158,16 +148,17 @@ function extractSessionInfo(socket) { } module.exports = { - wsRouter, start: (server) => { - io = _io(server, { + io = new _io.Server({ maxHttpBufferSize: 1e6, cors: { origin: "*", methods: ["GET", "POST", "PUT"] }, - path: '/socket' + path: '/ws-assist/socket' }); + io.attachApp(server); + io.on('connection', async (socket) => { debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`); @@ -207,10 +198,13 @@ module.exports = { } socket.on('disconnect', async () => { + // console.log(`${socket.id} disconnected from ${socket.peerId}, waiting ${wsReconnectionTimeout / 1000}s before checking remaining`); debug && console.log(`${socket.id} disconnected from ${socket.peerId}`); if (socket.identity === IDENTITIES.agent) { socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id); } + // wait a little bit before notifying everyone + // setTimeout(async () => { debug && console.log("checking for number of connected agents and sessions"); let {c_sessions, c_agents} = await sessions_agents_count(io, socket); if (c_sessions === -1 && c_agents === -1) { @@ -224,6 +218,9 @@ module.exports = { debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`); socket.to(socket.peerId).emit(NO_AGENTS); } + + + // }, wsReconnectionTimeout); }); socket.onAny(async (eventName, ...args) => { @@ -245,28 +242,13 @@ module.exports = { }); }); - console.log("WS server started") - setInterval((io) => { - try { - let count = 0; - console.log(` ====== Rooms: ${io.sockets.adapter.rooms.size} ====== `); - const arr = Array.from(io.sockets.adapter.rooms) - const filtered = arr.filter(room => !room[1].has(room[0])) - for (let i of filtered) { - let {projectKey, sessionId} = extractPeerId(i[0]); - if (projectKey !== null && sessionId !== null) { - count++; - } - } - console.log(` ====== Valid Rooms: ${count} ====== `); - if (debug) { - for (let item of filtered) { - console.log(`Room: ${item[0]} connected: ${item[1].size}`) - } - } - } catch (e) { - console.error(e); - } - }, 20000, io); + console.log("WS server started"); + debug ? console.log("Debugging enabled.") : console.log("Debugging disabled, set debug=\"1\" to enable debugging."); + }, + handlers: { + socketsList, + socketsListByProject, + socketsLive, + socketsLiveByProject } }; \ No newline at end of file diff --git a/utilities/servers/websocket_back.js b/utilities/servers/websocket_back.js new file mode 100644 index 000000000..ab6a2c4d5 --- /dev/null +++ b/utilities/servers/websocket_back.js @@ -0,0 +1,272 @@ +const _io = require('socket.io'); +const express = require('express'); +const uaParser = require('ua-parser-js'); +const geoip2Reader = require('@maxmind/geoip2-node').Reader; +var {extractPeerId} = require('./peerjs-server'); +var wsRouter = express.Router(); +const IDENTITIES = {agent: 'agent', session: 'session'}; +const NEW_AGENT = "NEW_AGENT"; +const NO_AGENTS = "NO_AGENT"; +const AGENT_DISCONNECT = "AGENT_DISCONNECTED"; +const AGENTS_CONNECTED = "AGENTS_CONNECTED"; +const NO_SESSIONS = "SESSION_DISCONNECTED"; +const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED"; +// const wsReconnectionTimeout = process.env.wsReconnectionTimeout | 10 * 1000; + +let io; +let debug = process.env.debug === "1" || false; +wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) { + debug && console.log("[WS]looking for all available sessions"); + let liveSessions = {}; + for (let peerId of io.sockets.adapter.rooms.keys()) { + let {projectKey, sessionId} = extractPeerId(peerId); + if (projectKey !== undefined) { + liveSessions[projectKey] = liveSessions[projectKey] || []; + liveSessions[projectKey].push(sessionId); + } + } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"data": liveSessions})); +}); +wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, res) { + debug && console.log(`[WS]looking for available sessions for ${req.params.projectKey}`); + let liveSessions = {}; + for (let peerId of io.sockets.adapter.rooms.keys()) { + let {projectKey, sessionId} = extractPeerId(peerId); + if (projectKey === req.params.projectKey) { + liveSessions[projectKey] = liveSessions[projectKey] || []; + liveSessions[projectKey].push(sessionId); + } + } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); +}); + +wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) { + debug && console.log("[WS]looking for all available LIVE sessions"); + let liveSessions = {}; + for (let peerId of io.sockets.adapter.rooms.keys()) { + let {projectKey, sessionId} = extractPeerId(peerId); + if (projectKey !== undefined) { + let connected_sockets = await io.in(peerId).fetchSockets(); + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.session) { + liveSessions[projectKey] = liveSessions[projectKey] || []; + liveSessions[projectKey].push(item.handshake.query.sessionInfo); + } + } + } + } + + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"data": liveSessions})); +}); +wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function (req, res) { + debug && console.log(`[WS]looking for available LIVE sessions for ${req.params.projectKey}`); + let liveSessions = {}; + for (let peerId of io.sockets.adapter.rooms.keys()) { + let {projectKey, sessionId} = extractPeerId(peerId); + if (projectKey === req.params.projectKey) { + let connected_sockets = await io.in(peerId).fetchSockets(); + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.session) { + liveSessions[projectKey] = liveSessions[projectKey] || []; + liveSessions[projectKey].push(item.handshake.query.sessionInfo); + } + } + } + } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []})); +}); + +const findSessionSocketId = async (io, peerId) => { + const connected_sockets = await io.in(peerId).fetchSockets(); + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.session) { + return item.id; + } + } + return null; +}; + +async function sessions_agents_count(io, socket) { + let c_sessions = 0, c_agents = 0; + if (io.sockets.adapter.rooms.get(socket.peerId)) { + const connected_sockets = await io.in(socket.peerId).fetchSockets(); + + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.session) { + c_sessions++; + } else { + c_agents++; + } + } + } else { + c_agents = -1; + c_sessions = -1; + } + return {c_sessions, c_agents}; +} + +async function get_all_agents_ids(io, socket) { + let agents = []; + if (io.sockets.adapter.rooms.get(socket.peerId)) { + const connected_sockets = await io.in(socket.peerId).fetchSockets(); + for (let item of connected_sockets) { + if (item.handshake.query.identity === IDENTITIES.agent) { + agents.push(item.id); + } + } + } + return agents; +} + +function extractSessionInfo(socket) { + if (socket.handshake.query.sessionInfo !== undefined) { + debug && console.log("received headers"); + debug && console.log(socket.handshake.headers); + socket.handshake.query.sessionInfo = JSON.parse(socket.handshake.query.sessionInfo); + + let ua = uaParser(socket.handshake.headers['user-agent']); + socket.handshake.query.sessionInfo.userOs = ua.os.name || null; + socket.handshake.query.sessionInfo.userBrowser = ua.browser.name || null; + socket.handshake.query.sessionInfo.userBrowserVersion = ua.browser.version || null; + socket.handshake.query.sessionInfo.userDevice = ua.device.model || null; + socket.handshake.query.sessionInfo.userDeviceType = ua.device.type || 'desktop'; + socket.handshake.query.sessionInfo.userCountry = null; + + const options = { + // you can use options like `cache` or `watchForUpdates` + }; + // console.log("Looking for MMDB file in " + process.env.MAXMINDDB_FILE); + geoip2Reader.open(process.env.MAXMINDDB_FILE, options) + .then(reader => { + debug && console.log("looking for location of "); + debug && console.log(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address); + let country = reader.country(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address); + socket.handshake.query.sessionInfo.userCountry = country.country.isoCode; + }) + .catch(error => { + console.error(error); + }); + } +} + +module.exports = { + wsRouter, + start: (server) => { + io = _io(server, { + maxHttpBufferSize: 1e6, + cors: { + origin: "*", + methods: ["GET", "POST", "PUT"] + }, + path: '/socket' + }); + + io.on('connection', async (socket) => { + debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`); + socket.peerId = socket.handshake.query.peerId; + socket.identity = socket.handshake.query.identity; + const {projectKey, sessionId} = extractPeerId(socket.peerId); + socket.sessionId = sessionId; + socket.projectKey = projectKey; + socket.lastMessageReceivedAt = Date.now(); + let {c_sessions, c_agents} = await sessions_agents_count(io, socket); + if (socket.identity === IDENTITIES.session) { + if (c_sessions > 0) { + debug && console.log(`session already connected, refusing new connexion`); + io.to(socket.id).emit(SESSION_ALREADY_CONNECTED); + return socket.disconnect(); + } + extractSessionInfo(socket); + if (c_agents > 0) { + debug && console.log(`notifying new session about agent-existence`); + let agents_ids = await get_all_agents_ids(io, socket); + io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids); + } + + } else if (c_sessions <= 0) { + debug && console.log(`notifying new agent about no SESSIONS`); + io.to(socket.id).emit(NO_SESSIONS); + } + socket.join(socket.peerId); + if (io.sockets.adapter.rooms.get(socket.peerId)) { + debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`); + } + if (socket.identity === IDENTITIES.agent) { + if (socket.handshake.query.agentInfo !== undefined) { + socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo); + } + socket.to(socket.peerId).emit(NEW_AGENT, socket.id, socket.handshake.query.agentInfo); + } + + socket.on('disconnect', async () => { + debug && console.log(`${socket.id} disconnected from ${socket.peerId}`); + if (socket.identity === IDENTITIES.agent) { + socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id); + } + debug && console.log("checking for number of connected agents and sessions"); + let {c_sessions, c_agents} = await sessions_agents_count(io, socket); + if (c_sessions === -1 && c_agents === -1) { + debug && console.log(`room not found: ${socket.peerId}`); + } + if (c_sessions === 0) { + debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`); + socket.to(socket.peerId).emit(NO_SESSIONS); + } + if (c_agents === 0) { + debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`); + socket.to(socket.peerId).emit(NO_AGENTS); + } + }); + + socket.onAny(async (eventName, ...args) => { + socket.lastMessageReceivedAt = Date.now(); + if (socket.identity === IDENTITIES.session) { + debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}, members: ${io.sockets.adapter.rooms.get(socket.peerId).size}`); + socket.to(socket.peerId).emit(eventName, args[0]); + } else { + debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`); + let socketId = await findSessionSocketId(io, socket.peerId); + if (socketId === null) { + debug && console.log(`session not found for:${socket.peerId}`); + io.to(socket.id).emit(NO_SESSIONS); + } else { + debug && console.log("message sent"); + io.to(socketId).emit(eventName, socket.id, args[0]); + } + } + }); + + }); + console.log("WS server started") + setInterval((io) => { + try { + let count = 0; + console.log(` ====== Rooms: ${io.sockets.adapter.rooms.size} ====== `); + const arr = Array.from(io.sockets.adapter.rooms) + const filtered = arr.filter(room => !room[1].has(room[0])) + for (let i of filtered) { + let {projectKey, sessionId} = extractPeerId(i[0]); + if (projectKey !== null && sessionId !== null) { + count++; + } + } + console.log(` ====== Valid Rooms: ${count} ====== `); + if (debug) { + for (let item of filtered) { + console.log(`Room: ${item[0]} connected: ${item[1].size}`) + } + } + } catch (e) { + console.error(e); + } + }, 20000, io); + } +}; \ No newline at end of file