Merge pull request #4 from AsgardEternal/multiconfig

Multiconfig
This commit is contained in:
Skillet 2023-11-18 13:16:49 -05:00 committed by GitHub
commit 2e5a88127c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 20 deletions

4
.gitignore vendored
View File

@ -14,3 +14,7 @@ yarn.lock
.vs/ .vs/
/squad-server/plugins/db-log-addOn.js /squad-server/plugins/db-log-addOn.js
/squad-server/plugins/mapvote.js /squad-server/plugins/mapvote.js
config.json
config.*.json
!config.example.json

View File

@ -4,4 +4,4 @@
yarn run lint-staged yarn run lint-staged
yarn run build-all yarn run build-all
git add README.md git add README.md
git add config.json git add config.example.json

139
README.md
View File

@ -37,7 +37,7 @@ SquadJS relies on being able to access the Squad server log directory in order t
1. [Download SquadJS](https://github.com/Team-Silver-Sphere/SquadJS/releases/latest) and unzip the download. 1. [Download SquadJS](https://github.com/Team-Silver-Sphere/SquadJS/releases/latest) and unzip the download.
2. Open the unzipped folder in your terminal. 2. Open the unzipped folder in your terminal.
3. Install the dependencies by running `yarn install` in your terminal. Due to the use of Yarn Workspaces it is important to use `yarn install` and **not** `npm install` as this will not work and will break stuff. 3. Install the dependencies by running `yarn install` in your terminal. Due to the use of Yarn Workspaces it is important to use `yarn install` and **not** `npm install` as this will not work and will break stuff.
4. Configure the `config.json` file. See below for more details. 4. Create a new configuration. See [here](#configuring-squadjs) for more details.
5. Start SquadJS by running `node index.js` in your terminal. 5. Start SquadJS by running `node index.js` in your terminal.
**Note** - If you are interested in testing versions of SquadJS not yet released please download/clone the `master` branch. Please also see [here](#versions-and-releases) for more information on our versions and release procedures. **Note** - If you are interested in testing versions of SquadJS not yet released please download/clone the `master` branch. Please also see [here](#versions-and-releases) for more information on our versions and release procedures.
@ -45,10 +45,14 @@ SquadJS relies on being able to access the Squad server log directory in order t
<br> <br>
## **Configuring SquadJS** ## **Configuring SquadJS**
SquadJS can be configured via a JSON configuration file which, by default, is located in the SquadJS and is named [config.json](./config.json). SquadJS can be configured via a JSON configuration file. The default location for the config is [config.json](./config.json). This file has to be created manually.
`config.example.json` can be used as template for new configurations.
You can maintain multiple configs using this naming scheme: `config.<configname>.json`. This naming scheme is useful for multiple servers or [config merging](#merged-configurations).
The config file needs to be valid JSON syntax. If an error is thrown saying the config cannot be parsed then try putting the config into a JSON syntax checker (there's plenty to choose from that can be found via Google). The config file needs to be valid JSON syntax. If an error is thrown saying the config cannot be parsed then try putting the config into a JSON syntax checker (there's plenty to choose from that can be found via Google).
### Configuration Sections
<details> <details>
<summary>Server</summary> <summary>Server</summary>
@ -191,9 +195,140 @@ The `logger` section configures how verbose a module of SquadJS will be as well
``` ```
The larger the number set in the `verboseness` section for a specified module the more it will print to the console. The larger the number set in the `verboseness` section for a specified module the more it will print to the console.
Early stages of SquadJS initialization, before the configuration has been read and applied, will only log with a `verboseness` of 1. In order to enable verbose output for in those early stages the `VERBOSE` environment variable can be set to `true`. This will ignore all `verboseness` configurations and log everything.
--- ---
</details> </details>
### Merged Configurations
The configuration can be spread over multiple config files. This can be helpful if run multiple servers and want to ensure all servers run the same configuration. Or if you have a large configuration for a plugin (e.g. a lot of [ChatCommands](#chatcommands) ) that make your config unreadable.
Config files can be included by specifying their path in the `baseincludes` list your config. These files will be loaded in order and overwrite values from previously loaded configs. There is also a `overwrites` list that allows you to specify configs that should overwrite values in this config. Normal users should use the `baseincludes`. All relative paths have a small search order in which the file is first looked for, first in the directory relative to which the config that includes it, then inside the SquadJS directory.
This allows sharing of a 'config folder' that references configs relative to other configs in that folder and makes sharing configs easier.
<details>
<summary>Example with multiple config files</summary>
---
In this example we have 2 servers running and want the same configuration on both servers. The only diffrences are each server uses a diffrent discord bot and server 1 has verbose logging for the RCON module enabled.
Server 1 will be started using `node index.js config.server1.json`.
Server 2 will be started using `node index.js config.server2.json`.
- <details>
<summary>config.server1.json</summary>
```json
{
"baseincludes": ["./config.base.json"],
"server": {
"id": 1,
"host": "localhost",
"queryPort": 27165,
"rconPort": 21114,
"rconPassword": "test1",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/server1/log/folder"
},
"connectors": {
"discord": "Token Discord Bot 1"
},
"logger": {
"verboseness": {
"RCON": 4
}
}
}
```
</details>
- <details>
<summary>config.server2.json</summary>
```json
{
"baseincludes": ["./config.base.json"],
"server": {
"id": 2,
"host": "localhost",
"queryPort": 27175,
"rconPort": 21124,
"rconPassword": "test2",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/server2/log/folder"
},
"connectors": {
"discord": "Token Discord Bot 2"
}
}
```
</details>
- <details>
<summary>config.base.json</summary>
```json
{
"baseincludes": ["./config.example.json"],
"server": {
"ftp": {
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
},
"adminLists": [
{
"type": "",
"source": ""
}
]
},
"connectors": {
"mysql": {
"host": "host",
"port": 3306,
"username": "squadjs",
"password": "password",
"database": "squadjs",
"dialect": "mysql"
},
"sqlite": "sqlite:database.sqlite"
},
"plugins": [ "THIS HAS BEEN REMOVED TO MAKE THE CONFIG MORE READABLE" ],
"logger": {
"verboseness": {
"SquadServer": 1,
"LogParser": 1,
"RCON": 1
},
"colors": {
"SquadServer": "yellowBright",
"SquadServerFactory": "yellowBright",
"LogParser": "blueBright",
"RCON": "redBright"
}
}
}
```
</details>
---
</details>
<br>
<details>
<summary>Precise loading order of configs</summary>
Configs are parsed recursively through their `baseincludes`, their own json, and finally their `overwrites` and stored sequentially to a list that contains the json from each config. So in the above example the resultant list of configs loaded from loading `config.server1.json` would be `config.example.json, config.base.json, config.server1.json`. These configs then overwrite the current configuration in the order that they are in this list. This means that if you have `base-includes = ["config.external.json", "config.example.json"]` set in `config.myconfig.json`, with `config.external.json` also including `config.example.json`, the resultant list would like: `config.example.json, config.external.json, config.example.json, config.myconfig.json` and be merged as such.
Due to this, the ordering of the included jsons was most likely an error here, and you would actually want `config.external.json` second and `config.example.json` first.
</details>
Please remember to avoid circular dependencies and it is considered good practice to include `config.example.json` as the base for other configs. If a new property is added to the example config that is required by SquadJS due to an update, your configs will prevent SquadJS from running.
If you have problems with this, you can view the merged config by forcing SquadJS to log everything to the console.
See [here](#console-output-configuration) for more information on console output.
<br> <br>
## **Plugins** ## **Plugins**

View File

@ -1,4 +1,6 @@
{ {
"baseincludes": [],
"overwrites": [],
"server": { "server": {
"id": 1, "id": 1,
"host": "xxx.xxx.xxx.xxx", "host": "xxx.xxx.xxx.xxx",

View File

@ -11,7 +11,7 @@ class Logger {
let colorFunc = chalk[this.colors[module] || 'white']; let colorFunc = chalk[this.colors[module] || 'white'];
if (typeof colorFunc !== 'function') colorFunc = chalk.white; if (typeof colorFunc !== 'function') colorFunc = chalk.white;
if ((this.verboseness[module] || 1) >= verboseness) if (process.env.VERBOSE === 'true' || (this.verboseness[module] || 1) >= verboseness)
console.log( console.log(
`${this.includeTimestamps ? '[' + new Date().toISOString() + ']' : ''}[${colorFunc( `${this.includeTimestamps ? '[' + new Date().toISOString() + ']' : ''}[${colorFunc(
module module

View File

@ -5,19 +5,20 @@ async function main() {
await printLogo(); await printLogo();
const config = process.env.config; const config = process.env.config;
const configPath = process.argv[2]; const configPath = process.argv.length > 2 ? process.argv[2] : null;
if (config && configPath) throw new Error('Cannot accept both a config and config path.'); if (config && configPath) throw new Error('Cannot accept both a config and config path.');
// create a SquadServer instance // create a SquadServer instance
const server = config const server = config
? await SquadServerFactory.buildFromConfigString(config) ? await SquadServerFactory.buildFromConfigString(config)
: await SquadServerFactory.buildFromConfigFile(configPath || './config.json'); : await SquadServerFactory.buildFromConfigFile(configPath ? configPath : './config.json');
// watch the server // watch the server
await server.watch(); // await server.watch();
// now mount the plugins // now mount the plugins
await Promise.all(server.plugins.map(async (plugin) => await plugin.mount())); // await Promise.all(server.plugins.map(async (plugin) => await plugin.mount()));
} }
main(); main();

View File

@ -1,10 +1,11 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import {fileURLToPath} from 'url';
import Discord from 'discord.js'; import Discord from 'discord.js';
import sequelize from 'sequelize'; import sequelize from 'sequelize';
import AwnAPI from './utils/awn-api.js'; import AwnAPI from './utils/awn-api.js';
import ConfigTools from './utils/config-tools.js';
import Logger from 'core/logger'; import Logger from 'core/logger';
@ -17,6 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default class SquadServerFactory { export default class SquadServerFactory {
static async buildFromConfig(config) { static async buildFromConfig(config) {
Logger.verbose('SquadServerFactory', 4, `Logging config:\n${JSON.stringify(config)}`);
Logger.setTimeStamps(config.logger.timestamps ? config.logger.timestamps : false); Logger.setTimeStamps(config.logger.timestamps ? config.logger.timestamps : false);
const plugins = await Plugins.getPlugins(); const plugins = await Plugins.getPlugins();
@ -147,11 +149,35 @@ export default class SquadServerFactory {
throw new Error(`${type.connector} is an unsupported connector type.`); throw new Error(`${type.connector} is an unsupported connector type.`);
} }
static parseConfig(configString) { static parseConfig(configString, configfile = "", previousconfigfiles = null) {
configfile = path.resolve(configfile);
if (!previousconfigfiles) previousconfigfiles = []
if (previousconfigfiles.indexOf(configfile) > -1){
Logger.verbose('SquadServerFactory', 1, `Found circular config with ${previousconfigfiles.slice(1)}`);
throw new Error(`Config has circular dependency at ${configfile} in with previous configs ${previousconfigfiles}!`);
}
else previousconfigfiles.push(configfile)
try { try {
return JSON.parse(configString); const jsonconfig = JSON.parse(configString);
const beforeincludes = jsonconfig.baseincludes ? jsonconfig.baseincludes.flatMap((filename) => {
try {
return SquadServerFactory.parseConfig(
SquadServerFactory.readConfigFile(
filename,
path.dirname(configfile)
),
filename,
previousconfigfiles
);
} catch (err) {
throw new Error(`Config file ${filename} from ${configfile} did not load. ${err}`)
}
}) : [];
const overwrites = jsonconfig.overwrites ? jsonconfig.overwrites.flatMap((filename) => SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(filename), filename, previousconfigfiles)) : [];
const retconfig = [beforeincludes, jsonconfig, overwrites].flat(1);
return retconfig;
} catch (err) { } catch (err) {
throw new Error('Unable to parse config file.'); throw new Error(`Unable to parse config file. ${err}`);
} }
} }
@ -160,15 +186,22 @@ export default class SquadServerFactory {
return SquadServerFactory.buildFromConfig(SquadServerFactory.parseConfig(configString)); return SquadServerFactory.buildFromConfig(SquadServerFactory.parseConfig(configString));
} }
static readConfigFile(configPath = './config.json') { static readConfigFile(configPath = './config.json', dirlocation = null) {
configPath = path.resolve(__dirname, '../', configPath); const configLoc = dirlocation ? path.resolve(__dirname, '../', dirlocation, configPath) : path.resolve(__dirname, '../', configPath);
if (!fs.existsSync(configPath)) throw new Error('Config file does not exist.'); if (!fs.existsSync(configLoc)) throw new Error(`Config file does not exist. ${configLoc}`);
return fs.readFileSync(configPath, 'utf8');
Logger.verbose('SquadServerFactory', 1, `Reading config file ${configLoc}`);
return fs.readFileSync(configLoc, 'utf8');
} }
static buildFromConfigFile(configPath) { static buildFromConfigFile(configPath) {
Logger.verbose('SquadServerFactory', 1, 'Reading config file...'); Logger.verbose('SquadServerFactory', 1, 'Reading config file...');
return SquadServerFactory.buildFromConfigString(SquadServerFactory.readConfigFile(configPath)); Logger.verbose('SquadServerFactory', 4, JSON.stringify(configPath));
let configs = SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(configPath));
const mergedconfig = ConfigTools.mergeConfigs({}, ...configs);
Logger.verbose('SquadServerFactory', 4, `Merged config: ${JSON.stringify(mergedconfig)}`)
return SquadServerFactory.buildFromConfig(mergedconfig);
} }
static async buildConfig() { static async buildConfig() {
@ -197,7 +230,7 @@ export default class SquadServerFactory {
} }
static async buildConfigFile() { static async buildConfigFile() {
const configPath = path.resolve(__dirname, '../config.json'); const configPath = path.resolve(__dirname, '../config.example.json');
const config = await SquadServerFactory.buildConfig(); const config = await SquadServerFactory.buildConfig();
const configString = JSON.stringify(config, null, 2); const configString = JSON.stringify(config, null, 2);

View File

@ -37,7 +37,7 @@ SquadJS relies on being able to access the Squad server log directory in order t
1. [Download SquadJS](https://github.com/Team-Silver-Sphere/SquadJS/releases/latest) and unzip the download. 1. [Download SquadJS](https://github.com/Team-Silver-Sphere/SquadJS/releases/latest) and unzip the download.
2. Open the unzipped folder in your terminal. 2. Open the unzipped folder in your terminal.
3. Install the dependencies by running `yarn install` in your terminal. Due to the use of Yarn Workspaces it is important to use `yarn install` and **not** `npm install` as this will not work and will break stuff. 3. Install the dependencies by running `yarn install` in your terminal. Due to the use of Yarn Workspaces it is important to use `yarn install` and **not** `npm install` as this will not work and will break stuff.
4. Configure the `config.json` file. See below for more details. 4. Create a new configuration. See [here](#configuring-squadjs) for more details.
5. Start SquadJS by running `node index.js` in your terminal. 5. Start SquadJS by running `node index.js` in your terminal.
**Note** - If you are interested in testing versions of SquadJS not yet released please download/clone the `master` branch. Please also see [here](#versions-and-releases) for more information on our versions and release procedures. **Note** - If you are interested in testing versions of SquadJS not yet released please download/clone the `master` branch. Please also see [here](#versions-and-releases) for more information on our versions and release procedures.
@ -45,10 +45,14 @@ SquadJS relies on being able to access the Squad server log directory in order t
<br> <br>
## **Configuring SquadJS** ## **Configuring SquadJS**
SquadJS can be configured via a JSON configuration file which, by default, is located in the SquadJS and is named [config.json](./config.json). SquadJS can be configured via a JSON configuration file. The default location for the config is [config.json](./config.json). This file has to be created manualy.
`config.example.json` can be used as template for new configurations.
You can maintain multiple configs using this naming scheme: `config.<configname>.json`. This naming scheme is useful for multiple servers or [config merging](#merged-configurations).
The config file needs to be valid JSON syntax. If an error is thrown saying the config cannot be parsed then try putting the config into a JSON syntax checker (there's plenty to choose from that can be found via Google). The config file needs to be valid JSON syntax. If an error is thrown saying the config cannot be parsed then try putting the config into a JSON syntax checker (there's plenty to choose from that can be found via Google).
### Configuration Sections
<details> <details>
<summary>Server</summary> <summary>Server</summary>
@ -191,9 +195,124 @@ The `logger` section configures how verbose a module of SquadJS will be as well
``` ```
The larger the number set in the `verboseness` section for a specified module the more it will print to the console. The larger the number set in the `verboseness` section for a specified module the more it will print to the console.
Early stages of SquadJS initialization, before the configuration has been read and applied, will only log with a `verboseness` of 1. In order to enable verbose output for in those early stages the `VERBOSE` environment variable can be set to `true`. This will ignore all `verboseness` configurations and log everything.
--- ---
</details> </details>
### Merged Configurations
The configuration can be spread over multiple config files. This can be helpful if run multiple servers and want to ensure all servers run the same configuration. Or if you have a large configuration for a plugin (e.g. a lot of [ChatCommands](#chatcommands) ) that make your config unreadable.
<details>
<summary>Example with multiple config files</summary>
---
In this example we have 2 servers running and want the same configuration on both servers. The only diffrences are each server uses a diffrent discord bot and server 1 has verbose logging for the RCON module enabled.
Server 1 will be started using `node index.js config.base.json config.server1.json`.
Server 2 will be started using `node index.js config.base.json config.server2.json`.
- <details>
<summary>config.server1.json</summary>
```json
{
"server": {
"id": 1,
"host": "localhost",
"queryPort": 27165,
"rconPort": 21114,
"rconPassword": "test1",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/server1/log/folder"
},
"connectors": {
"discord": "Token Discord Bot 1"
},
"logger": {
"verboseness": {
"RCON": 4
}
}
}
```
</details>
- <details>
<summary>config.server2.json</summary>
```json
{
"server": {
"id": 2,
"host": "localhost",
"queryPort": 27175,
"rconPort": 21124,
"rconPassword": "test2",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/server2/log/folder"
},
"connectors": {
"discord": "Token Discord Bot 2"
}
}
```
</details>
- <details>
<summary>config.base.json</summary>
```json
{
"server": {
"ftp": {
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
},
"adminLists": [
{
"type": "",
"source": ""
}
]
},
"connectors": {
"mysql": {
"host": "host",
"port": 3306,
"username": "squadjs",
"password": "password",
"database": "squadjs",
"dialect": "mysql"
},
"sqlite": "sqlite:database.sqlite"
},
"plugins": [ "THIS HAS BEEN REMOVED TO MAKE THE CONFIG MORE READABLE" ],
"logger": {
"verboseness": {
"SquadServer": 1,
"LogParser": 1,
"RCON": 1
},
"colors": {
"SquadServer": "yellowBright",
"SquadServerFactory": "yellowBright",
"LogParser": "blueBright",
"RCON": "redBright"
}
}
}
```
</details>
---
</details>
If you have problems with this, you can view the merged config by forcing SquadJS to log everything to the console.
See [here](#console-output-configuration) for more information on console output.
<br> <br>
## **Plugins** ## **Plugins**

View File

@ -0,0 +1,27 @@
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
export default class ConfigTools {
static mergeConfigs(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
ConfigTools.mergeConfigs(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
if (Array.isArray(target) && Array.isArray(source)) {
target = [...target, ...source];
}
return ConfigTools.mergeConfigs(target, ...sources);
}
}