@colyseus/tools 0.16.0-preview.3 → 0.16.0-preview.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,19 +7,19 @@
7
7
  <a href="https://npmjs.com/package/colyseus">
8
8
  <img src="https://img.shields.io/npm/dm/colyseus.svg?style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAHdElNRQfjAgETESWYxR33AAAAtElEQVQoz4WQMQrCQBRE38Z0QoTcwF4Qg1h4BO0sxGOk80iCtViksrIQRRBTewWxMI1mbELYjYu+4rPMDPtn12ChMT3gavb4US5Jym0tcBIta3oDHv4Gwmr7nC4QAxBrCdzM2q6XqUnm9m9r59h7Rc0n2pFv24k4ttGMUXW+sGELTJjSr7QDKuqLS6UKFChVWWuFkZw9Z2AAvAirKT+JTlppIRnd6XgaP4goefI2Shj++OnjB3tBmHYK8z9zAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTAyLTAxVDE4OjE3OjM3KzAxOjAwGQQixQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wMi0wMVQxODoxNzozNyswMTowMGhZmnkAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC">
9
9
  </a>
10
- <a href="http://chat.colyseus.io">
11
- <img src="https://img.shields.io/discord/525739117951320081.svg?style=for-the-badge&colorB=7581dc&logo=discord&logoColor=white">
12
- </a>
13
10
  <a href="https://github.com/colyseus/colyseus/discussions" title="Discuss on Forum">
14
11
  <img src="https://img.shields.io/badge/discuss-on%20forum-brightgreen.svg?style=for-the-badge&colorB=0069b8&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAHdElNRQfjAgETDROxCNUzAAABB0lEQVQoz4WRvyvEARjGP193CnWRH+dHQmGwKZtFGcSmxHAL400GN95ktIpV2dzlLzDJgsGgGNRdDAzoQueS/PgY3HXHyT3T+/Y87/s89UANBKXBdoZo5J6L4K1K5ZxHfnjnlQUf3bKvkgy57a0r9hS3cXfMO1kWJMza++tj3Ac7/LY343x1NA9cNmYMwnSS/SP8JVFuSJmr44iFqvtmpjhmhBCrOOazCesq6H4P3bPBjFoIBydOk2bUA17I080Es+wSZ51B4DIA2zgjSpYcEe44Js01G0XjRcCU+y4ZMrDeLmfc9EnVd5M/o0VMeu6nJZxWJivLmhyw1WHTvrr2b4+2OFqra+ALwouTMDcqmjMAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDItMDFUMTg6MTM6MTkrMDE6MDAC9f6fAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTAyLTAxVDE4OjEzOjE5KzAxOjAwc6hGIwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAASUVORK5CYII=" alt="Discussion forum" />
15
12
  </a>
13
+ <a href="http://chat.colyseus.io">
14
+ <img src="https://img.shields.io/discord/525739117951320081.svg?style=for-the-badge&colorB=7581dc&logo=discord&logoColor=white">
15
+ </a>
16
16
  <h3>
17
17
  Multiplayer Framework for Node.js. <br /><a href="https://docs.colyseus.io/">View documentation</a>
18
18
  </h3>
19
19
  </div>
20
20
 
21
- Colyseus is an Authoritative Multiplayer Framework for Node.js, with SDKs
22
- available for the Web, Unity, Defold, Haxe, Cocos and Construct3. ([See official SDKs](https://docs.colyseus.io/client/))
21
+ Colyseus is an Authoritative Multiplayer Framework for Node.js, with clients
22
+ available for the Web, Unity3d, Defold, Haxe, and Cocos. ([See official clients](#%EF%B8%8F-official-client-integration))
23
23
 
24
24
  The project focuses on providing synchronizable data structures for realtime and
25
25
  turn-based games, matchmaking, and ease of usage both on the server-side and
package/build/index.js CHANGED
@@ -34,10 +34,11 @@ __export(src_exports, {
34
34
  module.exports = __toCommonJS(src_exports);
35
35
  var import_loadenv = require("./loadenv.js");
36
36
  var import_os = __toESM(require("os"));
37
+ var import_fs = __toESM(require("fs"));
38
+ var import_net = __toESM(require("net"));
37
39
  var import_http = __toESM(require("http"));
38
40
  var import_cors = __toESM(require("cors"));
39
41
  var import_express = __toESM(require("express"));
40
- var import_node_os_utils = __toESM(require("node-os-utils"));
41
42
  var import_core = require("@colyseus/core");
42
43
  var import_ws_transport = require("@colyseus/ws-transport");
43
44
  let BunWebSockets = void 0;
@@ -81,7 +82,9 @@ async function listen(options, port = Number(process.env.PORT || 2567)) {
81
82
  await options.beforeListen?.();
82
83
  }
83
84
  if (process.env.COLYSEUS_CLOUD !== void 0) {
84
- await gameServer.listen(`/run/colyseus/${port}.sock`);
85
+ const socketPath = `/run/colyseus/${port}.sock`;
86
+ await checkInactiveSocketFile(socketPath);
87
+ await gameServer.listen(socketPath);
85
88
  } else {
86
89
  await gameServer.listen(port);
87
90
  }
@@ -157,31 +160,22 @@ async function getTransport(options) {
157
160
  app.get("/__healthcheck", (req, res) => {
158
161
  res.status(200).end();
159
162
  });
160
- app.get("/__cloudstats", async (req, res) => {
161
- if (process.env.CLOUD_SECRET && req.headers.authorization !== process.env.CLOUD_SECRET) {
162
- res.status(401).end();
163
- return;
164
- }
165
- const rooms = (await import_core.matchMaker.stats.fetchAll()).reduce((prev, curr) => {
166
- return prev + curr.roomCount;
167
- }, 0);
168
- const ccu = await import_core.matchMaker.stats.getGlobalCCU();
169
- const mem = await import_node_os_utils.default.mem.used();
170
- const cpu = await import_node_os_utils.default.cpu.usage() / 100;
171
- res.json({
172
- version: 1,
173
- mem: mem.usedMemMb / mem.totalMemMb,
174
- cpu,
175
- ccu,
176
- rooms
177
- });
178
- });
179
163
  if (options.displayLogs) {
180
164
  import_core.logger.info("\u2705 Express initialized");
181
165
  }
182
166
  }
183
167
  return transport;
184
168
  }
169
+ function checkInactiveSocketFile(sockFilePath) {
170
+ return new Promise((resolve, reject) => {
171
+ const client = import_net.default.createConnection({ path: sockFilePath }).on("connect", () => {
172
+ client.end();
173
+ throw new Error(`EADDRINUSE: Already listening on '${sockFilePath}'`);
174
+ }).on("error", () => {
175
+ import_fs.default.unlink(sockFilePath, () => resolve(true));
176
+ });
177
+ });
178
+ }
185
179
  // Annotate the CommonJS export names for ESM import in node:
186
180
  0 && (module.exports = {
187
181
  getTransport,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import './loadenv.js';\nimport os from 'os';\nimport http from 'http';\nimport cors from 'cors';\nimport express from 'express';\nimport osUtils from 'node-os-utils';\nimport { logger, Server, ServerOptions, Transport, matchMaker } from '@colyseus/core';\nimport { WebSocketTransport } from '@colyseus/ws-transport';\n\nlet BunWebSockets: any = undefined;\n\n// @ts-ignore\nimport('@colyseus/bun-websockets')\n .then((module) => BunWebSockets = module)\n .catch(() => { });\n\nexport interface ConfigOptions {\n options?: ServerOptions,\n displayLogs?: boolean,\n getId?: () => string,\n initializeTransport?: (options: any) => Transport,\n initializeExpress?: (app: express.Express) => void,\n initializeGameServer?: (app: Server) => void,\n beforeListen?: () => void,\n}\n\nconst ALLOWED_KEYS: { [key in keyof ConfigOptions]: string } = {\n 'displayLogs': \"boolean\",\n 'options': \"object\",\n 'getId': \"function\",\n 'initializeTransport': \"function\",\n 'initializeExpress': \"function\",\n 'initializeGameServer': \"function\",\n 'beforeListen': \"function\"\n};\n\nexport default function (options: ConfigOptions) {\n for (const option in options) {\n if (!ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid option '${option}'. Allowed options are: ${Object.keys(ALLOWED_KEYS).join(\", \")}`);\n }\n if(options[option] !== undefined && typeof(options[option]) !== ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid type for ${option}: please provide a ${ALLOWED_KEYS[option]} value.`);\n }\n }\n\n return options;\n}\n\n/**\n * Listen on your development environment\n * @param options Application options\n * @param port Port number to bind Colyseus + Express\n */\nexport async function listen(\n options: ConfigOptions | Server,\n port: number = Number(process.env.PORT || 2567),\n) {\n // Force 2567 port on Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n port = 2567;\n }\n\n //\n // Handling multiple processes\n // Use NODE_APP_INSTANCE to play nicely with pm2\n //\n const processNumber = Number(process.env.NODE_APP_INSTANCE || \"0\");\n port += processNumber;\n\n let gameServer: Server;\n let displayLogs = true;\n\n if (options instanceof Server) {\n gameServer = options;\n\n } else {\n gameServer = await buildServerFromOptions(options, port);\n displayLogs = options.displayLogs;\n\n await options.initializeGameServer?.(gameServer);\n await matchMaker.onReady;\n await options.beforeListen?.();\n }\n\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // listening on socket\n // @ts-ignore\n await gameServer.listen(`/run/colyseus/${port}.sock`);\n\n } else {\n // listening on port\n await gameServer.listen(port);\n }\n\n // notify process manager (production)\n if (typeof(process.send) === \"function\") {\n process.send('ready');\n }\n\n if (displayLogs) {\n logger.info(`\u2694\uFE0F Listening on http://localhost:${port}`);\n }\n\n return gameServer;\n}\n\nasync function buildServerFromOptions(options: ConfigOptions, port: number) {\n const serverOptions = options.options || {};\n options.displayLogs = options.displayLogs ?? true;\n\n // automatically configure for production under Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // special configuration is required when using multiple processes\n const useRedisConfig = (os.cpus().length > 1) || (process.env.REDIS_URI !== undefined);\n\n if (!serverOptions.driver && useRedisConfig) {\n let RedisDriver: any = undefined;\n try {\n RedisDriver = require('@colyseus/redis-driver').RedisDriver;\n serverOptions.driver = new RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n\n if (!serverOptions.presence && useRedisConfig) {\n let RedisPresence: any = undefined;\n try {\n RedisPresence = require('@colyseus/redis-presence').RedisPresence;\n serverOptions.presence = new RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n\n if (useRedisConfig) {\n // force \"publicAddress\" when more than 1 process is available\n serverOptions.publicAddress = process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME;\n\n // nginx is responsible for forwarding /{port}/ to this process\n serverOptions.publicAddress += \"/\" + port;\n }\n }\n\n const transport = await getTransport(options);\n return new Server({\n ...serverOptions,\n transport,\n });\n}\n\nexport async function getTransport(options: ConfigOptions) {\n let transport: Transport;\n\n if (!options.initializeTransport) {\n if (BunWebSockets !== undefined) {\n // @colyseus/bun-websockets\n options.initializeTransport = (options: any) => new BunWebSockets.BunWebSockets(options);\n\n } else {\n // use WebSocketTransport by default\n options.initializeTransport = (options: any) => new WebSocketTransport(options);\n }\n }\n\n let app: express.Express | undefined = express();\n let server = http.createServer(app);\n\n transport = await options.initializeTransport({ server, app });\n\n //\n // TODO: refactor me!\n // BunWebSockets: There's no need to instantiate \"app\" and \"server\" above\n //\n if (transport['expressApp']) {\n app = transport['expressApp'];\n }\n\n if (app) {\n // Enable CORS\n app.use(cors({ origin: true, credentials: true, }));\n\n // Enable JSON parsing.\n app.use(express.json());\n\n if (options.initializeExpress) {\n await options.initializeExpress(app);\n }\n\n // health check for load balancers\n app.get(\"/__healthcheck\", (req, res) => {\n res.status(200).end();\n });\n\n app.get(\"/__cloudstats\", async (req, res) => {\n if (\n process.env.CLOUD_SECRET &&\n req.headers.authorization !== process.env.CLOUD_SECRET\n ) {\n res.status(401).end();\n return;\n }\n\n // count rooms per process\n const rooms = (await matchMaker.stats.fetchAll()).reduce((prev, curr) => {\n return prev + curr.roomCount;\n }, 0);\n\n const ccu = await matchMaker.stats.getGlobalCCU();\n const mem = await osUtils.mem.used();\n const cpu = (await osUtils.cpu.usage()) / 100;\n\n res.json({\n version: 1,\n mem: (mem.usedMemMb / mem.totalMemMb),\n cpu,\n ccu,\n rooms,\n });\n });\n\n if (options.displayLogs) {\n logger.info(\"\u2705 Express initialized\");\n }\n }\n\n return transport;\n}\n"],
5
- "mappings": "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",
6
- "names": ["module", "os", "options", "express", "http", "cors", "osUtils"]
4
+ "sourcesContent": ["import './loadenv.js';\nimport os from 'os';\nimport fs from \"fs\";\nimport net from \"net\";\nimport http from 'http';\nimport cors from 'cors';\nimport express from 'express';\nimport { logger, Server, ServerOptions, Transport, matchMaker } from '@colyseus/core';\nimport { WebSocketTransport } from '@colyseus/ws-transport';\n\nlet BunWebSockets: any = undefined;\n\n// @ts-ignore\nimport('@colyseus/bun-websockets')\n .then((module) => BunWebSockets = module)\n .catch(() => { });\n\nexport interface ConfigOptions {\n options?: ServerOptions,\n displayLogs?: boolean,\n getId?: () => string,\n initializeTransport?: (options: any) => Transport,\n initializeExpress?: (app: express.Express) => void,\n initializeGameServer?: (app: Server) => void,\n beforeListen?: () => void,\n}\n\nconst ALLOWED_KEYS: { [key in keyof ConfigOptions]: string } = {\n 'displayLogs': \"boolean\",\n 'options': \"object\",\n 'getId': \"function\",\n 'initializeTransport': \"function\",\n 'initializeExpress': \"function\",\n 'initializeGameServer': \"function\",\n 'beforeListen': \"function\"\n};\n\nexport default function (options: ConfigOptions) {\n for (const option in options) {\n if (!ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid option '${option}'. Allowed options are: ${Object.keys(ALLOWED_KEYS).join(\", \")}`);\n }\n if(options[option] !== undefined && typeof(options[option]) !== ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid type for ${option}: please provide a ${ALLOWED_KEYS[option]} value.`);\n }\n }\n\n return options;\n}\n\n/**\n * Listen on your development environment\n * @param options Application options\n * @param port Port number to bind Colyseus + Express\n */\nexport async function listen(\n options: ConfigOptions | Server,\n port: number = Number(process.env.PORT || 2567),\n) {\n // Force 2567 port on Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n port = 2567;\n }\n\n //\n // Handling multiple processes\n // Use NODE_APP_INSTANCE to play nicely with pm2\n //\n const processNumber = Number(process.env.NODE_APP_INSTANCE || \"0\");\n port += processNumber;\n\n let gameServer: Server;\n let displayLogs = true;\n\n if (options instanceof Server) {\n gameServer = options;\n\n } else {\n gameServer = await buildServerFromOptions(options, port);\n displayLogs = options.displayLogs;\n\n await options.initializeGameServer?.(gameServer);\n await matchMaker.onReady;\n await options.beforeListen?.();\n }\n\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // listening on socket\n const socketPath: any = `/run/colyseus/${port}.sock`;\n\n // check if .sock file is active\n // (fixes \"ADDRINUSE\" issue when restarting the server)\n await checkInactiveSocketFile(socketPath);\n\n await gameServer.listen(socketPath);\n\n } else {\n // listening on port\n await gameServer.listen(port);\n }\n\n // notify process manager (production)\n if (typeof(process.send) === \"function\") {\n process.send('ready');\n }\n\n if (displayLogs) {\n logger.info(`\u2694\uFE0F Listening on http://localhost:${port}`);\n }\n\n return gameServer;\n}\n\nasync function buildServerFromOptions(options: ConfigOptions, port: number) {\n const serverOptions = options.options || {};\n options.displayLogs = options.displayLogs ?? true;\n\n // automatically configure for production under Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n\n // special configuration is required when using multiple processes\n const useRedisConfig = (os.cpus().length > 1) || (process.env.REDIS_URI !== undefined);\n\n if (!serverOptions.driver && useRedisConfig) {\n let RedisDriver: any = undefined;\n try {\n RedisDriver = require('@colyseus/redis-driver').RedisDriver;\n serverOptions.driver = new RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n\n if (!serverOptions.presence && useRedisConfig) {\n let RedisPresence: any = undefined;\n try {\n RedisPresence = require('@colyseus/redis-presence').RedisPresence;\n serverOptions.presence = new RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n\n if (useRedisConfig) {\n // force \"publicAddress\" when more than 1 process is available\n serverOptions.publicAddress = process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME;\n\n // nginx is responsible for forwarding /{port}/ to this process\n serverOptions.publicAddress += \"/\" + port;\n }\n }\n\n const transport = await getTransport(options);\n return new Server({\n ...serverOptions,\n transport,\n });\n}\n\nexport async function getTransport(options: ConfigOptions) {\n let transport: Transport;\n\n if (!options.initializeTransport) {\n if (BunWebSockets !== undefined) {\n // @colyseus/bun-websockets\n options.initializeTransport = (options: any) => new BunWebSockets.BunWebSockets(options);\n\n } else {\n // use WebSocketTransport by default\n options.initializeTransport = (options: any) => new WebSocketTransport(options);\n }\n }\n\n let app: express.Express | undefined = express();\n let server = http.createServer(app);\n\n transport = await options.initializeTransport({ server, app });\n\n //\n // TODO: refactor me!\n // BunWebSockets: There's no need to instantiate \"app\" and \"server\" above\n //\n if (transport['expressApp']) {\n app = transport['expressApp'];\n }\n\n if (app) {\n // Enable CORS\n app.use(cors({ origin: true, credentials: true, }));\n\n // Enable JSON parsing.\n app.use(express.json());\n\n if (options.initializeExpress) {\n await options.initializeExpress(app);\n }\n\n // health check for load balancers\n app.get(\"/__healthcheck\", (req, res) => {\n res.status(200).end();\n });\n\n if (options.displayLogs) {\n logger.info(\"\u2705 Express initialized\");\n }\n }\n\n return transport;\n}\n\n/**\n * Check if a socket file is active and remove it if it's not.\n */\nfunction checkInactiveSocketFile(sockFilePath: string) {\n return new Promise((resolve, reject) => {\n const client = net.createConnection({ path: sockFilePath })\n .on('connect', () => {\n // socket file is active, close the connection\n client.end();\n throw new Error(`EADDRINUSE: Already listening on '${sockFilePath}'`);\n })\n .on('error', () => {\n // socket file is inactive, remove it\n fs.unlink(sockFilePath, () => resolve(true));\n });\n });\n}"],
5
+ "mappings": "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",
6
+ "names": ["module", "os", "options", "express", "http", "cors", "net", "fs"]
7
7
  }
package/build/index.mjs CHANGED
@@ -8,10 +8,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // packages/tools/src/index.ts
9
9
  import "./loadenv.mjs";
10
10
  import os from "os";
11
+ import fs from "fs";
12
+ import net from "net";
11
13
  import http from "http";
12
14
  import cors from "cors";
13
15
  import express from "express";
14
- import osUtils from "node-os-utils";
15
16
  import { logger, Server, matchMaker } from "@colyseus/core";
16
17
  import { WebSocketTransport } from "@colyseus/ws-transport";
17
18
  var BunWebSockets = void 0;
@@ -55,7 +56,9 @@ async function listen(options, port = Number(process.env.PORT || 2567)) {
55
56
  await options.beforeListen?.();
56
57
  }
57
58
  if (process.env.COLYSEUS_CLOUD !== void 0) {
58
- await gameServer.listen(`/run/colyseus/${port}.sock`);
59
+ const socketPath = `/run/colyseus/${port}.sock`;
60
+ await checkInactiveSocketFile(socketPath);
61
+ await gameServer.listen(socketPath);
59
62
  } else {
60
63
  await gameServer.listen(port);
61
64
  }
@@ -131,31 +134,22 @@ async function getTransport(options) {
131
134
  app.get("/__healthcheck", (req, res) => {
132
135
  res.status(200).end();
133
136
  });
134
- app.get("/__cloudstats", async (req, res) => {
135
- if (process.env.CLOUD_SECRET && req.headers.authorization !== process.env.CLOUD_SECRET) {
136
- res.status(401).end();
137
- return;
138
- }
139
- const rooms = (await matchMaker.stats.fetchAll()).reduce((prev, curr) => {
140
- return prev + curr.roomCount;
141
- }, 0);
142
- const ccu = await matchMaker.stats.getGlobalCCU();
143
- const mem = await osUtils.mem.used();
144
- const cpu = await osUtils.cpu.usage() / 100;
145
- res.json({
146
- version: 1,
147
- mem: mem.usedMemMb / mem.totalMemMb,
148
- cpu,
149
- ccu,
150
- rooms
151
- });
152
- });
153
137
  if (options.displayLogs) {
154
138
  logger.info("\u2705 Express initialized");
155
139
  }
156
140
  }
157
141
  return transport;
158
142
  }
143
+ function checkInactiveSocketFile(sockFilePath) {
144
+ return new Promise((resolve, reject) => {
145
+ const client = net.createConnection({ path: sockFilePath }).on("connect", () => {
146
+ client.end();
147
+ throw new Error(`EADDRINUSE: Already listening on '${sockFilePath}'`);
148
+ }).on("error", () => {
149
+ fs.unlink(sockFilePath, () => resolve(true));
150
+ });
151
+ });
152
+ }
159
153
  export {
160
154
  src_default as default,
161
155
  getTransport,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import './loadenv.js';\nimport os from 'os';\nimport http from 'http';\nimport cors from 'cors';\nimport express from 'express';\nimport osUtils from 'node-os-utils';\nimport { logger, Server, ServerOptions, Transport, matchMaker } from '@colyseus/core';\nimport { WebSocketTransport } from '@colyseus/ws-transport';\n\nlet BunWebSockets: any = undefined;\n\n// @ts-ignore\nimport('@colyseus/bun-websockets')\n .then((module) => BunWebSockets = module)\n .catch(() => { });\n\nexport interface ConfigOptions {\n options?: ServerOptions,\n displayLogs?: boolean,\n getId?: () => string,\n initializeTransport?: (options: any) => Transport,\n initializeExpress?: (app: express.Express) => void,\n initializeGameServer?: (app: Server) => void,\n beforeListen?: () => void,\n}\n\nconst ALLOWED_KEYS: { [key in keyof ConfigOptions]: string } = {\n 'displayLogs': \"boolean\",\n 'options': \"object\",\n 'getId': \"function\",\n 'initializeTransport': \"function\",\n 'initializeExpress': \"function\",\n 'initializeGameServer': \"function\",\n 'beforeListen': \"function\"\n};\n\nexport default function (options: ConfigOptions) {\n for (const option in options) {\n if (!ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid option '${option}'. Allowed options are: ${Object.keys(ALLOWED_KEYS).join(\", \")}`);\n }\n if(options[option] !== undefined && typeof(options[option]) !== ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid type for ${option}: please provide a ${ALLOWED_KEYS[option]} value.`);\n }\n }\n\n return options;\n}\n\n/**\n * Listen on your development environment\n * @param options Application options\n * @param port Port number to bind Colyseus + Express\n */\nexport async function listen(\n options: ConfigOptions | Server,\n port: number = Number(process.env.PORT || 2567),\n) {\n // Force 2567 port on Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n port = 2567;\n }\n\n //\n // Handling multiple processes\n // Use NODE_APP_INSTANCE to play nicely with pm2\n //\n const processNumber = Number(process.env.NODE_APP_INSTANCE || \"0\");\n port += processNumber;\n\n let gameServer: Server;\n let displayLogs = true;\n\n if (options instanceof Server) {\n gameServer = options;\n\n } else {\n gameServer = await buildServerFromOptions(options, port);\n displayLogs = options.displayLogs;\n\n await options.initializeGameServer?.(gameServer);\n await matchMaker.onReady;\n await options.beforeListen?.();\n }\n\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // listening on socket\n // @ts-ignore\n await gameServer.listen(`/run/colyseus/${port}.sock`);\n\n } else {\n // listening on port\n await gameServer.listen(port);\n }\n\n // notify process manager (production)\n if (typeof(process.send) === \"function\") {\n process.send('ready');\n }\n\n if (displayLogs) {\n logger.info(`\u2694\uFE0F Listening on http://localhost:${port}`);\n }\n\n return gameServer;\n}\n\nasync function buildServerFromOptions(options: ConfigOptions, port: number) {\n const serverOptions = options.options || {};\n options.displayLogs = options.displayLogs ?? true;\n\n // automatically configure for production under Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // special configuration is required when using multiple processes\n const useRedisConfig = (os.cpus().length > 1) || (process.env.REDIS_URI !== undefined);\n\n if (!serverOptions.driver && useRedisConfig) {\n let RedisDriver: any = undefined;\n try {\n RedisDriver = require('@colyseus/redis-driver').RedisDriver;\n serverOptions.driver = new RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n\n if (!serverOptions.presence && useRedisConfig) {\n let RedisPresence: any = undefined;\n try {\n RedisPresence = require('@colyseus/redis-presence').RedisPresence;\n serverOptions.presence = new RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n\n if (useRedisConfig) {\n // force \"publicAddress\" when more than 1 process is available\n serverOptions.publicAddress = process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME;\n\n // nginx is responsible for forwarding /{port}/ to this process\n serverOptions.publicAddress += \"/\" + port;\n }\n }\n\n const transport = await getTransport(options);\n return new Server({\n ...serverOptions,\n transport,\n });\n}\n\nexport async function getTransport(options: ConfigOptions) {\n let transport: Transport;\n\n if (!options.initializeTransport) {\n if (BunWebSockets !== undefined) {\n // @colyseus/bun-websockets\n options.initializeTransport = (options: any) => new BunWebSockets.BunWebSockets(options);\n\n } else {\n // use WebSocketTransport by default\n options.initializeTransport = (options: any) => new WebSocketTransport(options);\n }\n }\n\n let app: express.Express | undefined = express();\n let server = http.createServer(app);\n\n transport = await options.initializeTransport({ server, app });\n\n //\n // TODO: refactor me!\n // BunWebSockets: There's no need to instantiate \"app\" and \"server\" above\n //\n if (transport['expressApp']) {\n app = transport['expressApp'];\n }\n\n if (app) {\n // Enable CORS\n app.use(cors({ origin: true, credentials: true, }));\n\n // Enable JSON parsing.\n app.use(express.json());\n\n if (options.initializeExpress) {\n await options.initializeExpress(app);\n }\n\n // health check for load balancers\n app.get(\"/__healthcheck\", (req, res) => {\n res.status(200).end();\n });\n\n app.get(\"/__cloudstats\", async (req, res) => {\n if (\n process.env.CLOUD_SECRET &&\n req.headers.authorization !== process.env.CLOUD_SECRET\n ) {\n res.status(401).end();\n return;\n }\n\n // count rooms per process\n const rooms = (await matchMaker.stats.fetchAll()).reduce((prev, curr) => {\n return prev + curr.roomCount;\n }, 0);\n\n const ccu = await matchMaker.stats.getGlobalCCU();\n const mem = await osUtils.mem.used();\n const cpu = (await osUtils.cpu.usage()) / 100;\n\n res.json({\n version: 1,\n mem: (mem.usedMemMb / mem.totalMemMb),\n cpu,\n ccu,\n rooms,\n });\n });\n\n if (options.displayLogs) {\n logger.info(\"\u2705 Express initialized\");\n }\n }\n\n return transport;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import './loadenv.js';\nimport os from 'os';\nimport fs from \"fs\";\nimport net from \"net\";\nimport http from 'http';\nimport cors from 'cors';\nimport express from 'express';\nimport { logger, Server, ServerOptions, Transport, matchMaker } from '@colyseus/core';\nimport { WebSocketTransport } from '@colyseus/ws-transport';\n\nlet BunWebSockets: any = undefined;\n\n// @ts-ignore\nimport('@colyseus/bun-websockets')\n .then((module) => BunWebSockets = module)\n .catch(() => { });\n\nexport interface ConfigOptions {\n options?: ServerOptions,\n displayLogs?: boolean,\n getId?: () => string,\n initializeTransport?: (options: any) => Transport,\n initializeExpress?: (app: express.Express) => void,\n initializeGameServer?: (app: Server) => void,\n beforeListen?: () => void,\n}\n\nconst ALLOWED_KEYS: { [key in keyof ConfigOptions]: string } = {\n 'displayLogs': \"boolean\",\n 'options': \"object\",\n 'getId': \"function\",\n 'initializeTransport': \"function\",\n 'initializeExpress': \"function\",\n 'initializeGameServer': \"function\",\n 'beforeListen': \"function\"\n};\n\nexport default function (options: ConfigOptions) {\n for (const option in options) {\n if (!ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid option '${option}'. Allowed options are: ${Object.keys(ALLOWED_KEYS).join(\", \")}`);\n }\n if(options[option] !== undefined && typeof(options[option]) !== ALLOWED_KEYS[option]) {\n throw new Error(`\u274C Invalid type for ${option}: please provide a ${ALLOWED_KEYS[option]} value.`);\n }\n }\n\n return options;\n}\n\n/**\n * Listen on your development environment\n * @param options Application options\n * @param port Port number to bind Colyseus + Express\n */\nexport async function listen(\n options: ConfigOptions | Server,\n port: number = Number(process.env.PORT || 2567),\n) {\n // Force 2567 port on Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n port = 2567;\n }\n\n //\n // Handling multiple processes\n // Use NODE_APP_INSTANCE to play nicely with pm2\n //\n const processNumber = Number(process.env.NODE_APP_INSTANCE || \"0\");\n port += processNumber;\n\n let gameServer: Server;\n let displayLogs = true;\n\n if (options instanceof Server) {\n gameServer = options;\n\n } else {\n gameServer = await buildServerFromOptions(options, port);\n displayLogs = options.displayLogs;\n\n await options.initializeGameServer?.(gameServer);\n await matchMaker.onReady;\n await options.beforeListen?.();\n }\n\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n // listening on socket\n const socketPath: any = `/run/colyseus/${port}.sock`;\n\n // check if .sock file is active\n // (fixes \"ADDRINUSE\" issue when restarting the server)\n await checkInactiveSocketFile(socketPath);\n\n await gameServer.listen(socketPath);\n\n } else {\n // listening on port\n await gameServer.listen(port);\n }\n\n // notify process manager (production)\n if (typeof(process.send) === \"function\") {\n process.send('ready');\n }\n\n if (displayLogs) {\n logger.info(`\u2694\uFE0F Listening on http://localhost:${port}`);\n }\n\n return gameServer;\n}\n\nasync function buildServerFromOptions(options: ConfigOptions, port: number) {\n const serverOptions = options.options || {};\n options.displayLogs = options.displayLogs ?? true;\n\n // automatically configure for production under Colyseus Cloud\n if (process.env.COLYSEUS_CLOUD !== undefined) {\n\n // special configuration is required when using multiple processes\n const useRedisConfig = (os.cpus().length > 1) || (process.env.REDIS_URI !== undefined);\n\n if (!serverOptions.driver && useRedisConfig) {\n let RedisDriver: any = undefined;\n try {\n RedisDriver = require('@colyseus/redis-driver').RedisDriver;\n serverOptions.driver = new RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n\n if (!serverOptions.presence && useRedisConfig) {\n let RedisPresence: any = undefined;\n try {\n RedisPresence = require('@colyseus/redis-presence').RedisPresence;\n serverOptions.presence = new RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n\n if (useRedisConfig) {\n // force \"publicAddress\" when more than 1 process is available\n serverOptions.publicAddress = process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME;\n\n // nginx is responsible for forwarding /{port}/ to this process\n serverOptions.publicAddress += \"/\" + port;\n }\n }\n\n const transport = await getTransport(options);\n return new Server({\n ...serverOptions,\n transport,\n });\n}\n\nexport async function getTransport(options: ConfigOptions) {\n let transport: Transport;\n\n if (!options.initializeTransport) {\n if (BunWebSockets !== undefined) {\n // @colyseus/bun-websockets\n options.initializeTransport = (options: any) => new BunWebSockets.BunWebSockets(options);\n\n } else {\n // use WebSocketTransport by default\n options.initializeTransport = (options: any) => new WebSocketTransport(options);\n }\n }\n\n let app: express.Express | undefined = express();\n let server = http.createServer(app);\n\n transport = await options.initializeTransport({ server, app });\n\n //\n // TODO: refactor me!\n // BunWebSockets: There's no need to instantiate \"app\" and \"server\" above\n //\n if (transport['expressApp']) {\n app = transport['expressApp'];\n }\n\n if (app) {\n // Enable CORS\n app.use(cors({ origin: true, credentials: true, }));\n\n // Enable JSON parsing.\n app.use(express.json());\n\n if (options.initializeExpress) {\n await options.initializeExpress(app);\n }\n\n // health check for load balancers\n app.get(\"/__healthcheck\", (req, res) => {\n res.status(200).end();\n });\n\n if (options.displayLogs) {\n logger.info(\"\u2705 Express initialized\");\n }\n }\n\n return transport;\n}\n\n/**\n * Check if a socket file is active and remove it if it's not.\n */\nfunction checkInactiveSocketFile(sockFilePath: string) {\n return new Promise((resolve, reject) => {\n const client = net.createConnection({ path: sockFilePath })\n .on('connect', () => {\n // socket file is active, close the connection\n client.end();\n throw new Error(`EADDRINUSE: Already listening on '${sockFilePath}'`);\n })\n .on('error', () => {\n // socket file is inactive, remove it\n fs.unlink(sockFilePath, () => resolve(true));\n });\n });\n}"],
5
+ "mappings": ";;;;;;;;AAAA,OAAO;AACP,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,SAAS;AAChB,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,OAAO,aAAa;AACpB,SAAS,QAAQ,QAAkC,kBAAkB;AACrE,SAAS,0BAA0B;AAEnC,IAAI,gBAAqB;AAGzB,OAAO,0BAA0B,EAC9B,KAAK,CAAC,WAAW,gBAAgB,MAAM,EACvC,MAAM,MAAM;AAAE,CAAC;AAYlB,IAAM,eAAyD;AAAA,EAC7D,eAAe;AAAA,EACf,WAAW;AAAA,EACX,SAAS;AAAA,EACT,uBAAuB;AAAA,EACvB,qBAAqB;AAAA,EACrB,wBAAwB;AAAA,EACxB,gBAAgB;AAClB;AAEe,SAAR,YAAkB,SAAwB;AAC/C,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,aAAa,MAAM,GAAG;AACzB,YAAM,IAAI,MAAM,0BAAqB,MAAM,2BAA2B,OAAO,KAAK,YAAY,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,IAC9G;AACA,QAAG,QAAQ,MAAM,MAAM,UAAa,OAAO,QAAQ,MAAM,MAAO,aAAa,MAAM,GAAG;AACpF,YAAM,IAAI,MAAM,2BAAsB,MAAM,sBAAsB,aAAa,MAAM,CAAC,SAAS;AAAA,IACjG;AAAA,EACF;AAEA,SAAO;AACT;AAOA,eAAsB,OAClB,SACA,OAAe,OAAO,QAAQ,IAAI,QAAQ,IAAI,GAChD;AAEE,MAAI,QAAQ,IAAI,mBAAmB,QAAW;AAC1C,WAAO;AAAA,EACX;AAMA,QAAM,gBAAgB,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AACjE,UAAQ;AAER,MAAI;AACJ,MAAI,cAAc;AAElB,MAAI,mBAAmB,QAAQ;AAC3B,iBAAa;AAAA,EAEjB,OAAO;AACH,iBAAa,MAAM,uBAAuB,SAAS,IAAI;AACvD,kBAAc,QAAQ;AAEtB,UAAM,QAAQ,uBAAuB,UAAU;AAC/C,UAAM,WAAW;AACjB,UAAM,QAAQ,eAAe;AAAA,EACjC;AAEA,MAAI,QAAQ,IAAI,mBAAmB,QAAW;AAE1C,UAAM,aAAkB,iBAAiB,IAAI;AAI7C,UAAM,wBAAwB,UAAU;AAExC,UAAM,WAAW,OAAO,UAAU;AAAA,EAEtC,OAAO;AAEH,UAAM,WAAW,OAAO,IAAI;AAAA,EAChC;AAGA,MAAI,OAAO,QAAQ,SAAU,YAAY;AACrC,YAAQ,KAAK,OAAO;AAAA,EACxB;AAEA,MAAI,aAAa;AACb,WAAO,KAAK,+CAAqC,IAAI,EAAE;AAAA,EAC3D;AAEA,SAAO;AACX;AAEA,eAAe,uBAAuB,SAAwB,MAAc;AAC1E,QAAM,gBAAgB,QAAQ,WAAW,CAAC;AAC1C,UAAQ,cAAc,QAAQ,eAAe;AAG7C,MAAI,QAAQ,IAAI,mBAAmB,QAAW;AAG5C,UAAM,iBAAkB,GAAG,KAAK,EAAE,SAAS,KAAO,QAAQ,IAAI,cAAc;AAE5E,QAAI,CAAC,cAAc,UAAU,gBAAgB;AAC3C,UAAI,cAAmB;AACvB,UAAI;AACF,sBAAc,UAAQ,wBAAwB,EAAE;AAChD,sBAAc,SAAS,IAAI,YAAY,QAAQ,IAAI,SAAS;AAAA,MAC9D,SAAS,GAAG;AACV,eAAO,KAAK,EAAE;AACd,eAAO,KAAK,0CAAqC;AACjD,eAAO,KAAK,qDAA8C;AAC1D,eAAO,KAAK,EAAE;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,YAAY,gBAAgB;AAC7C,UAAI,gBAAqB;AACzB,UAAI;AACF,wBAAgB,UAAQ,0BAA0B,EAAE;AACpD,sBAAc,WAAW,IAAI,cAAc,QAAQ,IAAI,SAAS;AAAA,MAClE,SAAS,GAAG;AACV,eAAO,KAAK,EAAE;AACd,eAAO,KAAK,4CAAuC;AACnD,eAAO,KAAK,uDAAgD;AAC5D,eAAO,KAAK,EAAE;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,gBAAgB;AAElB,oBAAc,gBAAgB,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI;AAGxE,oBAAc,iBAAiB,MAAM;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,aAAa,OAAO;AAC5C,SAAO,IAAI,OAAO;AAAA,IAChB,GAAG;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAAa,SAAwB;AACvD,MAAI;AAEJ,MAAI,CAAC,QAAQ,qBAAqB;AAC9B,QAAI,kBAAkB,QAAW;AAE/B,cAAQ,sBAAsB,CAACA,aAAiB,IAAI,cAAc,cAAcA,QAAO;AAAA,IAEzF,OAAO;AAEL,cAAQ,sBAAsB,CAACA,aAAiB,IAAI,mBAAmBA,QAAO;AAAA,IAChF;AAAA,EACJ;AAEA,MAAI,MAAmC,QAAQ;AAC/C,MAAI,SAAS,KAAK,aAAa,GAAG;AAElC,cAAY,MAAM,QAAQ,oBAAoB,EAAE,QAAQ,IAAI,CAAC;AAM7D,MAAI,UAAU,YAAY,GAAG;AAC3B,UAAM,UAAU,YAAY;AAAA,EAC9B;AAEA,MAAI,KAAK;AAEP,QAAI,IAAI,KAAK,EAAE,QAAQ,MAAM,aAAa,KAAM,CAAC,CAAC;AAGlD,QAAI,IAAI,QAAQ,KAAK,CAAC;AAEtB,QAAI,QAAQ,mBAAmB;AAC3B,YAAM,QAAQ,kBAAkB,GAAG;AAAA,IACvC;AAGA,QAAI,IAAI,kBAAkB,CAAC,KAAK,QAAQ;AACtC,UAAI,OAAO,GAAG,EAAE,IAAI;AAAA,IACtB,CAAC;AAED,QAAI,QAAQ,aAAa;AACrB,aAAO,KAAK,4BAAuB;AAAA,IACvC;AAAA,EACF;AAEA,SAAO;AACX;AAKA,SAAS,wBAAwB,cAAsB;AACrD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,IAAI,iBAAiB,EAAE,MAAM,aAAa,CAAC,EACvD,GAAG,WAAW,MAAM;AAEnB,aAAO,IAAI;AACX,YAAM,IAAI,MAAM,qCAAqC,YAAY,GAAG;AAAA,IACtE,CAAC,EACA,GAAG,SAAS,MAAM;AAEjB,SAAG,OAAO,cAAc,MAAM,QAAQ,IAAI,CAAC;AAAA,IAC7C,CAAC;AAAA,EACL,CAAC;AACH;",
6
6
  "names": ["options"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/tools",
3
- "version": "0.16.0-preview.3",
3
+ "version": "0.16.0-preview.4",
4
4
  "description": "Simplify the development and production settings for your Colyseus project.",
5
5
  "input": "./src/index.ts",
6
6
  "main": "./build/index.js",
@@ -14,7 +14,8 @@
14
14
  },
15
15
  "bin": {
16
16
  "colyseus-system-boot": "system-boot.js",
17
- "colyseus-post-deploy": "post-deploy.js"
17
+ "colyseus-post-deploy": "post-deploy.js",
18
+ "colyseus-report-stats": "report-stats.js"
18
19
  },
19
20
  "repository": {
20
21
  "type": "git",
@@ -31,29 +32,43 @@
31
32
  "url": "https://github.com/colyseus/colyseus/issues"
32
33
  },
33
34
  "files": [
35
+ "pm2",
34
36
  "html",
35
37
  "build",
36
38
  "LICENSE",
37
39
  "README.md"
38
40
  ],
39
- "homepage": "https:/colyseus.io",
41
+ "homepage": "https://colyseus.io",
42
+ "dependencies": {
43
+ "@pm2/io": "^6.0.1",
44
+ "cors": "^2.8.5",
45
+ "dotenv": "^8.2.0",
46
+ "express": "^4.16.2"
47
+ },
40
48
  "devDependencies": {
41
49
  "@types/cors": "^2.8.10",
42
50
  "@types/dotenv": "^8.2.0",
43
- "uwebsockets-express": "^1.1.10"
51
+ "uwebsockets-express": "^1.1.10",
52
+ "@colyseus/core": "^0.16.0-preview.36",
53
+ "@colyseus/ws-transport": "^0.16.0-preview.8",
54
+ "@colyseus/uwebsockets-transport": "^0.16.0-preview.13"
44
55
  },
45
- "dependencies": {
46
- "node-os-utils": "^1.3.7",
47
- "cors": "^2.8.5",
48
- "dotenv": "^8.2.0",
49
- "express": "^4.16.2",
50
- "@colyseus/core": "^0.16.0-preview.30",
51
- "@colyseus/ws-transport": "^0.16.0-preview.7"
56
+ "peerDependencies": {
57
+ "@colyseus/core": "0.15.x",
58
+ "@colyseus/ws-transport": "0.15.x"
52
59
  },
53
60
  "publishConfig": {
54
61
  "access": "public"
55
62
  },
63
+ "apps": [
64
+ {
65
+ "merge_logs": true,
66
+ "max_memory_restart": "256M",
67
+ "script": "./pm2/post-deploy-agent.js",
68
+ "instance_var": "INSTANCE_ID"
69
+ }
70
+ ],
56
71
  "scripts": {
57
- "start": "ts-node-dev example/app.ts"
72
+ "start": "tsx example/app.ts"
58
73
  }
59
74
  }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * PM2 Agent for no downtime deployments on Colyseus Cloud.
3
+ *
4
+ * How it works:
5
+ * - New process(es) are spawned (MAX_ACTIVE_PROCESSES/2)
6
+ * - NGINX configuration is updated so new traffic only goes through the new process
7
+ * - Old processes are asynchronously and gracefully stopped.
8
+ * - The rest of the processes are spawned/reactivated.
9
+ */
10
+ const pm2 = require('pm2');
11
+ const fs = require('fs');
12
+ const cst = require('pm2/constants');
13
+ const io = require('@pm2/io');
14
+ const path = require('path');
15
+ const shared = require('./shared');
16
+
17
+ const opts = { env: process.env.NODE_ENV || "production", };
18
+ let config = undefined;
19
+
20
+ io.initModule({
21
+ pid: path.resolve('/var/run/colyseus-agent.pid'),
22
+ widget: {
23
+ type: 'generic',
24
+ logo: 'https://colyseus.io/images/logos/logo-dark-color.png',
25
+ theme : ['#9F1414', '#591313', 'white', 'white'],
26
+ }
27
+ });
28
+
29
+ pm2.connect(function(err) {
30
+ if (err) {
31
+ console.error(err.stack || err);
32
+ process.exit();
33
+ }
34
+ console.log('PM2 post-deploy agent is up and running...');
35
+
36
+ /**
37
+ * Remote actions
38
+ */
39
+ io.action('post-deploy', async function (arg0, reply) {
40
+ const [cwd, ecosystemFilePath] = arg0.split(':');
41
+ console.log("Received 'post-deploy' action!", { cwd, config: ecosystemFilePath });
42
+
43
+ let replied = false;
44
+
45
+ //
46
+ // Override 'reply' to decrement amount of concurrent deployments
47
+ //
48
+ const onReply = function() {
49
+ if (replied) { return; }
50
+ replied = true;
51
+ reply.apply(null, arguments);
52
+ }
53
+
54
+ try {
55
+ config = await shared.getAppConfig(ecosystemFilePath);
56
+ opts.cwd = cwd;
57
+ postDeploy(cwd, onReply);
58
+
59
+ } catch (err) {
60
+ onReply({ success: false, message: err?.message });
61
+ }
62
+ });
63
+ });
64
+
65
+ const restartingAppIds = new Set();
66
+
67
+ function postDeploy(cwd, reply) {
68
+ shared.listApps(function(err, apps) {
69
+ if (err) {
70
+ console.error(err);
71
+ return reply({ success: false, message: err?.message });
72
+ }
73
+
74
+ // first deploy, start all processes
75
+ if (apps.length === 0) {
76
+ return pm2.start(config, {...opts}, (err, result) => {
77
+ reply({ success: !err, message: err?.message });
78
+ updateAndSave(err, result);
79
+ });
80
+ }
81
+
82
+ //
83
+ // detect if cwd has changed, and restart PM2 if it has
84
+ //
85
+ if (apps[0].pm2_env.pm_cwd !== cwd) {
86
+ console.log("App Root Directory changed. Restarting may take a bit longer...");
87
+
88
+ //
89
+ // remove all and start again with new cwd
90
+ //
91
+ return pm2.delete('all', function (err) {
92
+ logIfError(err);
93
+
94
+ // start again
95
+ pm2.start(config, { ...opts }, (err, result) => {
96
+ reply({ success: !err, message: err?.message });
97
+ updateAndSave(err, result);
98
+ });
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Graceful restart logic:
104
+ * List of PM2 app envs to stop or restart
105
+ */
106
+ const appsToStop = [];
107
+ const appsStopped = [];
108
+ let numAppsStopping = 0;
109
+ let numTotalApps = undefined;
110
+
111
+ apps.forEach((app) => {
112
+ const env = app.pm2_env;
113
+
114
+ /**
115
+ * Asynchronously teardown/stop processes with active connections
116
+ */
117
+ if (env.status === cst.STOPPED_STATUS) {
118
+ appsStopped.push(env);
119
+
120
+ } else if (env.status !== cst.STOPPING_STATUS) {
121
+ appsToStop.push(env);
122
+
123
+ } else if (!restartingAppIds.has(env.pm_id)) {
124
+ numAppsStopping++;
125
+ }
126
+ });
127
+
128
+ /**
129
+ * - Start new process
130
+ * - Update NGINX config to expose only the new process
131
+ * - Stop old processes
132
+ * - Spawn/reactivate the rest of the processes (shared.MAX_ACTIVE_PROCESSES)
133
+ */
134
+ const onFirstAppsStart = async (initialApps, err, result) => {
135
+ /**
136
+ * release post-deploy action while proceeding with graceful restart of other processes
137
+ */
138
+ reply({ success: !err, message: err?.message });
139
+
140
+ if (err) { return console.error(err); }
141
+
142
+ let numActiveApps = initialApps.length + restartingAppIds.size;
143
+
144
+ /**
145
+ * - Write NGINX config to expose only the new active process
146
+ * - The old ones processes will go down asynchronously (or will be restarted)
147
+ */
148
+ writeNginxConfig(initialApps);
149
+
150
+ //
151
+ // Wait 1.5 seconds to ensure NGINX is updated & reloaded
152
+ //
153
+ await new Promise(resolve => setTimeout(resolve, 1500));
154
+
155
+ //
156
+ // Asynchronously stop/restart apps with active connections
157
+ // (They make take from minutes up to hours to stop)
158
+ //
159
+ appsToStop.forEach((app_env) => {
160
+ if (numActiveApps < shared.MAX_ACTIVE_PROCESSES) {
161
+ numActiveApps++;
162
+
163
+ restartingAppIds.add(app_env.pm_id);
164
+ pm2.restart(app_env.pm_id, (err, _) => {
165
+ restartingAppIds.delete(app_env.pm_id);
166
+ if (err) { return logIfError(err); }
167
+
168
+ // reset counter stats (restart_time=0)
169
+ pm2.reset(app_env.pm_id, logIfError);
170
+ });
171
+
172
+ } else {
173
+ pm2.stop(app_env.pm_id, logIfError);
174
+ }
175
+ });
176
+
177
+ if (numActiveApps < shared.MAX_ACTIVE_PROCESSES) {
178
+ const missingOnlineApps = shared.MAX_ACTIVE_PROCESSES - numActiveApps;
179
+
180
+ // console.log("Active apps is lower than MAX_ACTIVE_PROCESSES, will SCALE again =>", {
181
+ // missingOnlineApps,
182
+ // numActiveApps,
183
+ // newNumTotalApps: numTotalApps + missingOnlineApps
184
+ // });
185
+
186
+ pm2.scale(apps[0].name, numTotalApps + missingOnlineApps, updateAndSaveIfAllRunning);
187
+ }
188
+ };
189
+
190
+ const numHalfMaxActiveProcesses = Math.ceil(shared.MAX_ACTIVE_PROCESSES / 2);
191
+
192
+ /**
193
+ * Re-use previously stopped apps if available
194
+ */
195
+ if (appsStopped.length >= numHalfMaxActiveProcesses) {
196
+ const initialApps = appsStopped.splice(0, numHalfMaxActiveProcesses);
197
+
198
+ let numSucceeded = 0;
199
+ initialApps.forEach((app_env) => {
200
+ // console.log("pm2.restart => ", app_env.pm_id);
201
+
202
+ restartingAppIds.add(app_env.pm_id);
203
+ pm2.restart(app_env.pm_id, (err) => {
204
+ restartingAppIds.delete(app_env.pm_id);
205
+ if (err) { return replyIfError(err, reply); }
206
+
207
+ // reset counter stats (restart_time=0)
208
+ pm2.reset(app_env.pm_id, logIfError);
209
+
210
+ // TODO: set timeout here to exit if some processes are not restarting
211
+
212
+ numSucceeded++;
213
+ if (numSucceeded === initialApps.length) {
214
+ onFirstAppsStart(initialApps);
215
+ }
216
+ });
217
+ });
218
+
219
+ } else {
220
+ /**
221
+ * Increment to +(MAX/2) processes
222
+ */
223
+ let LAST_NODE_APP_INSTANCE = apps[apps.length - 1].pm2_env.NODE_APP_INSTANCE;
224
+ const initialApps = Array.from({ length: numHalfMaxActiveProcesses }).map((_, i) => {
225
+ const new_app_env = Object.assign({}, apps[0].pm2_env);
226
+ new_app_env.NODE_APP_INSTANCE = ++LAST_NODE_APP_INSTANCE;
227
+ return new_app_env;
228
+ });
229
+
230
+ numTotalApps = apps.length + numHalfMaxActiveProcesses;
231
+
232
+ // Ensure to scale to a number of processes where `numHalfMaxActiveProcesses` can start immediately.
233
+ pm2.scale(apps[0].name, numTotalApps, onFirstAppsStart.bind(undefined, initialApps));
234
+ }
235
+ });
236
+ }
237
+
238
+ function updateAndSave() {
239
+ // console.log("updateAndExit");
240
+ updateAndReloadNginx(() => complete());
241
+ }
242
+
243
+ function updateAndSaveIfAllRunning(err) {
244
+ if (err) { return console.error(err); }
245
+
246
+ updateAndReloadNginx((app_envs) => {
247
+ // console.log("updateAndExitIfAllRunning, app_ids (", app_envs.map(app_env => app_env.NODE_APP_INSTANCE) ,") => ", app_envs.length, "/", shared.MAX_ACTIVE_PROCESSES);
248
+
249
+ //
250
+ // TODO: add timeout to exit here, in case some processes are not starting
251
+ //
252
+ if (app_envs.length === shared.MAX_ACTIVE_PROCESSES) {
253
+ complete();
254
+ }
255
+ });
256
+ }
257
+
258
+ function updateAndReloadNginx(cb) {
259
+ //
260
+ // If you are self-hosting and reading this file, consider using the
261
+ // following in your self-hosted environment:
262
+ //
263
+ // #!/bin/bash
264
+ // # Requires fswatch (`apt install fswatch`)
265
+ // # Reload NGINX when colyseus_servers.conf changes
266
+ // fswatch /etc/nginx/colyseus_servers.conf -m poll_monitor --event=Updated | while read event
267
+ // do
268
+ // service nginx reload
269
+ // done
270
+
271
+ shared.listApps(function(err, apps) {
272
+ if (apps.length === 0) { err = "no apps running."; }
273
+ if (err) { return console.error(err); }
274
+
275
+ const app_envs = apps
276
+ .filter(app => app.pm2_env.status !== cst.STOPPING_STATUS && app.pm2_env.status !== cst.STOPPED_STATUS)
277
+ .map((app) => app.pm2_env);
278
+
279
+ writeNginxConfig(app_envs);
280
+
281
+ cb?.(app_envs);
282
+ });
283
+ }
284
+
285
+ function writeNginxConfig(app_envs) {
286
+ // console.log("writeNginxConfig: ", app_envs.map(app_env => app_env.NODE_APP_INSTANCE));
287
+
288
+ const port = 2567;
289
+ const addresses = [];
290
+
291
+ app_envs.forEach(function(app_env) {
292
+ addresses.push(`unix:${shared.PROCESS_UNIX_SOCK_PATH}${port + app_env.NODE_APP_INSTANCE}.sock`);
293
+ });
294
+
295
+ // write NGINX config
296
+ fs.writeFileSync(shared.NGINX_SERVERS_CONFIG_FILE, addresses.map(address => `server ${address};`).join("\n"), logIfError);
297
+ }
298
+
299
+ function complete() {
300
+ // "pm2 save"
301
+ pm2.dump(logIfError);
302
+ }
303
+
304
+ function logIfError (err) {
305
+ if (err) {
306
+ console.error(err);
307
+ }
308
+ }
309
+
310
+ function replyIfError(err, reply) {
311
+ if (err) {
312
+ console.error(err);
313
+ reply({ success: false, message: err?.message });
314
+ }
315
+ }
package/pm2/shared.js ADDED
@@ -0,0 +1,73 @@
1
+ const pm2 = require('pm2');
2
+ const os = require('os');
3
+
4
+ const NAMESPACE = 'cloud';
5
+ const MAX_ACTIVE_PROCESSES = os.cpus().length;
6
+
7
+ function listApps(callback) {
8
+ pm2.list((err, apps) => {
9
+ if (err) { return callback(err);; }
10
+
11
+ // Filter out @colyseus/tools module (PM2 post-deploy agent)
12
+ apps = apps.filter(app => app.name !== '@colyseus/tools');
13
+
14
+ callback(err, apps);
15
+ });
16
+ }
17
+
18
+ async function getAppConfig(ecosystemFilePath) {
19
+ const module = await import(ecosystemFilePath);
20
+ const config = module.default;
21
+
22
+ /**
23
+ * Tune PM2 app config
24
+ */
25
+ if (config.apps && config.apps.length >= 0) {
26
+ const app = config.apps[0];
27
+
28
+ // app.name = "colyseus-app";
29
+ app.namespace = NAMESPACE;
30
+ app.exec_mode = "fork";
31
+
32
+ app.instances = MAX_ACTIVE_PROCESSES;
33
+
34
+ app.time = true;
35
+ app.wait_ready = true;
36
+ app.watch = false;
37
+
38
+ // default: merge logs into a single file
39
+ if (app.merge_logs === undefined) {
40
+ app.merge_logs = true;
41
+ }
42
+
43
+ // default: wait for 30 minutes before forcibly killing
44
+ // (prevent forcibly killing while rooms are still active)
45
+ if (!app.kill_timeout) {
46
+ app.kill_timeout = 30 * 60 * 1000;
47
+ }
48
+
49
+ // default: retry kill after 1 second
50
+ if (!app.kill_retry_time) {
51
+ app.kill_retry_time = 5000;
52
+ }
53
+ }
54
+
55
+ return config;
56
+ }
57
+
58
+ module.exports = {
59
+ /**
60
+ * Constants
61
+ */
62
+ NGINX_SERVERS_CONFIG_FILE: '/etc/nginx/colyseus_servers.conf',
63
+ PROCESS_UNIX_SOCK_PATH: '/run/colyseus/',
64
+
65
+ MAX_ACTIVE_PROCESSES,
66
+ NAMESPACE,
67
+
68
+ /**
69
+ * Shared methods
70
+ */
71
+ listApps,
72
+ getAppConfig,
73
+ }
package/post-deploy.js CHANGED
@@ -1,17 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  const pm2 = require('pm2');
3
- const os = require('os');
4
3
  const fs = require('fs');
5
4
  const path = require('path');
5
+ const shared = require('./pm2/shared');
6
6
 
7
- const opts = { env: process.env.NODE_ENV || "production" };
8
- const maxCPU = os.cpus().length;
9
-
10
- // // allow deploying from other path as root.
11
- // if (process.env.npm_config_local_prefix) {
12
- // process.chdir(process.env.npm_config_local_prefix);
13
- // pm2.cwd = process.env.npm_config_local_prefix;
14
- // }
7
+ const opts = { env: process.env.NODE_ENV || "production", };
15
8
 
16
9
  const CONFIG_FILE = [
17
10
  'ecosystem.config.cjs',
@@ -20,88 +13,125 @@ const CONFIG_FILE = [
20
13
  'pm2.config.js',
21
14
  ].find((filename) => fs.existsSync(path.resolve(pm2.cwd, filename)));
22
15
 
16
+ /**
17
+ * TODO: if not provided, auto-detect entry-point & dynamically generate ecosystem config
18
+ */
23
19
  if (!CONFIG_FILE) {
24
20
  throw new Error('missing ecosystem config file. make sure to provide one with a valid "script" entrypoint file path.');
25
21
  }
26
22
 
27
- pm2.list(function(err, apps) {
28
- bailOnErr(err);
23
+ const CONFIG_FILE_PATH = `${pm2.cwd}/${CONFIG_FILE}`;
29
24
 
30
- if (apps.length === 0) {
31
- // first deploy
32
- pm2.start(CONFIG_FILE, {...opts}, onAppRunning);
25
+ /**
26
+ * Try to handle post-deploy via PM2 module first (pm2 install @colyseus/tools)
27
+ * If not available, fallback to legacy post-deploy script.
28
+ */
29
+ pm2.trigger('@colyseus/tools', 'post-deploy', `${pm2.cwd}:${CONFIG_FILE_PATH}`, async function (err, result) {
30
+ if (err) {
31
+ console.log("Proceeding with legacy post-deploy script...");
32
+ postDeploy();
33
33
 
34
34
  } else {
35
+ if (result[0].data?.return?.success === false) {
36
+ console.error(result[0].data?.return?.message);
37
+ process.exit(1);
38
+ } else {
39
+ console.log("Post-deploy success.");
40
+ process.exit();
41
+ }
42
+ }
43
+ });
35
44
 
36
- //
37
- // detect if cwd has changed, and restart PM2 if it has
38
- //
39
- if (apps[0].pm2_env.pm_cwd !== pm2.cwd) {
45
+ async function postDeploy() {
46
+ shared.listApps(function (err, apps) {
47
+ bailOnErr(err);
40
48
 
49
+ if (apps.length === 0) {
41
50
  //
42
- // remove all and start again with new cwd
51
+ // first deploy
43
52
  //
44
- pm2.delete('all', function(err) {
45
-
46
- // kill & start again
47
- pm2.kill(function() {
48
- pm2.start(CONFIG_FILE, { ...opts }, onAppRunning);
49
- });
50
-
51
- });
53
+ pm2.start(CONFIG_FILE_PATH, { ...opts }, () => onAppRunning());
52
54
 
53
55
  } else {
56
+
54
57
  //
55
- // reload existing apps
58
+ // detect if cwd has changed, and restart PM2 if it has
56
59
  //
57
- pm2.reload(CONFIG_FILE, {...opts}, function(err, apps) {
58
- bailOnErr(err);
59
-
60
- const name = apps[0].name;
61
-
62
- // scale app to use all CPUs available
63
- if (apps.length !== maxCPU) {
64
- pm2.scale(name, maxCPU, onAppRunning);
65
-
66
- } else {
67
- onAppRunning();
68
- }
69
- });
60
+ if (apps[0].pm2_env.pm_cwd !== pm2.cwd) {
61
+ //
62
+ // remove all and start again with new cwd
63
+ //
64
+ restartAll();
65
+
66
+ } else {
67
+ //
68
+ // reload existing apps
69
+ //
70
+ reloadAll();
71
+ }
70
72
  }
73
+ });
74
+ }
71
75
 
76
+ function onAppRunning(reloadedAppIds) {
77
+ // reset reloaded app stats
78
+ if (reloadedAppIds) {
79
+ resetAppStats(reloadedAppIds);
72
80
  }
73
- });
74
-
75
- function onAppRunning() {
76
- updateColyseusBootService();
77
81
  updateAndReloadNginx();
78
82
  }
79
83
 
80
- function updateColyseusBootService() {
81
- //
82
- // Update colyseus-boot.service to use the correct paths for the application
83
- //
84
+ function restartAll () {
85
+ pm2.delete('all', function (err) {
86
+ // kill & start again
87
+ pm2.kill(function () {
88
+ pm2.start(CONFIG_FILE_PATH, { ...opts }, () => onAppRunning());
89
+ });
90
+ });
91
+ }
84
92
 
85
- const COLYSEUS_CLOUD_BOOT_SERVICE = '/etc/systemd/system/colyseus-boot.service';
93
+ function reloadAll(retry = 0) {
94
+ pm2.reload(CONFIG_FILE_PATH, { ...opts }, function (err, apps) {
95
+ if (err) {
96
+ //
97
+ // Retry in case of "Reload in progress" error.
98
+ //
99
+ if (err.message === 'Reload in progress' && retry < 5) {
100
+ console.warn(err.message, ", retrying...");
101
+ setTimeout(() => reloadAll(retry + 1), 1000);
86
102
 
87
- // ignore if no boot service found
88
- if (!fs.existsSync(COLYSEUS_CLOUD_BOOT_SERVICE)) {
89
- return;
90
- }
103
+ } else {
104
+ bailOnErr(err);
105
+ }
91
106
 
92
- const workingDirectory = pm2.cwd;
93
- const execStart = `${detectPackageManager()} colyseus-post-deploy`;
107
+ return;
108
+ }
94
109
 
95
- const contents = fs.readFileSync(COLYSEUS_CLOUD_BOOT_SERVICE, 'utf8');
96
- try {
97
- fs.writeFileSync(COLYSEUS_CLOUD_BOOT_SERVICE, contents
98
- .replace(/WorkingDirectory=(.*)/, `WorkingDirectory=${workingDirectory}`)
99
- .replace(/ExecStart=(.*)/, `ExecStart=${execStart}`));
100
- } catch (e) {
101
- // couldn't write to file
102
- }
110
+ const name = apps[0].name;
111
+ const reloadedAppIds = apps.map(app => app.pm_id);
112
+
113
+ // scale app to use all CPUs available
114
+ if (apps.length !== shared.MAX_ACTIVE_PROCESSES) {
115
+ pm2.scale(name, shared.MAX_ACTIVE_PROCESSES, () => onAppRunning(reloadedAppIds));
116
+
117
+ } else {
118
+ onAppRunning(reloadedAppIds);
119
+ }
120
+ });
103
121
  }
104
122
 
123
+ function resetAppStats (reloadedAppIds) {
124
+ reloadedAppIds.forEach((pm_id) => {
125
+ pm2.reset(pm_id, (err, _) => {
126
+ if (err) {
127
+ console.error(err);
128
+ } else {
129
+ console.log(`metrics re-set for app_id: ${pm_id}`);
130
+ }
131
+ });
132
+ });
133
+ };
134
+
105
135
  function updateAndReloadNginx() {
106
136
  //
107
137
  // If you are self-hosting and reading this file, consider using the
@@ -115,9 +145,7 @@ function updateAndReloadNginx() {
115
145
  // service nginx reload
116
146
  // done
117
147
 
118
- const NGINX_SERVERS_CONFIG_FILE = '/etc/nginx/colyseus_servers.conf';
119
-
120
- pm2.list(function(err, apps) {
148
+ shared.listApps(function(err, apps) {
121
149
  if (apps.length === 0) {
122
150
  err = "no apps running.";
123
151
  }
@@ -127,11 +155,11 @@ function updateAndReloadNginx() {
127
155
  const addresses = [];
128
156
 
129
157
  apps.forEach(function(app) {
130
- addresses.push(`unix:/run/colyseus/${ port + app.pm2_env.NODE_APP_INSTANCE }.sock`);
158
+ addresses.push(`unix:${shared.PROCESS_UNIX_SOCK_PATH}${port + app.pm2_env.NODE_APP_INSTANCE}.sock`);
131
159
  });
132
160
 
133
161
  // write NGINX config
134
- fs.writeFileSync(NGINX_SERVERS_CONFIG_FILE, addresses.map(address => `server ${address};`).join("\n"), bailOnErr);
162
+ fs.writeFileSync(shared.NGINX_SERVERS_CONFIG_FILE, addresses.map(address => `server ${address};`).join("\n"), bailOnErr);
135
163
 
136
164
  // "pm2 save"
137
165
  pm2.dump(function (err, ret) {
@@ -144,30 +172,6 @@ function updateAndReloadNginx() {
144
172
  });
145
173
  }
146
174
 
147
- function detectPackageManager() {
148
- const lockfiles = {
149
- // npm
150
- "npm exec": path.resolve(pm2.cwd, 'package-lock.json'),
151
-
152
- // yarn
153
- "yarn exec": path.resolve(pm2.cwd, 'yarn.lock'),
154
-
155
- // pnpm
156
- "pnpm exec": path.resolve(pm2.cwd, 'pnpm-lock.yaml'),
157
-
158
- // bun
159
- "bunx": path.resolve(pm2.cwd, 'bun.lockb'),
160
- };
161
-
162
- for (const [key, value] of Object.entries(lockfiles)) {
163
- if (fs.existsSync(value)) {
164
- return key;
165
- }
166
- }
167
-
168
- return "npm";
169
- }
170
-
171
175
  function bailOnErr(err) {
172
176
  if (err) {
173
177
  console.error(err);
@@ -176,4 +180,3 @@ function bailOnErr(err) {
176
180
  process.exit(1);
177
181
  }
178
182
  }
179
-
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const net = require('net');
5
+ const pm2 = require('pm2');
6
+
7
+ const COLYSEUS_CLOUD_URL = `${process.env.ENDPOINT}/vultr/stats`;
8
+
9
+ const FAILED_ATTEMPS_FILE = "/var/tmp/pm2-stats-attempts.txt";
10
+ const FETCH_TIMEOUT = 7000;
11
+
12
+ async function retryFailedAttempts() {
13
+ /**
14
+ * Retry cached failed attempts
15
+ */
16
+ if (!fs.existsSync(FAILED_ATTEMPS_FILE)) {
17
+ return;
18
+ }
19
+
20
+ // Retry up to last 30 attempts
21
+ const failedAttempts = fs.readFileSync(FAILED_ATTEMPS_FILE, "utf8").split("\n").slice(-30);
22
+
23
+ for (const body of failedAttempts) {
24
+ // skip if empty
25
+ if (!body) { continue; }
26
+
27
+ try {
28
+ await fetch(COLYSEUS_CLOUD_URL, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "Authorization": `Bearer ${process.env.COLYSEUS_SECRET}`,
33
+ "ngrok-skip-browser-warning": "yes",
34
+ },
35
+ body,
36
+ signal: AbortSignal.timeout(FETCH_TIMEOUT)
37
+ });
38
+ } catch (e) {
39
+ console.error(e);
40
+ }
41
+ }
42
+
43
+ fs.unlinkSync(FAILED_ATTEMPS_FILE);
44
+ }
45
+
46
+ async function fetchRetry(url, options, retries = 3) {
47
+ try {
48
+ return await fetch(url, options)
49
+ } catch (err) {
50
+ if (retries === 1) throw err;
51
+ return await fetchRetry(url, options, retries - 1);
52
+ }
53
+ };
54
+
55
+ pm2.Client.executeRemote('getMonitorData', {}, async function(err, list) {
56
+ if (err) { return console.error(err); }
57
+
58
+ //
59
+ // Filter out:
60
+ // - @colyseus/tools module (PM2 agent)
61
+ // - "stopped" processes (gracefully shut down)
62
+ //
63
+ list = list.filter((item) => {
64
+ return (
65
+ item.name !== '@colyseus/tools' &&
66
+ item.pm2_env.status !== 'stopped'
67
+ );
68
+ });
69
+
70
+ const aggregate = { ccu: 0, roomcount: 0, };
71
+ const apps = {};
72
+
73
+ await Promise.all(list.map(async (item) => {
74
+ const env = item.pm2_env;
75
+ const app_id = env.pm_id;
76
+ const uptime = new Date(env.pm_uptime); // uptime in milliseconds
77
+ const axm_monitor = env.axm_monitor;
78
+ const restart_time = env.restart_time;
79
+ const status = env.status; // 'online', 'stopped', 'stopping', 'waiting restart', 'launching', 'errored'
80
+ const node_version = env.node_version;
81
+
82
+ const monit = {
83
+ cpu: item.monit.cpu, // 0 ~ 100 (%)
84
+ memory: item.monit.memory / 1024 / 1024, // in MB
85
+ };
86
+
87
+ const version = {};
88
+ if (env.versioning) {
89
+ version.revision = env.versioning.revision;
90
+ version.comment = env.versioning.comment;
91
+ version.branch = env.versioning.branch;
92
+ version.update_time = env.versioning.update_time;
93
+ }
94
+
95
+ aggregate.ccu += axm_monitor.ccu?.value ?? 0;
96
+ aggregate.roomcount += axm_monitor.roomcount?.value ?? 0;
97
+
98
+ const custom_monitor = {};
99
+ for (const originalKey in axm_monitor) {
100
+ const key = (originalKey.indexOf(" ") !== -1)
101
+ ? axm_monitor[originalKey].type
102
+ : originalKey;
103
+ custom_monitor[key] = Number(axm_monitor[originalKey].value);
104
+ }
105
+
106
+ // check if process .sock file is active
107
+ const socket_is_active = await checkSocketIsActive(`/run/colyseus/${(2567 + env.NODE_APP_INSTANCE)}.sock`);
108
+
109
+ apps[app_id] = {
110
+ pid: item.pid,
111
+ uptime,
112
+ app_id,
113
+ status,
114
+ restart_time,
115
+ ...monit,
116
+ ...custom_monitor,
117
+ version,
118
+ node_version,
119
+ socket_is_active,
120
+ };
121
+ }));
122
+
123
+ const fetchipv4 = await fetch("http://169.254.169.254/v1.json");
124
+ const ip = (await fetchipv4.json()).interfaces[0].ipv4.address;
125
+
126
+ const body = {
127
+ version: 1,
128
+ ip,
129
+ time: new Date(),
130
+ aggregate,
131
+ apps,
132
+ };
133
+
134
+ console.log(body);
135
+
136
+ try {
137
+ const response = await fetchRetry(COLYSEUS_CLOUD_URL, {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ "Authorization": `Bearer ${process.env.COLYSEUS_SECRET}`,
142
+ "ngrok-skip-browser-warning": "yes",
143
+ },
144
+ body: JSON.stringify(body),
145
+ signal: AbortSignal.timeout(FETCH_TIMEOUT)
146
+ });
147
+
148
+ if (response.status !== 200) {
149
+ throw new Error(`Failed to send stats to Colyseus Cloud. Status: ${response.status}`);
150
+ }
151
+
152
+ console.log("OK");
153
+
154
+ // Only retry failed attempts if the current attempt was successful
155
+ await retryFailedAttempts();
156
+
157
+ } catch (e) {
158
+ console.error("Failed to send stats to Colyseus Cloud. ");
159
+
160
+ // cache failed attempts
161
+ fs.appendFileSync(FAILED_ATTEMPS_FILE, JSON.stringify(body) + "\n");
162
+
163
+ } finally {
164
+ process.exit();
165
+ }
166
+ });
167
+
168
+ function checkSocketIsActive(sockFilePath) {
169
+ return new Promise((resolve, _) => {
170
+ const client = net.createConnection({ path: sockFilePath, timeout: 5000 })
171
+ .on('connect', () => {
172
+ client.end(); // close the connection
173
+ resolve(true);
174
+ })
175
+ .on('error', () => resolve(false))
176
+ .on('timeout', () => resolve(false));
177
+ });
178
+ }
package/system-boot.js CHANGED
@@ -2,56 +2,110 @@
2
2
 
3
3
  const os = require('os');
4
4
  const fs = require('fs');
5
- const { exec } = require('child_process');
5
+ const { execSync } = require('child_process');
6
6
 
7
7
  const NGINX_LIMITS_CONFIG_FILE = '/etc/nginx/colyseus_limits.conf';
8
- const LIMITS_CONF_FILE = "/etc/security/limits.conf";
8
+ const LIMITS_CONF_FILE = '/etc/security/limits.d/colyseus.conf';
9
+ const SYSCTL_FILE = '/etc/sysctl.d/local.conf'
9
10
 
10
- // update file descriptor limits systemwide + nginx worker connections
11
+ const MAX_CCU_PER_CPU = 8000;
11
12
 
12
- function bailOnErr(err) {
13
- if (err) {
14
- console.error(err);
13
+ /**
14
+ * System-wide limits configuration for high-CCU environments
15
+ * @param {Object} options
16
+ * @param {number} options.maxCCUPerCPU - Maximum concurrent users per CPU core
17
+ * @param {number} options.connectionsMultiplier - Multiplier for worker connections (default: 3)
18
+ * @param {number} options.fileDescriptorMultiplier - Multiplier for max file descriptors (default: 4)
19
+ */
20
+ function configureSystemLimits(options = {}) {
21
+ const {
22
+ connectionsMultiplier = 3,
23
+ fileDescriptorMultiplier = 4
24
+ } = options;
15
25
 
16
- // exit with error!
17
- process.exit(1);
18
- }
19
- }
26
+ const numCPU = os.cpus().length;
27
+ const maxCCU = numCPU * MAX_CCU_PER_CPU;
20
28
 
21
- function updateNOFileConfig(cb) {
22
- // const numCPU = os.cpus().length;
23
- const totalmemMB = os.totalmem() / 1024 / 1024;
24
- const estimatedCCUPerGB = 5000;
29
+ // Calculate limits
30
+ const workerConnections = maxCCU * connectionsMultiplier;
31
+ const maxFileDescriptors = maxCCU * fileDescriptorMultiplier;
25
32
 
26
- const maxCCU = Math.floor((totalmemMB / 1024) * estimatedCCUPerGB);
27
- const workerConnections = maxCCU * 2; // 2x because of proxy_pass
28
- const nginxMaxNOFileLimit = workerConnections * 2; // 2x
29
- const systemMaxNOFileLimit = workerConnections * 3; // 3x for other system operations
33
+ // Nginx-specific calculations
34
+ const workerRlimitNofile = (workerConnections / numCPU) * 2;
30
35
 
31
- // immediatelly apply new nofile limit
32
- // (apparently this has no effect)
33
- exec(`ulimit -n ${systemMaxNOFileLimit}`, bailOnErr);
36
+ // Validation checks
37
+ if (workerConnections > 65535) {
38
+ console.warn(`Warning: worker_connections (${workerConnections}) exceeds typical max_socket_backlog (65535)`);
39
+ }
34
40
 
35
- // update "/etc/security/limits.conf" file.
36
- fs.writeFileSync(LIMITS_CONF_FILE, `
37
- * - nofile ${systemMaxNOFileLimit}
38
- `, bailOnErr);
41
+ if (maxFileDescriptors > 1048576) {
42
+ console.warn(`Warning: Very high file descriptor limit (${maxFileDescriptors}). Verify system capabilities.`);
43
+ }
39
44
 
40
- if (fs.existsSync(NGINX_LIMITS_CONFIG_FILE)) {
41
- fs.writeFileSync(NGINX_LIMITS_CONFIG_FILE, `
42
- worker_rlimit_nofile ${nginxMaxNOFileLimit};
45
+ if (process.argv.includes('--dry-run')) {
46
+ console.log({
47
+ maxCCU,
48
+ workerConnections,
49
+ maxFileDescriptors,
50
+ workerRlimitNofile
51
+ })
52
+ process.exit();
53
+ }
43
54
 
55
+ // Configuration updates
56
+ try {
57
+ // Update Nginx limits
58
+ if (fs.existsSync(NGINX_LIMITS_CONFIG_FILE)) {
59
+ fs.writeFileSync(NGINX_LIMITS_CONFIG_FILE, `
60
+ worker_rlimit_nofile ${workerRlimitNofile};
44
61
  events {
62
+ use epoll;
45
63
  worker_connections ${workerConnections};
46
- # multi_accept on;
64
+ multi_accept on;
47
65
  }
48
- `, cb);
49
- console.log("new nofile limit:", { workerConnections, systemMaxNOFileLimit, nginxMaxNOFileLimit });
66
+ `);
67
+ }
68
+
69
+ // Update system-wide limits
70
+ fs.writeFileSync(LIMITS_CONF_FILE, `
71
+ # System-wide file descriptor limits
72
+ * - nofile ${maxFileDescriptors}
73
+ nginx soft nofile ${maxFileDescriptors}
74
+ nginx hard nofile ${maxFileDescriptors}
75
+ `);
50
76
 
51
- } else {
52
- console.warn(NGINX_LIMITS_CONFIG_FILE, "not found.");
77
+ // Update sysctl with doubled file-max for safety margin
78
+ fs.writeFileSync(SYSCTL_FILE, `
79
+ # System-wide file descriptor limit
80
+ fs.file-max = ${maxFileDescriptors * 2}
81
+
82
+ # TCP buffer optimization
83
+ net.core.rmem_max = 16777216
84
+ net.core.wmem_max = 16777216
85
+ net.ipv4.tcp_rmem = 4096 87380 16777216
86
+ net.ipv4.tcp_wmem = 4096 87380 16777216
87
+
88
+ # Connection handling optimization
89
+ net.core.netdev_max_backlog = 50000
90
+ net.core.somaxconn = 65535
91
+ net.ipv4.tcp_tw_reuse = 1
92
+ net.ipv4.ip_local_port_range = 1024 65535
93
+
94
+ # TCP timeout optimization
95
+ net.ipv4.tcp_fin_timeout = 30
96
+ net.ipv4.tcp_keepalive_time = 30
97
+ net.ipv4.tcp_keepalive_intvl = 10
98
+ net.ipv4.tcp_keepalive_probes = 3
99
+ `);
100
+
101
+ // Apply sysctl changes
102
+ execSync("sysctl -p", { stdio: 'inherit' });
103
+ console.log(`System limits configured successfully for ${maxCCU} CCU (${MAX_CCU_PER_CPU}/CPU)`);
104
+
105
+ } catch (error) {
106
+ console.error('Failed to update system limits:', error);
107
+ process.exit(1);
53
108
  }
54
109
  }
55
110
 
56
-
57
- updateNOFileConfig(bailOnErr);
111
+ configureSystemLimits();