mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Merge pull request #481 from Hornwitser/rename-master-slave
Rename master/slave terminology to controller/host
This commit is contained in:
144 files changed
+2464
-2407
No files matched your search
@@ -26,7 +26,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: pnpm i --no-optional --no-frozen-lockfile
|
||||
- run: wget -q -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
|
||||
- run: node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --output-dir temp/test/mods
|
||||
- run: node packages/lib/build_mod --source-dir packages/host/lua/clusterio_lib --output-dir temp/test/mods
|
||||
- run: wget -q -O temp/test/mods/subspace_storage_1.99.8.zip -L https://github.com/clusterio/subspace_storage/releases/download/1.99.6/subspace_storage_1.99.8.zip
|
||||
- name: Run tests
|
||||
if: ${{ matrix.node-version != '12.x' }}
|
||||
|
||||
+2
-2
@@ -35,9 +35,9 @@ Factorio*
|
||||
/mods
|
||||
/database
|
||||
/plugin-list.json
|
||||
/config-master.json
|
||||
/config-controller.json
|
||||
/config-control.json
|
||||
/config-slave.json
|
||||
/config-host.json
|
||||
pnpm-lock.yaml
|
||||
mod_cache.json
|
||||
/static/
|
||||
|
||||
+22
-21
@@ -8,11 +8,11 @@
|
||||
Removed the Hotpatch scenario and the depency on it for getting code into the game.
|
||||
Added a save patcher than runs before starting up Factorio that patches in lua modules based on the event_loader lib into the savegame.
|
||||
Regular freeplay games can now be used with Clusterio and will be compatible without having to convert them to Hotpatch.
|
||||
- Daemonized slaves.
|
||||
Slaves now have the ability to run multiple Factorio instances, starting and stopping them individually.
|
||||
To manage this the local command line interface has been replaced with a remote interface on the master server that can be accessed through the clusterioctl cli tool.
|
||||
- Rewritten the communication between slaves and the master from scratch.
|
||||
The new system is based on a WebSocket connection between the slaves and the master server and provides efficient validated bi-directonal communication.
|
||||
- Daemonized hosts.
|
||||
Hosts now have the ability to run multiple Factorio instances, starting and stopping them individually.
|
||||
To manage this the local command line interface has been replaced with a remote interface on the controller that can be accessed through the clusterioctl cli tool.
|
||||
- Rewritten the communication between hosts and the controller from scratch.
|
||||
The new system is based on a WebSocket connection between the hosts and the controller and provides efficient validated bi-directonal communication.
|
||||
- Rewritten the plugin system from scratch.
|
||||
Plugins now inherit from a base class and use the same WebSocket connection Clusterio uses to communicate.
|
||||
- New configuration system with support for initializing configs when needed, modifying config entries using built-in commands, and updating instance configuration remotely.
|
||||
@@ -24,9 +24,9 @@
|
||||
### Features
|
||||
|
||||
- Added export of pollution statitics.
|
||||
- Reconnection logic that esures no data is dropped talking to the master server provided the session can be resumed.
|
||||
- Reconnection logic that esures no data is dropped talking to the controller provided the session can be resumed.
|
||||
- Added support for having multiple Factorio installs and selecting which version to use on a per instance basis.
|
||||
- Added config to specify which path the master interface is accessed under, allowing it to be proxied behind web-server.
|
||||
- Added config to specify which path the controller interface is accessed under, allowing it to be proxied behind web-server.
|
||||
- Added WebSocket usage statistics.
|
||||
- Added commands sent statistic.
|
||||
- Added Factorio server CPU and memory usage statistic.
|
||||
@@ -37,18 +37,18 @@
|
||||
Previously this was handled by the playerManager plugin.
|
||||
- Added stripping of long paths in the Factorio server log.
|
||||
- Added option to configure maximum number of commands to send in parallel.
|
||||
- Added option to auto start instances on slave startup.
|
||||
- Added metric mapping of slave and instance ids to their names, allowing them to be displayed by name in queries that join with the mapping.
|
||||
- Added option to auto start instances on host startup.
|
||||
- Added metric mapping of host and instance ids to their names, allowing them to be displayed by name in queries that join with the mapping.
|
||||
- Added option to configure timeout for exported Prometheus metrics.
|
||||
- Added option to set address to bind HTTP(S) port to.
|
||||
- Added an overview of the installed plugins to the web interface.
|
||||
- Added clusterioctl command to list plugins on the master server.
|
||||
- Added clusterioctl command to list plugins on the controller.
|
||||
- Added Player Auth plugin for logging in to the web interface by proving the ability to log in to a server as a given user.
|
||||
- Added Node.js based installer to simplify setting up a cluster.
|
||||
|
||||
### Changes
|
||||
|
||||
- Added error handling during master startup.
|
||||
- Added error handling during controller startup.
|
||||
- Factorio game and rcon port now defaults to a random port above 49151.
|
||||
- Removed unimplemented mods update command.
|
||||
- Fixed rcon password being generated with Math.random().
|
||||
@@ -69,15 +69,16 @@
|
||||
- Removed factorio_version from config.
|
||||
The version installed is auto detected and used instead.
|
||||
- Removed the playerManager specific command CLI tools/delete_player.js.
|
||||
- Creating an instance, assigning it to a slave, creating a save for an instance and starting an instance is now four separate commands.
|
||||
- Removed oddball limits to slaves.json size.
|
||||
- Moved slave specific and instance specific configuration into their own configuration files.
|
||||
- Creating an instance, assigning it to a host, creating a save for an instance and starting an instance is now four separate commands.
|
||||
- Removed oddball limits to hosts.json size.
|
||||
- Moved host specific and instance specific configuration into their own configuration files.
|
||||
- Removed unused binary option from plugin config.
|
||||
- Removed info and shout command from globalChat plugin - Removed mirrorAllChat and enableCrossServerShout configuration options for globalChat plugin.
|
||||
- Removed UPSdisplay plugin.
|
||||
UPS statistics is exported by the statistics exporter plugin.
|
||||
- Master server now defaults to hosting on https on port 8443.
|
||||
- Renamed client to slave.
|
||||
- Controller now defaults to hosting on https on port 8443.
|
||||
- Renamed master to controller.
|
||||
- Renamed slave/client to host.
|
||||
- Removed rotation of factorio-current.log files.
|
||||
|
||||
### Breaking Changes
|
||||
@@ -100,7 +101,7 @@
|
||||
- Removed the remoteCommands plugin and the old runCommand interface.
|
||||
Breaks playerManager and external tools sending commands.
|
||||
- Removed broken serverManager plugin.
|
||||
- Removed fields info, time, rconPort, rconPassword, serverPort, unique, mods, instanceName, playerCount, mac, and meta from the slaves in the slave database.
|
||||
- Removed fields info, time, rconPort, rconPassword, serverPort, unique, mods, instanceName, playerCount, mac, and meta from the hosts in the host database.
|
||||
- Removed getInstanceName and lib/clusterTools.
|
||||
Breaks playerManager, and discordChat.
|
||||
- Removed the /api/rconPasswords, and /api/slaves endpoints.
|
||||
@@ -109,7 +110,7 @@
|
||||
Breaks researchSync, and UPSdisplay.
|
||||
- Removed the hello event from the socket connection handshake.
|
||||
Breaks playerManager, trainTeleports, serverSelect, and discordChat
|
||||
- Changed the format of database/slaves.json.
|
||||
- Changed the format of database/hosts.json.
|
||||
- Removed the output file subscription system.
|
||||
Breaks inventoryImports, playerManager, trainTeleports, serverSelect and researchSync.
|
||||
- Removed the factorioOutput hook from instance plugins.
|
||||
@@ -134,13 +135,13 @@
|
||||
- Removed msBetweenCommands config option.
|
||||
The RCON is instead limited to 5 concurrent commands.
|
||||
- Removed allowRemoteCommandExecution config option.
|
||||
Remote commands are always allowed with the move to master managing slaves/instances.
|
||||
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line arguments from the master server.
|
||||
Remote commands are always allowed with the move to controller managing hosts/instances.
|
||||
- Removed `--databaseDirectory`, `--controllerPort`, and `--sslPort` command line arguments from the controller.
|
||||
- Implemented a new config system that replaces the old.
|
||||
Breaks all plugins.
|
||||
- Removed usage of socket.io entirely in favor of a plain WebSocket connection.
|
||||
- Renamed clusterioMod plugin to subspace_storage.
|
||||
- Master no longer creates secret-api-token.txt.
|
||||
- Controller no longer creates secret-api-token.txt.
|
||||
- Removed automatic creation of self-signed TLS certificate.
|
||||
- Node.js versions below 12 are no longer supported.
|
||||
|
||||
|
||||
+2
-2
@@ -20,12 +20,12 @@ RUN pnpm install
|
||||
# Install plugins. This is intended as a reasonable default, enabling plugins to make for fun gameplay.
|
||||
# If you want a different set of plugins, consider using this as the base image for your own.
|
||||
#RUN pnpm install @clusterio/plugin-subspace_storage
|
||||
#RUN npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
|
||||
#RUN npx clusteriocontroller plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
COPY --from=subspace_storage_builder /subspace_storage/dist/ /clusterio/mods/
|
||||
|
||||
# Build Lua library mod
|
||||
RUN node packages/lib/build_mod --build --source-dir packages/slave/lua/clusterio_lib \
|
||||
RUN node packages/lib/build_mod --build --source-dir packages/host/lua/clusterio_lib \
|
||||
&& mv dist/* mods/ \
|
||||
&& mkdir temp \
|
||||
&& mkdir temp/test \
|
||||
|
||||
@@ -33,11 +33,11 @@ If you are starting a new cluster, it's highly recommended to use the 2.0 alpha.
|
||||
* [Windows setup](#windows-setup)
|
||||
* [MacOS setup](#macos-setup)
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Configure Master Server](#configure-master-server)
|
||||
* [Configure Controller](#configure-controller)
|
||||
* [Managing Factorio mods](#managing-factorio-mods)
|
||||
* [Setting up shared storage](#setting-up-shared-storage)
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Setting up remote slaves](#setting-up-remote-slaves)
|
||||
* [Setting up remote hosts](#setting-up-remote-hosts)
|
||||
* [Setting up clusterioctl](#setting-up-clusterioctl)
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
@@ -71,7 +71,7 @@ These are the plugins supported and maintained by the Clusterio developers:
|
||||
|
||||
- [Global Chat](/plugins/global_chat/README.md): share the in-game chat between servers.
|
||||
- [Research Sync](/plugins/research_sync/README.md): synchronize research progress and technologies unlocked between servers.
|
||||
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the master server.
|
||||
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the controller.
|
||||
- [Subspace Storage](https://github.com/clusterio/subspace_storage): Provide shared storage that can transport items between servers via teleport chests.
|
||||
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
|
||||
- [Inventory Sync](/plugins/inventory_sync/README.md): Synchronizes your inventory between servers.
|
||||
@@ -126,9 +126,9 @@ Clusterio has *very* limited support for using docker.
|
||||
|
||||
sudo docker build -t clusterio --no-cache --force-rm clusterio
|
||||
|
||||
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
|
||||
sudo docker run --name controller -e MODE=controller -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
sudo docker run --name slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/clusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
|
||||
sudo docker run --name host -e MODE=host -e INSTANCE=world1 -v /srv/clusterio/instances:/clusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
The -v flag is used to specify the instance directory.
|
||||
Your instances (save files etc) will be stored there.
|
||||
@@ -173,42 +173,42 @@ First install the package the plugin is provided by via npm, for example
|
||||
|
||||
Then tell clusterio that this plugin exists by adding it as a plugin.
|
||||
|
||||
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by master, slave and ctl.
|
||||
If you have slaves or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them.
|
||||
The clusteriomaster, clusterioslave and clusterioctl commands has the plugin sub-command so you do not need to install clusteriomaster to add plugins, instead use the clusterio command you have available.
|
||||
This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by controller, host and ctl.
|
||||
If you have hosts or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them.
|
||||
The clusteriocontroller, clusteriohost and clusterioctl commands has the plugin sub-command so you do not need to install clusteriocontroller to add plugins, instead use the clusterio command you have available.
|
||||
|
||||
For development purposes the `plugin add` command supports adding plugins by the absolute path to them, or a relative path that must start with either . or .. (which will then be resolved to an absolute path).
|
||||
|
||||
|
||||
## Configure Master Server
|
||||
## Configure Controller
|
||||
|
||||
By default the master server will listen for HTTP on port 8080.
|
||||
By default the controller will listen for HTTP on port 8080.
|
||||
You can change the port used with the command
|
||||
|
||||
npx clusteriomaster config set master.http_port 1234
|
||||
npx clusteriocontroller config set controller.http_port 1234
|
||||
|
||||
When changing the port you will also need to change the address slaves connect with.
|
||||
When changing the port you will also need to change the address hosts connect with.
|
||||
For the standalone installation mode you can use
|
||||
|
||||
npx clusterioslave config set slave.master_url http://localhost:1234/
|
||||
npx clusteriohost config set host.controller_url http://localhost:1234/
|
||||
|
||||
If you plan to make your cluster available externally set the address
|
||||
that it will be accessible under with, for example
|
||||
|
||||
npx clusteriomaster config set master.external_address http://203.0.113.4:1234/
|
||||
npx clusteriocontroller config set controller.external_address http://203.0.113.4:1234/
|
||||
|
||||
Change the url to reflect the IP, protocol, and port the master server is accessible under, dns names are also supported.
|
||||
If you're planning on making the master server accessible on the internet it's recommended to set up TLS, see the [Setting Up TLS](/docs/setting-up-tls.md) document for more details.
|
||||
Change the url to reflect the IP, protocol, and port the controller is accessible under, dns names are also supported.
|
||||
If you're planning on making the controller accessible on the internet it's recommended to set up TLS, see the [Setting Up TLS](/docs/setting-up-tls.md) document for more details.
|
||||
|
||||
You can list the config of the master server with the `npx clusteriomaster config list` command.
|
||||
See the [readme for @clusterio/master](/packages/master/README.md) for more information.
|
||||
You can list the config of the controller with the `npx clusteriocontroller config list` command.
|
||||
See the [readme for @clusterio/controller](/packages/controller/README.md) for more information.
|
||||
|
||||
|
||||
## Managing Factorio mods
|
||||
|
||||
Clusterio currently only provides a manual method for running mods on servers by copying them either to each instance's individual `mods` folder, or by copying them into a slave installation's `sharedMods` folder which will link/copy them to the `mods` folder of each instance on that slave if the file doesn't already exist in the `mods` folder for that instance.
|
||||
Clusterio currently only provides a manual method for running mods on servers by copying them either to each instance's individual `mods` folder, or by copying them into a host installation's `sharedMods` folder which will link/copy them to the `mods` folder of each instance on that host if the file doesn't already exist in the `mods` folder for that instance.
|
||||
Mods are copied from `sharedMods` on Windows and symlinked on Linux, this means that when mods are removed from `sharedMods` the mods will also be removed from the instance's `mods` folder on Linux if it got there by being linked by Clusterio, but not on Windows.
|
||||
|
||||
For managing the mod settings, the `mod-settings.dat` and `mod-list.json` files will also be linked/copied over if present in `sharedMods`, but since Factorio creates these files automatically it might be necessary to delete them from each instance's `mods` folder for changes to them in `sharedMods` to take effect.
|
||||
@@ -218,51 +218,51 @@ Better remote management of mods is a planned feature.
|
||||
|
||||
## Setting up shared storage
|
||||
|
||||
The chests teleporting items to and from the shared storage on the master server has been moved into the Subspace Storage mod and plugin.
|
||||
To get this to work you will need to install the Subspace Storage plugin and copy the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) into the `sharedMods` folder on each slave installation.
|
||||
The chests teleporting items to and from the shared storage on the controller has been moved into the Subspace Storage mod and plugin.
|
||||
To get this to work you will need to install the Subspace Storage plugin and copy the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) into the `sharedMods` folder on each host installation.
|
||||
|
||||
Do not install the [Clusterio mod](https://mods.factorio.com/mod/clusterio), this was for the old 1.2.x version of Clusterio and is not compatible with Clusterio 2.0.
|
||||
|
||||
|
||||
## Running Clusterio
|
||||
|
||||
After completing the installation start up the master server and at least one slave separately.
|
||||
The installer provides the `run-master` and `run-slave` scripts to make this simple.
|
||||
Once the master process is running you can log into the web interface which is hosted by default on http://localhost:8080/ (adjust the port number if you changed it), use the admin authentication token provided from the installation to log in.
|
||||
After completing the installation start up the controller and at least one host separately.
|
||||
The installer provides the `run-controller` and `run-host` scripts to make this simple.
|
||||
Once the controller process is running you can log into the web interface which is hosted by default on http://localhost:8080/ (adjust the port number if you changed it), use the admin authentication token provided from the installation to log in.
|
||||
|
||||
The basics of setting up a Factorio server from the web interface is to create an instance, assign it to a slave and then click start.
|
||||
The basics of setting up a Factorio server from the web interface is to create an instance, assign it to a host and then click start.
|
||||
|
||||
### Running via Systemd
|
||||
|
||||
The install script creates systemd service scripts for clusteriomaster and clusterioslave (if applicable) to start up as the user than ran the installer.
|
||||
The install script creates systemd service scripts for clusteriocontroller and clusteriohost (if applicable) to start up as the user than ran the installer.
|
||||
If you have copied these files over to `/etc/systemd/system/` then you can startup Clusterio as a background service using:
|
||||
|
||||
#### To start as a service run:
|
||||
|
||||
sudo systemctl start clusteriomaster # for the master server
|
||||
sudo systemctl start clusterioslave # for each server hosting a slave
|
||||
sudo systemctl start clusteriocontroller # for the controller
|
||||
sudo systemctl start clusteriohost # for each physical server intended to host Factorio servers
|
||||
|
||||
#### To automatically get it to start on boot:
|
||||
|
||||
sudo systemctl enable clusteriomaster # for the master server
|
||||
sudo systemctl enable clusterioslave # for each server hosting a slave
|
||||
sudo systemctl enable clusteriocontroller # for the controller
|
||||
sudo systemctl enable clusteriohost # for each physical server intended to host Factorio servers
|
||||
|
||||
|
||||
## Setting up remote slaves
|
||||
## Setting up remote hosts
|
||||
|
||||
Run the installer as described in the installation section and choose "Slave only" as the operating mode to install, it'll ask for a master URL and an authentication token.
|
||||
The URL is the same as what is needed to connect to the web interface, and the authentication token can be generated on the Slaves page in the web interface.
|
||||
Once you start up the slave it should show up in the Slaves list and be available for assigning and running instances on.
|
||||
Run the installer as described in the installation section and choose "Host only" as the operating mode to install, it'll ask for a controller URL and an authentication token.
|
||||
The URL is the same as what is needed to connect to the web interface, and the authentication token can be generated on the Hosts page in the web interface.
|
||||
Once you start up the host it should show up in the Hosts list and be available for assigning and running instances on.
|
||||
|
||||
|
||||
## Setting up clusterioctl
|
||||
|
||||
There's a command line interface available for Clusterio which is installed separately with the same installer as for the master and slave.
|
||||
There's a command line interface available for Clusterio which is installed separately with the same installer as for the controller and host.
|
||||
Run the installer as described in the installation section and choose "Ctl only" as the operating mode to install, you can do this in the same directory as you have installed other Clusterio component(s) to.
|
||||
The installer will ask for a master URL and an authentication token, these are the same as you would use to connect to the web interface.
|
||||
The installer will ask for a controller URL and an authentication token, these are the same as you would use to connect to the web interface.
|
||||
If you want to use a different user for the command line interface, you can generate an authentication token for an existing user with
|
||||
|
||||
npx clusteriomaster bootstrap generate-user-token <username>
|
||||
npx clusteriocontroller bootstrap generate-user-token <username>
|
||||
|
||||
|
||||
## Common problems
|
||||
@@ -276,7 +276,7 @@ According to [this link](https://askubuntu.com/questions/839520/open-port-443-fo
|
||||
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
|
||||
|
||||
### Port forwarding doesn't work on the master server when running under WSL
|
||||
### Port forwarding doesn't work on the controller when running under WSL
|
||||
|
||||
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip.
|
||||
This is also true when you correctly port-forwarded the correct ports.
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
master:
|
||||
controller:
|
||||
build: .
|
||||
command: npx clusteriomaster run
|
||||
slave:
|
||||
command: npx clusteriocontroller run
|
||||
host:
|
||||
build: .
|
||||
command: npx clusterioslave run
|
||||
command: npx clusteriohost run
|
||||
@@ -11,8 +11,8 @@ Links represents one side of a two way communication pipe, connectors deal with
|
||||
The Link class represents one side of a two way communication channel where the actual communication is done by a connector associated with the link.
|
||||
The link also knows what the endpoint types it's connected to are via the source and target attributes.
|
||||
|
||||
The endpoint types currently implemented are master, slave, instance, and control.
|
||||
Although slave and instances run in the same Node.js program they use virtual links to communicate between them in order to simplify the communication architecture.
|
||||
The endpoint types currently implemented are controller, host, instance, and control.
|
||||
Although host and instances run in the same Node.js program they use virtual links to communicate between them in order to simplify the communication architecture.
|
||||
|
||||
Upon creation the link will register with the connector in order to receive and process messages from it.
|
||||
The messages received are validated using the validator registered for the type of the message.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
<sub>This document describes the implementation and inner working of the configuration system in Clusterio, and unless you're developing Clusterio it's probably not going to be very useful for you.</sub>
|
||||
|
||||
Clusterio uses a group based configuration system to manage settings for the master server, instances, slaves, and the clusterioctl utility.
|
||||
Plugins get their own config group for the master server and instances which they can add their own fields to.
|
||||
Clusterio uses a group based configuration system to manage settings for the controller, instances, hosts, and the clusterioctl utility.
|
||||
Plugins get their own config group for the controller and instances which they can add their own fields to.
|
||||
The fields are defined early in the startup, and the groups and their fields for disabled plugins will still be created.
|
||||
|
||||
The built-in config groups and their fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config groups to the `MasterConfigGroup` and `InstanceConfigGroup` properties of the `info.js` export using subclasses of `PluginConfigGroup`.
|
||||
The built-in config groups and their fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config groups to the `ControllerConfigGroup` and `InstanceConfigGroup` properties of the `info.js` export using subclasses of `PluginConfigGroup`.
|
||||
The plugin group has the enabled field pre-defined on it, and it's used to enable/disable plugins.
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ The `define` method takes an object as argument with the following properties:
|
||||
|
||||
**access**:
|
||||
The locations this config field can be read and modified from.
|
||||
This is an array of strings which can contain the values `"master"`, `"slave"` and `"control"`.
|
||||
This is an array of strings which can contain the values `"controller"`, `"host"` and `"control"`.
|
||||
If a party is missing in this array then that party will not receive the value of this field when the config is shared, and will be unable to modify the field.
|
||||
Takes the value of `defaultAccess` set on the config group class if not passed.
|
||||
Note that instance config groups should have both `"master"` and `"slave"` for all of its fields or unexpected behaviour may occur.
|
||||
Note that instance config groups should have both `"controller"` and `"host"` for all of its fields or unexpected behaviour may occur.
|
||||
|
||||
**type**:
|
||||
The type of config value this field will support.
|
||||
@@ -73,6 +73,6 @@ Values for all config fields are created when the config is initialized, as well
|
||||
Once a field has a value it is never automatically removed, even if the field or group it was part of no longer exists.
|
||||
This ensures that plugin configuration is not lost should the server be started up without the plugins that defined the configuration installed.
|
||||
|
||||
For instance configs, they are kept on the master server and serialized and sent over to the relevant slaves on startup.
|
||||
This makes it possible to edit instance configs when the slave the instance is on is offline.
|
||||
A copy is also stored in the instance directory on the slave, and this config will be deliverd to the master server on slave startup, which enables instances to be copied between clusters as long as their IDs are unique.
|
||||
For instance configs, they are kept on the controller and serialized and sent over to the relevant hosts on startup.
|
||||
This makes it possible to edit instance configs when the host the instance is on is offline.
|
||||
A copy is also stored in the instance directory on the host, and this config will be deliverd to the controller on host startup, which enables instances to be copied between clusters as long as their IDs are unique.
|
||||
+67
-67
@@ -1,9 +1,9 @@
|
||||
# Configuration
|
||||
|
||||
|
||||
## Master Configuration
|
||||
## Controller Configuration
|
||||
|
||||
### master.name
|
||||
### controller.name
|
||||
|
||||
Name of the cluster.
|
||||
Used to distinguish it from other clusters and is prepended to the name of servers in the server list.
|
||||
@@ -11,37 +11,37 @@ Used to distinguish it from other clusters and is prepended to the name of serve
|
||||
Defaluts to "Your Cluster".
|
||||
|
||||
|
||||
### master.database_directory
|
||||
### controller.database_directory
|
||||
|
||||
Directory used to store cluster data to.
|
||||
Needs to be writeable by the master server.
|
||||
Needs to be writeable by the controller.
|
||||
This is also used by plugins to store its data.
|
||||
|
||||
Defaults to "database".
|
||||
|
||||
|
||||
### master.http_port
|
||||
### controller.http_port
|
||||
|
||||
Port to host HTTP server on.
|
||||
If set to null no HTTP server will be exposed.
|
||||
At least one of this and master.https_port needs to be set to a port.
|
||||
At least one of this and controller.https_port needs to be set to a port.
|
||||
|
||||
Defaults to 8080.
|
||||
|
||||
|
||||
### master.https_port
|
||||
### controller.https_port
|
||||
|
||||
Port to host HTTPS server on.
|
||||
Uses master.tls_certificate and master.tls_private_key as the certificate and private key for the HTTPS server.
|
||||
Uses controller.tls_certificate and controller.tls_private_key as the certificate and private key for the HTTPS server.
|
||||
If set to null no HTTPS server will be exposed.
|
||||
At least one of this and master.http_port needs to be set to a port.
|
||||
At least one of this and controller.http_port needs to be set to a port.
|
||||
|
||||
See the [Setting up TLS](/docs/setting-up-tls.md) document for a guide to setting up HTTPS with Clusterio.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### master.bind_address
|
||||
### controller.bind_address
|
||||
|
||||
IP address to bind the HTTP and HTTPS ports on.
|
||||
Useful to limit which interface to accept connections from.
|
||||
@@ -51,39 +51,39 @@ interfaces.
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### master.external_address
|
||||
### controller.external_address
|
||||
|
||||
External address the master server is accessible on.
|
||||
Currently only used for `clusteriomaster bootstrap create-ctl-config` in order to give the right url to connect to.
|
||||
External address the controller is accessible on.
|
||||
Currently only used for `clusteriocontroller bootstrap create-ctl-config` in order to give the right url to connect to.
|
||||
This should be a full URL ending with a /.
|
||||
|
||||
Defaults to null meaning assume localhost.
|
||||
|
||||
|
||||
### master.tls_certificate
|
||||
### controller.tls_certificate
|
||||
|
||||
Path to TLS certificate to use for the HTTPS server when master.https_port is configured.
|
||||
Path to TLS certificate to use for the HTTPS server when controller.https_port is configured.
|
||||
|
||||
Defaults to "database/certificates/cert.crt".
|
||||
|
||||
|
||||
### master.tls_private_key
|
||||
### controller.tls_private_key
|
||||
|
||||
Path to TLS private key to use for the HTTPS server when master.https_port is configured.
|
||||
Path to TLS private key to use for the HTTPS server when controller.https_port is configured.
|
||||
|
||||
Defaults to "database/certificates/cert.key".
|
||||
|
||||
|
||||
### master.auth_secret
|
||||
### controller.auth_secret
|
||||
|
||||
Base64 encoded authentication secret used to verify tokens issued to slaves, users, and sessions.
|
||||
Base64 encoded authentication secret used to verify tokens issued to hosts, users, and sessions.
|
||||
This should contain 256 bytes of random data, and if changed will cause all tokens become invalid.
|
||||
Keep this secret if leaked an attacker could easily compromise the cluster.
|
||||
|
||||
Defaults to null which means generate a secure secret on startup.
|
||||
|
||||
|
||||
### master.heartbeat_interval
|
||||
### controller.heartbeat_interval
|
||||
|
||||
Interval in seconds heartbeats are sent out at for WebSocket connections.
|
||||
If a WebSocket connection hasn't received a heartbeat in 2 times the heartbeat interval it will be considered stale and closed.
|
||||
@@ -92,33 +92,33 @@ A lower value means less time between connections going stale and them being clo
|
||||
Defaults to 15.
|
||||
|
||||
|
||||
### master.session_timeout
|
||||
### controller.session_timeout
|
||||
|
||||
Time in seconds from the connection is closed following a heartbeat timeout until the session is invalidated and data loss occurs.
|
||||
A higher timeout gives more time for a client to recconect and resume an active session over a connection that was dropped, but also leads to stale connections taking longer to clear out.
|
||||
In the case of the master or client crashing the data loss is unavoidable.
|
||||
In the case of the controller or client crashing the data loss is unavoidable.
|
||||
|
||||
Value should not be less than the configured `max_reconnect_delay` of the clients and it should be greater than 2 times `master.heartbeat_interval`.
|
||||
Value should not be less than the configured `max_reconnect_delay` of the clients and it should be greater than 2 times `controller.heartbeat_interval`.
|
||||
Defaults to 60.
|
||||
|
||||
|
||||
### master.metrics_timeout
|
||||
### controller.metrics_timeout
|
||||
|
||||
Timeout in seconds before a call to gather metrics from a slave times out.
|
||||
Timeout in seconds before a call to gather metrics from a host times out.
|
||||
This should be less than both the timeout and collection interval configured for the collection job in Prometheus.
|
||||
|
||||
Defaults to 8.
|
||||
|
||||
|
||||
### master.stream_timeout
|
||||
### controller.stream_timeout
|
||||
|
||||
Timeout in seconds for proxy streams through the master that has been created but is not yet sending data to be automatically closed.
|
||||
Timeout in seconds for proxy streams through the controller that has been created but is not yet sending data to be automatically closed.
|
||||
Streams that have started sending data before this timeout is not affected.
|
||||
|
||||
Defaults to 15.
|
||||
|
||||
|
||||
### master.default_role_id
|
||||
### controller.default_role_id
|
||||
|
||||
Role to automatically grant to new users.
|
||||
If null no role is granted.
|
||||
@@ -128,30 +128,30 @@ Defaults to 1 which correspond to the default Player role.
|
||||
|
||||
### <plugin_name>.load_plugin
|
||||
|
||||
Whether to load the plugin on the master server.
|
||||
Plugins not loaded on the master server will not be loaded on instances.
|
||||
Whether to load the plugin on the controller.
|
||||
Plugins not loaded on the controller will not be loaded on instances.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
## Slave Configuration
|
||||
## Host Configuration
|
||||
|
||||
### slave.name
|
||||
### host.name
|
||||
|
||||
Name of the slave.
|
||||
Shows up in slave lists and is used to reference this slave in the clusterioctl command line interface.
|
||||
Name of the host.
|
||||
Shows up in host lists and is used to reference this host in the clusterioctl command line interface.
|
||||
|
||||
Defaults to "New Slave".
|
||||
Defaults to "New Host".
|
||||
|
||||
|
||||
### slave.id
|
||||
### host.id
|
||||
|
||||
Immutable numeric id of the slave which uniquely identifies this slave in the cluster.
|
||||
Immutable numeric id of the host which uniquely identifies this host in the cluster.
|
||||
|
||||
Defaults to a random 31 bit number.
|
||||
|
||||
|
||||
### slave.factorio_directory
|
||||
### host.factorio_directory
|
||||
|
||||
Directory to look for the Factorio server(s) in.
|
||||
This can either point to a Factorio server directory, or to a directory containing multiple versions of the Factorio server.
|
||||
@@ -159,48 +159,48 @@ This can either point to a Factorio server directory, or to a directory containi
|
||||
Defaults to "factorio".
|
||||
|
||||
|
||||
### slave.instances_directory
|
||||
### host.instances_directory
|
||||
|
||||
Directory to store instances in.
|
||||
One sub-directory will be created in it for each instance assigned to this slave and these sub-directories will contain logs, saves, and various auto generated configuration files for the instances.
|
||||
One sub-directory will be created in it for each instance assigned to this host and these sub-directories will contain logs, saves, and various auto generated configuration files for the instances.
|
||||
|
||||
Defaults to "instances".
|
||||
|
||||
|
||||
### slave.master_url
|
||||
### host.controller_url
|
||||
|
||||
URL to connect to the master server to.
|
||||
URL to connect to the controller to.
|
||||
|
||||
Defaults to "http://localhost:8080/".
|
||||
|
||||
|
||||
### slave.master_token
|
||||
### host.controller_token
|
||||
|
||||
Access token used for authenticating with the master server.
|
||||
You can generate an access token with `clusterioctl slave generate-token --id <slave-id>`, or use the `clusterioctl slave create-config` to create a new slave config with the correct url and token in it.
|
||||
Access token used for authenticating with the controller.
|
||||
You can generate an access token with `clusterioctl host generate-token --id <host-id>`, or use the `clusterioctl host create-config` to create a new host config with the correct url and token in it.
|
||||
|
||||
Defaults to "enter token here".
|
||||
|
||||
|
||||
### slave.tls_ca
|
||||
### host.tls_ca
|
||||
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the master server.
|
||||
If you have a self signed certificate on the master server you will need to copy the certificate to your slaves and set it as the certificate authority with this option.
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the controller.
|
||||
If you have a self signed certificate on the controller you will need to copy the certificate to your hosts and set it as the certificate authority with this option.
|
||||
|
||||
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
|
||||
|
||||
|
||||
### slave.public_address
|
||||
### host.public_address
|
||||
|
||||
External address instances hosted on this slave can be accessed on.
|
||||
External address instances hosted on this host can be accessed on.
|
||||
This is used by plugins like Server Select to give the correct address to switch between instances in-game.
|
||||
|
||||
Defaults to "localhost".
|
||||
|
||||
|
||||
### slave.max_reconnect_delay
|
||||
### host.max_reconnect_delay
|
||||
|
||||
Maximum delay in seconds to wait after connection to the master server is dropped before attempting to reconnect to it.
|
||||
Maximum delay in seconds to wait after connection to the controller is dropped before attempting to reconnect to it.
|
||||
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all clients trying to reconnect at the same time.
|
||||
|
||||
Defaults to 60.
|
||||
@@ -222,19 +222,19 @@ Imutable numeric id of the instance which uniquely identifies this instance in t
|
||||
Defaluts to a random 31 bit integer.
|
||||
|
||||
|
||||
### instance.assigned_slave
|
||||
### instance.assigned_host
|
||||
|
||||
Slave this instance is assigned to.
|
||||
Host this instance is assigned to.
|
||||
To change this you need to use the separate assign instance interface.
|
||||
After being assigned to a slave the directory for the instance is created on the slave.
|
||||
After being assigned to a host the directory for the instance is created on the host.
|
||||
|
||||
Defaults to null meaning not assigned.
|
||||
|
||||
|
||||
### instance.auto_start
|
||||
|
||||
If enabled start this instance when the slave it is assigned to is started up.
|
||||
Does not affect start or stop of the instance while the slave is running.
|
||||
If enabled start this instance when the host it is assigned to is started up.
|
||||
Does not affect start or stop of the instance while the host is running.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
@@ -242,7 +242,7 @@ Defaults to false.
|
||||
### factorio.version
|
||||
|
||||
Version of Factorio to use for this instance.
|
||||
Can be a version like "1.0.0" or special string "latest" meaning the latest version of Factorio found on the slave.
|
||||
Can be a version like "1.0.0" or special string "latest" meaning the latest version of Factorio found on the host.
|
||||
|
||||
Defaults to "latest".
|
||||
|
||||
@@ -383,39 +383,39 @@ Defaults to 5.
|
||||
### <plugin_name>.load_plugin
|
||||
|
||||
Whether to load the given plugin on the instance.
|
||||
Note that plugins need to be loaded on the master server in order for them to be loaded on instances.
|
||||
Note that plugins need to be loaded on the controller in order for them to be loaded on instances.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
## Control Configuration
|
||||
|
||||
### control.master_url
|
||||
### control.controller_url
|
||||
|
||||
URL to connect to the master server to.
|
||||
URL to connect to the controller to.
|
||||
|
||||
Defaults to null meaning complain about it not being set and exit.
|
||||
|
||||
|
||||
### control.master_token
|
||||
### control.controller_token
|
||||
|
||||
Access token used for authenticating with the master server.
|
||||
You can generate an access token with `clusteriomaster bootstrap generate-user-token <username>`, or use the `clusteriomaster bootstrap create-ctl-config <username>` to create a new ctl config with the correct url and token in it.
|
||||
Access token used for authenticating with the controller.
|
||||
You can generate an access token with `clusteriocontroller bootstrap generate-user-token <username>`, or use the `clusteriocontroller bootstrap create-ctl-config <username>` to create a new ctl config with the correct url and token in it.
|
||||
|
||||
Defaults to null meaning complain about it not being set and exit.
|
||||
|
||||
|
||||
### control.tls_ca
|
||||
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the master server.
|
||||
If you have a self signed certificate on the master server you will need to copy the certificate to the computer you run clusterctl from and set it as the certificate authority with this option.
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the controller.
|
||||
If you have a self signed certificate on the controller you will need to copy the certificate to the computer you run clusterctl from and set it as the certificate authority with this option.
|
||||
|
||||
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
|
||||
|
||||
|
||||
### control.max_reconnect_delay
|
||||
|
||||
Max duration in seconds to wait before attempting to reconnect with the master server after the connection is dropped.
|
||||
Max duration in seconds to wait before attempting to reconnect with the controller after the connection is dropped.
|
||||
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all clients trying to reconnect at the same time.
|
||||
|
||||
Defaults to 60.
|
||||
@@ -68,17 +68,17 @@ The `plugins/` directory contains the core plugins for the project and can be ad
|
||||
|
||||
The documentation uses the binaries provided by the packages, but for the development environment you will need to call the packages directly:
|
||||
|
||||
npx clusteriomaster -> node packages/master
|
||||
npx clusterioslave -> node packages/slave
|
||||
npx clusteriocontroller -> node packages/controller
|
||||
npx clusteriohost -> node packages/host
|
||||
npx clusterioctl -> node packages/ctl
|
||||
|
||||
Note that to use `clusterioctl` you will have to create a control config first:
|
||||
|
||||
node packages/master bootstrap create-ctl-config [admin-user]
|
||||
node packages/controller bootstrap create-ctl-config [admin-user]
|
||||
|
||||
Once you've set up the cluster you can use `node packages/master run` to run the master server and `node packages/slave run` to run the slave which connects to the master server.
|
||||
Once you've set up the cluster you can use `node packages/controller run` to run the controller and `node packages/host run` to run the host which connects to the controller.
|
||||
|
||||
For web development on the master there are also the following flags:
|
||||
For web development on the controller there are also the following flags:
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
|
||||
@@ -86,7 +86,7 @@ For web development on the master there are also the following flags:
|
||||
|
||||
These options can be combined and multiple plugins can be actived for dev mode.
|
||||
Keep in mind if you're working on external plugins the `--dev-plugin` option is likely to only work if the plugin is placed inside the `external_plugins` directory and its dependencies are installed with pnpm as part of the clusterio repository workspace.
|
||||
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `pnpm run -r --if-present prepare` before you start the master without these flags again.
|
||||
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `pnpm run -r --if-present prepare` before you start the controller without these flags again.
|
||||
|
||||
|
||||
### Starting a Feature Branch
|
||||
|
||||
+23
-23
@@ -12,9 +12,9 @@ Events and requests are defined as instances of the `Request` and `Event` classe
|
||||
All messages sent and received are validated with JSON schemas to catch potential errors in sending messages and avoid having to write lots of validation code in message handlers.
|
||||
|
||||
Messages are sent by calling the `.send()` method of the message instance and passing a `Link` instance to send it over and the data to send.
|
||||
For the master server there's a `Link` instance for each slave and control connection stored in the `WsServer` class.
|
||||
For slaves the `Slave` instance is a link to the master server, and each `Instance` instance is an in-memory link to the `Slave` instance and has a corresponding `InstanceConnection` link stored on the `Slave` instance.
|
||||
For ctl and web_ui there's a `Control` instance that acts as the link to the master server.
|
||||
For the controller there's a `Link` instance for each host and control connection stored in the `WsServer` class.
|
||||
For hosts the `Host` instance is a link to the controller, and each `Instance` instance is an in-memory link to the `Host` instance and has a corresponding `InstanceConnection` link stored on the `Host` instance.
|
||||
For ctl and web_ui there's a `Control` instance that acts as the link to the controller.
|
||||
|
||||
Messages received invokes message handlers that were registered when the link was brought up.
|
||||
These handlers are named after the name of the property the message is stored under with `EventHandler` appended for events and `RequestHandler` appended for requests.
|
||||
@@ -24,7 +24,7 @@ The handlers are defined on the link class itself for core messages, and on the
|
||||
## Connection flow
|
||||
|
||||
Clients connect via WebSocket to the `/api/socket` endpoint and upon establishing connection to the server the connection is in the handshake phase and the server will send a `hello` message to the client.
|
||||
If the client has no previous session with the server it sends a `register_slave` or `register_control` message depending on what kind of client it is to the server.
|
||||
If the client has no previous session with the server it sends a `register_host` or `register_control` message depending on what kind of client it is to the server.
|
||||
If the register is valid then the server will respond with a `ready` message with a session token and the connection has left the handshake phase, otherwise the server will close the connection with a WebSocket close code indicating the kind of failure.
|
||||
|
||||
Upon leaving the handshake phase both parties should start sending heartbeat messages with the sequence number of the last received message.
|
||||
@@ -33,7 +33,7 @@ Messages sent over the link should be stored in a send buffer and only cleared f
|
||||
If the connection goes stale or is unexpectedly closed the client can reastablish the same session by connecting to the server again and sending a `resume` message after received the `hello` from the server.
|
||||
If the session resume is successful then the server will respond with a `continue` message and the connection is active again.
|
||||
The client should then re-send all messages it has in its send buffer that is greater than the `last_seq` number in the `continue` message.
|
||||
Otherwise the server will respond with `invalidate` and the client is expected to drop its send buffer start a new session with `register_slave` or `register_control`.
|
||||
Otherwise the server will respond with `invalidate` and the client is expected to drop its send buffer start a new session with `register_host` or `register_control`.
|
||||
|
||||

|
||||
|
||||
@@ -96,23 +96,23 @@ The following messages are the only valid messages that can be sent during the h
|
||||
|
||||
#### `hello`
|
||||
|
||||
Sent by the master server when the WebSocket connection has been opened.
|
||||
Sent by the controller when the WebSocket connection has been opened.
|
||||
|
||||
- `version: string` - The version of the master, e.g. "2.0.0".
|
||||
- `version: string` - The version of the controller, e.g. "2.0.0".
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are loaded on the master server.
|
||||
Object mapping plugin names to plugin version for plugins that are loaded on the controller.
|
||||
|
||||
#### `register_slave`
|
||||
#### `register_host`
|
||||
|
||||
Slave handshake for establishing a new connection session.
|
||||
Host handshake for establishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `agent: string` - Human readable string of the software connecting.
|
||||
- `version: string` - The protocol version of the slave, e.g. "2.0.0".
|
||||
- `name: string` - Name of the slave.
|
||||
- `id: integer` - ID of the slave.
|
||||
- `version: string` - The protocol version of the host, e.g. "2.0.0".
|
||||
- `name: string` - Name of the host.
|
||||
- `id: integer` - ID of the host.
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are available on the slave.
|
||||
Object mapping plugin names to plugin version for plugins that are available on the host.
|
||||
|
||||
#### `register_control`
|
||||
|
||||
@@ -131,12 +131,12 @@ Handshake for resuming an existing connection session where the connection had d
|
||||
|
||||
#### `invalidate`
|
||||
|
||||
Sent by the master server in reply to a resume for a session that cannot be resumed.
|
||||
Sent by the controller in reply to a resume for a session that cannot be resumed.
|
||||
The client should start a new session with a register\_\* message in response.
|
||||
|
||||
#### `continue`
|
||||
|
||||
Sent by the master server to signal resuming the session was successful.
|
||||
Sent by the controller to signal resuming the session was successful.
|
||||
|
||||
- `last_seq: integer | null` - Last message seq received by the server.
|
||||
- `session_timeout: integer` - Session timeout in seconds.
|
||||
@@ -144,7 +144,7 @@ Sent by the master server to signal resuming the session was successful.
|
||||
|
||||
#### `ready`
|
||||
|
||||
Sent by the master server to signal the connection has been established with a new session.
|
||||
Sent by the controller to signal the connection has been established with a new session.
|
||||
|
||||
- `session_token: string` - Token to resume the session with.
|
||||
- `session_timeout: integer` - Session timeout in seconds.
|
||||
@@ -159,7 +159,7 @@ The heartbeat should contain the `seq` of the last received message, and receivi
|
||||
|
||||
#### `heartbeat`
|
||||
|
||||
Sent by both the master server and clients at the interval given in the continue/ready messages.
|
||||
Sent by both the controller and clients at the interval given in the continue/ready messages.
|
||||
|
||||
- `last_seq: integer` - Last message seq received over the link.
|
||||
|
||||
@@ -235,15 +235,15 @@ Server hello after opening the WebSocket.
|
||||
}
|
||||
```
|
||||
|
||||
Slave registering a new connection session.
|
||||
Host registering a new connection session.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "register_slave",
|
||||
"type": "register_host",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"agent": "Clusterio Slave",
|
||||
"agent": "Clusterio Host",
|
||||
"version": "2.0.0",
|
||||
"name": "Foo",
|
||||
"id": 123
|
||||
@@ -251,7 +251,7 @@ Slave registering a new connection session.
|
||||
}
|
||||
```
|
||||
|
||||
Master server reply signaling the session is established and ready.
|
||||
Controller reply signaling the session is established and ready.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -265,4 +265,4 @@ Master server reply signaling the session is established and ready.
|
||||
}
|
||||
```
|
||||
|
||||
Slave and master can now send events and requests over the connection.
|
||||
Host and controller can now send events and requests over the connection.
|
||||
@@ -11,6 +11,6 @@ This can be disabled with the `factorio.enable_save_patching` config on a per in
|
||||
|
||||
## User Accounts
|
||||
|
||||
The master server creates and stores accounts for all Factorio users that log into instances under its control.
|
||||
The controller creates and stores accounts for all Factorio users that log into instances under its control.
|
||||
These accounts are used to store per player data and authorize management of the cluster.
|
||||
By default users accounts are assigned a Player role that gives a limited read access to the cluster resources, and admins of the cluster can be assigned a Cluster Admin role to get access to manage it.
|
||||
+4
-4
@@ -11,10 +11,10 @@
|
||||
},
|
||||
"source": {
|
||||
"include": [
|
||||
"packages/master/src/",
|
||||
"packages/master/master.js",
|
||||
"packages/slave/src/",
|
||||
"packages/slave/slave.js",
|
||||
"packages/controller/src/",
|
||||
"packages/controller/controller.js",
|
||||
"packages/host/src/",
|
||||
"packages/host/host.js",
|
||||
"packages/ctl/src/",
|
||||
"packages/ctl/ctl.js",
|
||||
"packages/lib/",
|
||||
|
||||
+14
-14
@@ -1,32 +1,32 @@
|
||||
# Managing a Cluster
|
||||
|
||||
Clusterio clusters are managed through the master server by using the `clusterioctl` command line interface which is invoked by running `npx clusterioctl <command>` in the clusterio directory.
|
||||
Clusterio clusters are managed through the controller by using the `clusterioctl` command line interface which is invoked by running `npx clusterioctl <command>` in the clusterio directory.
|
||||
This document uses the shorthand `ctl> foo` to indicate `npx clusterioctl foo` should be executed.
|
||||
Mandatory parameters are shown in `<angles bracket>` and optional pameters are in `[square brackets]`.
|
||||
|
||||
Before `clusterioctl` can be used it needs to be configured for the cluster it will connect to.
|
||||
The easiest way to do this is to run `npx clusteriomaster bootstrap create-ctl-config <username>` on the master server, which creates the necessary `config-control.json` for managing the cluster as the given user.
|
||||
The easiest way to do this is to run `npx clusteriocontroller bootstrap create-ctl-config <username>` on the controller, which creates the necessary `config-control.json` for managing the cluster as the given user.
|
||||
|
||||
|
||||
## Slaves
|
||||
## Hosts
|
||||
|
||||
### List slaves
|
||||
### List hosts
|
||||
|
||||
ctl> slave list
|
||||
ctl> host list
|
||||
|
||||
Lists all slaves.
|
||||
Lists all hosts.
|
||||
|
||||
### Generate token
|
||||
|
||||
ctl> slave generate-token [--id <slave id>]
|
||||
ctl> host generate-token [--id <host id>]
|
||||
|
||||
Generate a token for a slave. `--id` is the slave id.
|
||||
Generate a token for a host. `--id` is the host id.
|
||||
|
||||
### Create config
|
||||
|
||||
ctl> slave create-config [--id <slave id>] [--name <slave name>] [--generate-token <bool>] [--output <path to output>]
|
||||
ctl> host create-config [--id <host id>] [--name <host name>] [--generate-token <bool>] [--output <path to output>]
|
||||
|
||||
Create a slave config. `--id` id the slave id. `--name` is the slave name. `--generate-token` decides whether to also generate a token, defaulting to `0`. `--output` is the filename for the output of the config; Default is "config-slave.json", "-" for stdout.
|
||||
Create a host config. `--id` id the host id. `--name` is the host name. `--generate-token` decides whether to also generate a token, defaulting to `0`. `--output` is the filename for the output of the config; Default is "config-host.json", "-" for stdout.
|
||||
|
||||
## Instances
|
||||
|
||||
@@ -85,11 +85,11 @@ To be written.
|
||||
|
||||
Edit instance config in a text editor. Once file is saved and editor has been exited, config values will be read back in. `instance` is the instance to edit, `editor` is the editor to use. If `editor` is not given, clusterioctl will read from the `EDITOR` or `VISUAL` enviroment variables.
|
||||
|
||||
### Assign instance to slave
|
||||
### Assign instance to host
|
||||
|
||||
ctl> instace assign <instance> [slave]
|
||||
ctl> instace assign <instance> [host]
|
||||
|
||||
Assigns instance to slave. `instance` is instance to assign, `slave` is slave to assign to. If `slave` is not provided, instance will be unassigned.
|
||||
Assigns instance to host. `instance` is instance to assign, `host` is host to assign to. If `host` is not provided, instance will be unassigned.
|
||||
|
||||
## Users
|
||||
|
||||
@@ -189,7 +189,7 @@ By default there's a roled named Cluster Admin which grants access to everything
|
||||
|
||||
ctl> user delete <name>
|
||||
|
||||
Deletes everything stored on the master server for this user.
|
||||
Deletes everything stored on the controller for this user.
|
||||
|
||||
Note: If the player is banned from the cluster this will effectively unban them, as the ban status is stored with the user account.
|
||||
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
# Setting up TLS
|
||||
|
||||
By default Clusterio will listen for connections over HTTP which is fine for local networks but over the Internet it should idealy be over HTTPS as there are some credentials that could be exposed to eavesdroppers.
|
||||
The recommended setup is to have an ordinary HTTPS server software like Apache or Nginx to act as a reverse proxy in front of the Clusterio master server.
|
||||
The recommended setup is to have an ordinary HTTPS server software like Apache or Nginx to act as a reverse proxy in front of the Clusterio controller.
|
||||
The reverse proxy would then be set up with a TLS certificate from a Certificate Authoritiy like [Let's Encrypt](https://letsencrypt.org).
|
||||
Alternatively you can have Clusterio listen for connections over HTTPS directly by providing it with a TLS certificate and private key.
|
||||
|
||||
@@ -11,17 +11,17 @@ See the last section for how to create and use a self-signed certificate for an
|
||||
|
||||
## Using a reverse proxy
|
||||
|
||||
Configure the proxy to forward requests over HTTP to the port the master server is listening to so that for example a request to `https://example.com/instances/` is forwarded to `http://localhost:8080/instances/`.
|
||||
It is possible to host the interface under a sub-path, in that case a request under the sub-path should be mapped to the master server with the sub-path stripped out.
|
||||
Configure the proxy to forward requests over HTTP to the port the controller is listening to so that for example a request to `https://example.com/instances/` is forwarded to `http://localhost:8080/instances/`.
|
||||
It is possible to host the interface under a sub-path, in that case a request under the sub-path should be mapped to the controller with the sub-path stripped out.
|
||||
For example `https://example.com/sub/path/instances/` would be mapped to `http://localhost:8080/instances`.
|
||||
|
||||
Additianlly WebSocket connections to `/api/socket` also needs to be forwarded.
|
||||
Typically this requires explicit configuration and support for forwarding WebSocket connections.
|
||||
If the interface hosted under a sub-path the `/api/socket` address is relative to the sub-path and the sub-path also needs to be stripped out when forwarded.
|
||||
|
||||
You can also optionally set the master server to listen only on the loopback interface to prevent it being directly connected to from the outside.
|
||||
You can also optionally set the controller to listen only on the loopback interface to prevent it being directly connected to from the outside.
|
||||
|
||||
npx clusteriomaster config set master.bind_address 127.0.0.1
|
||||
npx clusteriocontroller config set controller.bind_address 127.0.0.1
|
||||
|
||||
|
||||
### Apache Config
|
||||
@@ -44,7 +44,7 @@ Pay attention to the use of trailing slashes, as they are important.
|
||||
|
||||
## Hosting HTTPS directly from Node.js
|
||||
|
||||
Listening on HTTPS is supported by the Clusterio master server via the `master.https_port` option provided paths to a certificate file and a private key file is configured for the `master.tls_certificate` and `master.tls_private_key` options.
|
||||
Listening on HTTPS is supported by the Clusterio controller via the `controller.https_port` option provided paths to a certificate file and a private key file is configured for the `controller.tls_certificate` and `controller.tls_private_key` options.
|
||||
Both of these are expected to be unencrypted PEM files.
|
||||
|
||||
|
||||
@@ -58,14 +58,14 @@ substituting `<ip>` with the IP address the server will be accessed under, for e
|
||||
Multiple addresses can be given by supplying a comma sepparated list of `IP:address` pairs and DNS names can also be specified using the `DNS:example.com` format.
|
||||
You need to supply every name you want to connect to the server under the subjectAltName field, for example to connect to the HTTPS port locally via an address like `https://localhost:4443/` you need to add `DNS:localhost`.
|
||||
|
||||
This creates a private key file named `key.pem` and a public certificate file named `cert.pem` and these are used for the `master.tls_certificate` and `master.tls_private_key` config options respectively on the master server:
|
||||
This creates a private key file named `key.pem` and a public certificate file named `cert.pem` and these are used for the `controller.tls_certificate` and `controller.tls_private_key` config options respectively on the controller:
|
||||
|
||||
npx clusteriomaster config set master.tls_certificate cert.pem
|
||||
npx clusteriomaster config set master.tls_private_key key.pem
|
||||
npx clusteriocontroller config set controller.tls_certificate cert.pem
|
||||
npx clusteriocontroller config set controller.tls_private_key key.pem
|
||||
|
||||
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as slaves and configure it as the `slave.tls_ca` option:
|
||||
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as hosts and configure it as the `host.tls_ca` option:
|
||||
|
||||
npx clusterioslave config set slave.tls_ca cert.pem
|
||||
npx clusteriohost config set host.tls_ca cert.pem
|
||||
|
||||
You will also need to copy the `cert.pem` file to all of the computers you want to remotely manage the cluster via clusterctl and configure it as `control.tls_ca` option:
|
||||
|
||||
|
||||
+63
-63
@@ -27,7 +27,7 @@ The basic file structure of a plugin is the following.
|
||||
plugin_name/
|
||||
+- info.js
|
||||
+- package.json
|
||||
+- master.js
|
||||
+- controller.js
|
||||
+- instance.js
|
||||
+- control.js
|
||||
+- test/
|
||||
@@ -41,7 +41,7 @@ The usual guides for creating such packages apply.
|
||||
At minimum the `package.json` file must contain a version entry.
|
||||
|
||||
A possible workflow for developing plugins is to place the plugin in a sub-directory of where clusterio has been installed, and rely on Node.js searching up the folder heirarchy for it to find `@clusterio/lib`.
|
||||
To add it to `plugin-list.json` so that it gets loaded use the `plugin add <path>` sub-command to either clusteriomaster, clusterioslave or clusterioctl.
|
||||
To add it to `plugin-list.json` so that it gets loaded use the `plugin add <path>` sub-command to either clusteriocontroller, clusteriohost or clusterioctl.
|
||||
Note that it's important that the path starts with ./ or ../ (use .\ or ..\ on Windows).
|
||||
|
||||
For a plugin the most important file is the `info.js` file.
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
title: "Foo Frobber",
|
||||
description: "Does advanced frobnication",
|
||||
instanceEntrypoint: "instance",
|
||||
masterEntrypoint: "master",
|
||||
controllerEntrypoint: "controller",
|
||||
messages: {
|
||||
/* See below */
|
||||
},
|
||||
@@ -80,19 +80,19 @@ The following properties are recognized:
|
||||
**instanceEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the InstancePlugin class definition for this plugin.
|
||||
This is an optional paramater.
|
||||
A plugin may have code only for instances but it must still be loaded on the master in order for it to be possible to load it on an instance.
|
||||
A plugin may have code only for instances but it must still be loaded on the controller in order for it to be possible to load it on an instance.
|
||||
|
||||
**InstanceConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the per instance configuration fields for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**masterEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the MasterPlugin class definiton for this plugin.
|
||||
**controllerEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the ControllerPlugin class definiton for this plugin.
|
||||
This is an optional parameter.
|
||||
A plugin can be made that only runs on the master server.
|
||||
A plugin can be made that only runs on the controller.
|
||||
|
||||
**MasterConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the master server configuration fields for this plugin.
|
||||
**ControllerConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the controller configuration fields for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**controlEntrypoint**:
|
||||
@@ -108,21 +108,21 @@ The optional module folder contains a Clusterio module that will be patched into
|
||||
See the section on [Clusterio Modules](developing-for-clusterio.md) in the Developing for Clusterio document.
|
||||
The only restriction imposed on modules embedded into plugins is that they must be named the same as the plugin.
|
||||
|
||||
While there is no standard for how to organize a plugin it's recommended to put the MasterPlugin class definition into master.js and the InstancePlugin class definition into instance.js.
|
||||
While there is no standard for how to organize a plugin it's recommended to put the ControllerPlugin class definition into controller.js and the InstancePlugin class definition into instance.js.
|
||||
You can put them into whatever file you want (even the same one for both).
|
||||
|
||||
For both instanceEntrypoint and masterEntrypoint the path should not end with .js and it should use forward slashes for directory separators if any.
|
||||
For both instanceEntrypoint and controllerEntrypoint the path should not end with .js and it should use forward slashes for directory separators if any.
|
||||
|
||||
|
||||
## Defining the plugin class
|
||||
|
||||
The plugin class should derive from its respective base class defined in `lib/plugin`.
|
||||
For example, to define a MasterPlugin class the following code can be used:
|
||||
For example, to define a ControllerPlugin class the following code can be used:
|
||||
|
||||
```js
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
class MasterPlugin extends libPlugin.BaseMasterPlugin {
|
||||
class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
async init() {
|
||||
this.foo = 42;
|
||||
await this.startFrobnication();
|
||||
@@ -132,11 +132,11 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MasterPlugin,
|
||||
ControllerPlugin,
|
||||
}
|
||||
```
|
||||
|
||||
For the instance plugin it's exactly the same except "Master" is replaced with "Instance", and for the clusterioctl plugin "Control" is used.
|
||||
For the instance plugin it's exactly the same except "Controller" is replaced with "Instance", and for the clusterioctl plugin "Control" is used.
|
||||
The available hooks that you can override are documented in the base class [in lib/plugin.js](/packages/lib/plugin.js).
|
||||
|
||||
It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class.
|
||||
@@ -157,7 +157,7 @@ The async init method is always called immediatly after the constructor, so ther
|
||||
## Logging Messages
|
||||
|
||||
The base plugin classes provide a winston logger for logging messages to the shared cluster log.
|
||||
For instances a copy of the log is also stored on the slave the instance is on.
|
||||
For instances a copy of the log is also stored on the host the instance is on.
|
||||
To use it, pass a string to one of the log levels functions, for example:
|
||||
|
||||
```js
|
||||
@@ -202,39 +202,39 @@ For plugin hooks expections thrown are automatically catched and logged, but for
|
||||
## Plugin Configuration
|
||||
|
||||
Clusterio provides a configuration system that handles storing, distributing, editing and validating config fields for you.
|
||||
You can take advantage of it by subclassing `PluginConfigGroup`, setting `defaultAccess` to where config entries can be accessed from, setting the `groupName` to your plugin name, defining fields on it, finalizing it, and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in the `info.js` export.
|
||||
You can take advantage of it by subclassing `PluginConfigGroup`, setting `defaultAccess` to where config entries can be accessed from, setting the `groupName` to your plugin name, defining fields on it, finalizing it, and passing it as either `ControllerConfigGroup` or `InstanceConfigGroup` in the `info.js` export.
|
||||
For example in info.js:
|
||||
|
||||
```js
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = "foo_frobber";
|
||||
MasterConfigGroup.define({
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = "foo_frobber";
|
||||
ControllerConfigGroup.define({
|
||||
name: "level",
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initial_value: 2,
|
||||
});
|
||||
MasterConfigGroup.finalize();
|
||||
ControllerConfigGroup.finalize();
|
||||
|
||||
module.exports = {
|
||||
...
|
||||
MasterConfigGroup: MasterConfigGroup,
|
||||
ControllerConfigGroup: ControllerConfigGroup,
|
||||
};
|
||||
```
|
||||
|
||||
Code inside the `MasterPlugin` class will then be able to access the level config field through the `Config` object at `this.master.config`, for example in the MasterPluginClass:
|
||||
Code inside the `ControllerPlugin` class will then be able to access the level config field through the `Config` object at `this.controller.config`, for example in the ControllerPluginClass:
|
||||
|
||||
```js
|
||||
async init() {
|
||||
let level = this.master.config.get("foo_frobber.level");
|
||||
let level = this.controller.config.get("foo_frobber.level");
|
||||
this.logger.info(`I got a frobnication level of ${level}`);
|
||||
}
|
||||
```
|
||||
|
||||
The same applies for instance configs, replace "master" with "instance" where appropriate.
|
||||
The same applies for instance configs, replace "controller" with "instance" where appropriate.
|
||||
See [Configuration System](config-system.md) for more details on how this system works.
|
||||
|
||||
|
||||
@@ -313,8 +313,8 @@ For RCON, commands longer than 50 characters may end up being executed after sho
|
||||
|
||||
## Defining Link Messages
|
||||
|
||||
You will most likely have to communicate with the master or other instances in your plugin for it to do anything useful.
|
||||
For this there's a WebSocket communication channel established between the slaves and the master server that plugins can define their own messages to send over it.
|
||||
You will most likely have to communicate with the controller or other instances in your plugin for it to do anything useful.
|
||||
For this there's a WebSocket communication channel established between the hosts and the controller that plugins can define their own messages to send over it.
|
||||
This channel is bi-directional and all messages sent over it are validated with a JSON schema (see [this guide][guide] for an introduction to writing JSON schema).
|
||||
|
||||
[guide]: https://json-schema.org/learn/getting-started-step-by-step.html
|
||||
@@ -335,7 +335,7 @@ The Event constructor takes an object of properties that define the event, for e
|
||||
messages: {
|
||||
startFrobnication: new libLink.Event({
|
||||
type: "foo_frobber:start_frobnication",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
forwardTo: "instance",
|
||||
eventRequired: ["frobnication_type"],
|
||||
eventProperties: {
|
||||
@@ -346,9 +346,9 @@ messages: {
|
||||
},
|
||||
```
|
||||
|
||||
This specifies an event that can be sent from the master to a slave, and from a slave to an instance.
|
||||
This specifies an event that can be sent from the controller to a host, and from a host to an instance.
|
||||
It also specifies that the event must contain the property `frobnication_type`, with a string value in the data payload and that it may optionally contain a boolean `urgent` property.
|
||||
It will also be forwarded by slaves to a specific instance.
|
||||
It will also be forwarded by hosts to a specific instance.
|
||||
|
||||
The following properties are recognized by the Event constructor:
|
||||
|
||||
@@ -361,17 +361,17 @@ The type of the message sent over the socket will have the suffix `_event` appen
|
||||
#### links
|
||||
|
||||
An array of strings describing which links this event can be sent over.
|
||||
Direction matters, `"master-slave"` means the event can be sent from the master to the slave, but can't be sent back the other way, unless `"slave-master"` is also present in the links array.
|
||||
Direction matters, `"controller-host"` means the event can be sent from the controller to the host, but can't be sent back the other way, unless `"host-controller"` is also present in the links array.
|
||||
|
||||
The available endpoints are `master`, `slave`, `instance`, and `control`.
|
||||
Master talks with slave and control, and slave talks to instance.
|
||||
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from master to instance it must have both `"master-slave"` and `"slave-instance"` in the links array).
|
||||
The available endpoints are `controller`, `host`, `instance`, and `control`.
|
||||
Controller talks with host and control, and host talks to instance.
|
||||
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from controller to instance it must have both `"controller-host"` and `"host-instance"` in the links array).
|
||||
See `forwardTo` and `broadcastTo` for ways to forward an event to the next link in a chain.
|
||||
|
||||
#### forwardTo
|
||||
|
||||
Target to forward an event to.
|
||||
Can either be `"master"`, to indicate a slave should forward it to the master server, or `"instance"`, to indicate it should be forwarded to the instances specified by the `instance_id` event property.
|
||||
Can either be `"controller"`, to indicate a host should forward it to the controller, or `"instance"`, to indicate it should be forwarded to the instances specified by the `instance_id` event property.
|
||||
This works by using a default handler for the event at the links that forward it.
|
||||
|
||||
#### broadcastTo
|
||||
@@ -379,7 +379,7 @@ This works by using a default handler for the event at the links that forward it
|
||||
Target to broadcast this message towards.
|
||||
A value of "instance" means the event will be broadcast to all instances downstream of the target it's sent to, but not back from where it came from.
|
||||
Currently, only "instance" is supported.
|
||||
This means that sending the event to a slave from an instance will cause it to be broadcast to all instances of that slave except for the instance it came from.
|
||||
This means that sending the event to a host from an instance will cause it to be broadcast to all instances of that host except for the instance it came from.
|
||||
|
||||
#### eventRequired
|
||||
|
||||
@@ -393,8 +393,8 @@ Object with properties mapping to a JSON schema of that property that specifies
|
||||
This is equivalent to using the `properties` keyword in JSON schema, except that the properties specified are by default required and additional properties are not allowed.
|
||||
See [this guide][guide] for an introduction to writing JSON schemas.
|
||||
|
||||
The forwardTo and broadcastTo can be combined such that specifying `"master"` as the forwardTo value and `"instance"` as the broadcastTo value will cause the event to be broadcast to all instances in the cluster.
|
||||
For this to work, you will need to specify `instance-slave`, `slave-master`, `master-slave`, and `slave-instance` as the links.
|
||||
The forwardTo and broadcastTo can be combined such that specifying `"controller"` as the forwardTo value and `"instance"` as the broadcastTo value will cause the event to be broadcast to all instances in the cluster.
|
||||
For this to work, you will need to specify `instance-host`, `host-controller`, `controller-host`, and `host-instance` as the links.
|
||||
|
||||
Keep in mind when forwarding events that if the target an event is being forwarded to is not online, the event will be dropped.
|
||||
Use a request if you need a confirmation that the message was received.
|
||||
@@ -410,7 +410,7 @@ For example, the following could be defined in `info.js`:
|
||||
messages: {
|
||||
reportFrobnication: new libLink.Request({
|
||||
type: "foo_frobber:report_frobnication",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
forwardTo: "instance",
|
||||
requestRequired: ["verbosity"],
|
||||
requestProperties: {
|
||||
@@ -427,10 +427,10 @@ messages: {
|
||||
},
|
||||
```
|
||||
|
||||
This specifies a request that can be sent from the master to a slave, and from a slave to an instance.
|
||||
This specifies a request that can be sent from the controller to a host, and from a host to an instance.
|
||||
The request data must contain the property `verbosity` with an integer number as the value, as well as the `instance_id` property (implied by `forwardTo: "instance"`) and it may also contain a boolean `special` property.
|
||||
It also defines that the response sent must contain a `report` property mapping to an array of strings.
|
||||
When received by a slave, it will also be forwarded to the instance specified by `instance_id`.
|
||||
When received by a host, it will also be forwarded to the instance specified by `instance_id`.
|
||||
|
||||
The following properties are recognized by the Request constructor:
|
||||
|
||||
@@ -443,17 +443,17 @@ The type of the message sent over the socket will have the suffix `_request` app
|
||||
#### links
|
||||
|
||||
An array of strings describing which links this request can be sent over.
|
||||
Direction matters; `"master-slave"` means the request can be sent from the master to the slave and the slave can reply to the master, but the slave can't send a request to the master unless `"slave-master"` is also present in the links array.
|
||||
Direction matters; `"controller-host"` means the request can be sent from the controller to the host and the host can reply to the controller, but the host can't send a request to the controller unless `"host-controller"` is also present in the links array.
|
||||
|
||||
The available endpoints are `master`, `slave`, `instance`, and `control`.
|
||||
Master talks with slave and control, and slave talks to instance.
|
||||
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from master to instance it must have both `"master-slave"` and `"slave-instance"` in the links array).
|
||||
The available endpoints are `controller`, `host`, `instance`, and `control`.
|
||||
Controller talks with host and control, and host talks to instance.
|
||||
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from controller to instance it must have both `"controller-host"` and `"host-instance"` in the links array).
|
||||
See `forwardTo` for ways to forward a request to the next link in a chain.
|
||||
|
||||
#### forwardTo
|
||||
|
||||
Target to forward the request to.
|
||||
Can either be `"master"` to indicate a slave should forward it to the master server when receiving it from an instance, or `"instance"` to indicate it should be forwarded to the instances specified by the `instance_id` request property.
|
||||
Can either be `"controller"` to indicate a host should forward it to the controller when receiving it from an instance, or `"instance"` to indicate it should be forwarded to the instances specified by the `instance_id` request property.
|
||||
This works by using a default handler for the request by the links that forward it.
|
||||
|
||||
#### requestRequired
|
||||
@@ -480,7 +480,7 @@ Same as the requestProperties only for the response sent back by the target.
|
||||
## Sending Link Messages
|
||||
|
||||
Link messages are sent by calling the `.send()` method on the Event/Request instance with the link you want to send it over and the data to send.
|
||||
For `InstancePlugin` code the link to the slave is the `instance` itself, which is accessible through the `.instance` property of the `InstancePlugin`.
|
||||
For `InstancePlugin` code the link to the host is the `instance` itself, which is accessible through the `.instance` property of the `InstancePlugin`.
|
||||
The `.info` property of the plugin class exposes the data exported from the plugin's `info.js` module.
|
||||
In other words:
|
||||
|
||||
@@ -497,26 +497,26 @@ For the Request class the send method is async and returns the response data rec
|
||||
### Handling connection events
|
||||
|
||||
There are a few connection related events that plugins neeed to repsond to in order to avoid data loss and connection problems.
|
||||
The most important is the prepare disconnect for the link between master and slave.
|
||||
This is signaled to `MasterPlugin` classes via the `onPrepareSlaveDisconnect` hook and to `InstancePlugin` classes via the `onPrepareMasterDisconnect` hook.
|
||||
The most important is the prepare disconnect for the link between controller and host.
|
||||
This is signaled to `ControllerPlugin` classes via the `onPrepareHostDisconnect` hook and to `InstancePlugin` classes via the `onPrepareControllerDisconnect` hook.
|
||||
|
||||
After the prepare disconnect the connection will be closed, which will result in pending requests and events being dropped.
|
||||
Plugins must respond to the prepare disconnect by stopping any processess it does that send events or requests over the link in question.
|
||||
This can be accomplished either through listening for the prepare disconnect hook, or by checking the `connected` property of the `SlaveConnection` class and `Slave` class on the master and slave respectively.
|
||||
This can be accomplished either through listening for the prepare disconnect hook, or by checking the `connected` property of the `HostConnection` class and `Host` class on the controller and host respectively.
|
||||
For example the sending of an event from an `InstancePlugin` class can be stopped while the connection is not connected, not in the dropped state, and not in the process of discunnecting by using the following code:
|
||||
|
||||
```js
|
||||
if (this.slave.connected) {
|
||||
if (this.host.connected) {
|
||||
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
|
||||
}
|
||||
```
|
||||
|
||||
If the event or request needs to be sent to the master it can be put into a queue stored on the plugin instance and sent out when the connection is established again.
|
||||
The re-establishement of the connection is notified to plugins via the `connect` event to the `onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks.
|
||||
If the event or request needs to be sent to the controller it can be put into a queue stored on the plugin instance and sent out when the connection is established again.
|
||||
The re-establishement of the connection is notified to plugins via the `connect` event to the `onControllerConnectionEvent` and `onHostConnectionEvent` hooks.
|
||||
|
||||
The second connection event which is of lesser importance to respond to is the `drop` connection event served through `onMasterConnectionEvent` for `InstancePlugin` classes and through `onSlaveConnectionEvent` for `MasterPlugin` classes.
|
||||
This is raised when the connection between the master and slave in question is lost, most likely due to networking issues.
|
||||
When in the dropped state the slave will keep trying to reconnect to the master server in order to re-establish it, and if successful no events or requests will be lost.
|
||||
The second connection event which is of lesser importance to respond to is the `drop` connection event served through `onControllerConnectionEvent` for `InstancePlugin` classes and through `onHostConnectionEvent` for `ControllerPlugin` classes.
|
||||
This is raised when the connection between the controller and host in question is lost, most likely due to networking issues.
|
||||
When in the dropped state the host will keep trying to reconnect to the controller in order to re-establish it, and if successful no events or requests will be lost.
|
||||
However while in the dropped state any requests and events sent gets queued up in memory until the connection is either re-established or the session times out.
|
||||
This means that if your plugin sends a lot of events or requests, they can end up being queued up in a buffer and sent out all at once the connection is re-estabished.
|
||||
To avoid this you should be throtteling and/or stopping your requests/events after `drop` has been raised, and continue back as normal when `resume` is raised.
|
||||
@@ -536,11 +536,11 @@ const fooMetric = new Counter(
|
||||
"clusterio_foo_frobber_foo_metric", "Measures the level of foo",
|
||||
);
|
||||
|
||||
// Somewhere in the master plugin code
|
||||
// Somewhere in the controller plugin code
|
||||
fooMetric.inc();
|
||||
```
|
||||
|
||||
This works for master plugins, and the metric will be automatically available through the /metric HTTP endpoint.
|
||||
This works for controller plugins, and the metric will be automatically available through the /metric HTTP endpoint.
|
||||
It's recommended that plugin metrics follow `clusterio_<plugin_name>_<metric_name>` as the naming scheme.
|
||||
|
||||
For metrics that are per-instance, you must define an `instance_id` label and set it accordingly, for example:
|
||||
@@ -557,11 +557,11 @@ const barMetric = new Gauge(
|
||||
barMetric.labels(String(this.instance.id)).set(someValue);
|
||||
```
|
||||
|
||||
Metrics are automatically registered to the default registry, and this default registry is automatically polled by the master server on slaves.
|
||||
This means that it's important that you place the definition of the metric at module level so that it's not created more than once over the lifetime of a slave.
|
||||
Metrics are automatically registered to the default registry, and this default registry is automatically polled by the controller on hosts.
|
||||
This means that it's important that you place the definition of the metric at module level so that it's not created more than once over the lifetime of a host.
|
||||
Since the metrics remember their values and would continue to be exported after an instance is shutdown, there's code at instance shutdown that removes all the values where the `instance_id` label matches the id of the instance shut down.
|
||||
|
||||
For statistics you need to update on collection there's an `onMetrics` hook on both master and instance plugins that is run before the metrics in the default registry are collected.
|
||||
For statistics you need to update on collection there's an `onMetrics` hook on both controller and instance plugins that is run before the metrics in the default registry are collected.
|
||||
|
||||
|
||||
## Adding Custom Commands to clusterioctl
|
||||
@@ -600,7 +600,7 @@ For a command the `definition` is the arguments to pass to [yargs.command](http:
|
||||
The `handler` is an async function that's invoked when the command is executed and it's passed the parsed command line arguments and a reference to the `Control` class of clusterioctl.
|
||||
It's possible to optain a reference to the plugin class with `control.plugins.get(info.name)`.
|
||||
|
||||
Note that messages sent from clusterioctl needs to have `"control-master"` as a part of the links array for it to be accepted by the master server, see [Defining Link Messages](#defining-link-messages) for how to define the messages that can be sent to the master.
|
||||
Note that messages sent from clusterioctl needs to have `"control-controller"` as a part of the links array for it to be accepted by the controller, see [Defining Link Messages](#defining-link-messages) for how to define the messages that can be sent to the controller.
|
||||
|
||||
To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Control plugin:
|
||||
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
"devDependencies": {
|
||||
"@clusterio/ctl": "workspace:*",
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"@clusterio/master": "workspace:*",
|
||||
"@clusterio/slave": "workspace:*",
|
||||
"@clusterio/controller": "workspace:*",
|
||||
"@clusterio/host": "workspace:*",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"express": "^4.16.2",
|
||||
|
||||
File renamed without changes.
@@ -1,27 +1,27 @@
|
||||
# Clusterio Master Server
|
||||
# Clusterio Controller
|
||||
|
||||
Communication hub for Clusterio clusters.
|
||||
The master server forwards data between Clusterio slaves connected to it and allows the cluster to be remotely managed through WebSocket connections to it either by using the included web interface or the Clusterio ctl command line utility.
|
||||
The controller forwards data between Clusterio hosts connected to it and allows the cluster to be remotely managed through WebSocket connections to it either by using the included web interface or the Clusterio ctl command line utility.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
npx clusteriomaster <command>
|
||||
npx clusteriocontroller <command>
|
||||
|
||||
Common options:
|
||||
|
||||
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the master server.
|
||||
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the controller.
|
||||
Defaults to `plugin-list.json` and will be created if it does not exist.
|
||||
See the `plugin` command for managing this list.
|
||||
|
||||
* `--config <file>` JSON file to use for storing configuration for the master server.
|
||||
Defaults to `config-master.json` and will be created if it does not exist.
|
||||
* `--config <file>` JSON file to use for storing configuration for the controller.
|
||||
Defaults to `config-controller.json` and will be created if it does not exist.
|
||||
See the `config` command for inspecting and modifying the configuration.
|
||||
|
||||
|
||||
### `plugin <command>`
|
||||
|
||||
Configure plugins available to be loaded by the master server.
|
||||
Configure plugins available to be loaded by the controller.
|
||||
The available plugins will be loaded unless they have been disabled in the configuration, see the config command for disabling plugins.
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ A relative path must start with ./ or ../ (or .\ and ..\ on Windows) otherwise i
|
||||
For example, installing the Subspace Storage plugin:
|
||||
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin remove <name>`
|
||||
@@ -46,10 +46,10 @@ Removing and unistalling a plugin is usually not neccessary as the functions pro
|
||||
|
||||
For example, uninstalling the Subspace Storage plugin:
|
||||
|
||||
npx clusteriomaster plugin remove subspace_storage
|
||||
npx clusteriocontroller plugin remove subspace_storage
|
||||
npm uninstall @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin list`
|
||||
@@ -59,8 +59,8 @@ Lists the plugins set up to be available by name followed by path.
|
||||
|
||||
### `config`
|
||||
|
||||
Manage the master server configuration offline.
|
||||
This should only be used when the master server is stopped, otherwise the config read might be out of date and config changes will be overwritten when the master server shuts down.
|
||||
Manage the controller configuration offline.
|
||||
This should only be used when the controller is stopped, otherwise the config read might be out of date and config changes will be overwritten when the controller shuts down.
|
||||
|
||||
|
||||
#### `config set <config-entry> [value]`
|
||||
@@ -96,7 +96,7 @@ You should be using your own Factorio multiplayer username here.
|
||||
#### `bootstrap generate-user-token <name>`
|
||||
|
||||
Generate an access token for logging in and possibly managing the cluster from the given user account.
|
||||
This can be used both in the web interface and as the `master_token` to connect with clusterioctl.
|
||||
This can be used both in the web interface and as the `controller_token` to connect with clusterioctl.
|
||||
|
||||
|
||||
#### `bootstrap create-ctl-config <name>`
|
||||
@@ -106,7 +106,7 @@ Creates a clusterioctl config for the given user with url and token set up for c
|
||||
|
||||
### `run`
|
||||
|
||||
Runs the master server.
|
||||
Runs the controller.
|
||||
|
||||
|
||||
## See Also
|
||||
@@ -1,17 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Clusterio master server
|
||||
* Clusterio controller
|
||||
*
|
||||
* Facilitates communication between slaves and control of the cluster
|
||||
* Facilitates communication between hosts and control of the cluster
|
||||
* through WebSocet connections, and hosts a webserver for browser
|
||||
* interfaces and Prometheus statistics export. It is remotely controlled
|
||||
* by {@link module:ctl/ctl}.
|
||||
*
|
||||
* @module master/master
|
||||
* @module controller/controller
|
||||
* @author Danielv123, Hornwitser
|
||||
* @example
|
||||
* npx clusteriomaster run
|
||||
* npx clusteriocontroller run
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -36,28 +36,28 @@ const libSharedCommands = require("@clusterio/lib/shared_commands");
|
||||
const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging");
|
||||
const libLoggingUtils = require("@clusterio/lib/logging_utils");
|
||||
|
||||
const Master = require("./src/Master");
|
||||
const Controller = require("./src/Controller");
|
||||
const UserManager = require("./src/UserManager");
|
||||
const version = require("./package").version;
|
||||
|
||||
// globals
|
||||
let master;
|
||||
let controller;
|
||||
|
||||
|
||||
void new libPrometheus.Gauge(
|
||||
"clusterio_master_slave_mapping",
|
||||
"Mapping of Slave ID to name",
|
||||
"clusterio_controller_host_mapping",
|
||||
"Mapping of Host ID to name",
|
||||
{
|
||||
labels: ["slave_id", "slave_name"],
|
||||
labels: ["host_id", "host_name"],
|
||||
callback: function(gauge) {
|
||||
gauge.clear();
|
||||
if (!master || !master.slaves) {
|
||||
if (!controller || !controller.hosts) {
|
||||
return;
|
||||
}
|
||||
for (let [id, slave] of master.slaves) {
|
||||
for (let [id, host] of controller.hosts) {
|
||||
gauge.labels({
|
||||
slave_id: String(id),
|
||||
slave_name: slave.name,
|
||||
host_id: String(id),
|
||||
host_name: host.name,
|
||||
}).set(1);
|
||||
};
|
||||
},
|
||||
@@ -65,20 +65,20 @@ void new libPrometheus.Gauge(
|
||||
);
|
||||
|
||||
void new libPrometheus.Gauge(
|
||||
"clusterio_master_instance_mapping",
|
||||
"Mapping of Instance ID to name and slave",
|
||||
"clusterio_controller_instance_mapping",
|
||||
"Mapping of Instance ID to name and host",
|
||||
{
|
||||
labels: ["instance_id", "instance_name", "slave_id"],
|
||||
labels: ["instance_id", "instance_name", "host_id"],
|
||||
callback: function(gauge) {
|
||||
gauge.clear();
|
||||
if (!master || !master.instances) {
|
||||
if (!controller || !controller.instances) {
|
||||
return;
|
||||
}
|
||||
for (let [id, instance] of master.instances) {
|
||||
for (let [id, instance] of controller.instances) {
|
||||
gauge.labels({
|
||||
instance_id: String(id),
|
||||
instance_name: String(instance.config.get("instance.name")),
|
||||
slave_id: String(instance.config.get("instance.assigned_slave")),
|
||||
host_id: String(instance.config.get("instance.assigned_host")),
|
||||
}).set(1);
|
||||
}
|
||||
},
|
||||
@@ -86,32 +86,32 @@ void new libPrometheus.Gauge(
|
||||
);
|
||||
|
||||
void new libPrometheus.Gauge(
|
||||
"clusterio_master_websocket_active_connections",
|
||||
"How many WebSocket connections are currently open to the master server",
|
||||
{ callback: function(gauge) { gauge.set(master.wsServer.activeConnectors.size); }}
|
||||
"clusterio_controller_websocket_active_connections",
|
||||
"How many WebSocket connections are currently open to the controller",
|
||||
{ callback: function(gauge) { gauge.set(controller.wsServer.activeConnectors.size); }}
|
||||
);
|
||||
|
||||
void new libPrometheus.Gauge(
|
||||
"clusterio_master_active_slaves",
|
||||
"How many slaves are currently connected to the master",
|
||||
{ callback: function(gauge) { gauge.set(master.wsServer.slaveConnections.size); }}
|
||||
"clusterio_controller_active_hosts",
|
||||
"How many hosts are currently connected to the controller",
|
||||
{ callback: function(gauge) { gauge.set(controller.wsServer.hostConnections.size); }}
|
||||
);
|
||||
|
||||
void new libPrometheus.Gauge(
|
||||
"clusterio_master_connected_clients_count", "How many clients are currently connected to this master server",
|
||||
"clusterio_controller_connected_clients_count", "How many clients are currently connected to this controller",
|
||||
{
|
||||
labels: ["type"], callback: async function(gauge) {
|
||||
gauge.labels("slave").set(master.wsServer.slaveConnections.size);
|
||||
gauge.labels("control").set(master.wsServer.controlConnections.length);
|
||||
gauge.labels("host").set(controller.wsServer.hostConnections.size);
|
||||
gauge.labels("control").set(controller.wsServer.controlConnections.length);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
async function handleBootstrapCommand(args, masterConfig) {
|
||||
async function handleBootstrapCommand(args, controllerConfig) {
|
||||
let subCommand = args._[1];
|
||||
let userManager = new UserManager(masterConfig);
|
||||
await userManager.load(path.join(masterConfig.get("master.database_directory"), "users.json"));
|
||||
let userManager = new UserManager(controllerConfig);
|
||||
await userManager.load(path.join(controllerConfig.get("controller.database_directory"), "users.json"));
|
||||
if (subCommand === "create-admin") {
|
||||
if (!args.name) {
|
||||
logger.error("name cannot be blank");
|
||||
@@ -127,7 +127,7 @@ async function handleBootstrapCommand(args, masterConfig) {
|
||||
let adminRole = libUsers.ensureDefaultAdminRole(userManager.roles);
|
||||
admin.roles.add(adminRole);
|
||||
admin.isAdmin = true;
|
||||
await userManager.save(path.join(masterConfig.get("master.database_directory"), "users.json"));
|
||||
await userManager.save(path.join(controllerConfig.get("controller.database_directory"), "users.json"));
|
||||
|
||||
} else if (subCommand === "generate-user-token") {
|
||||
let user = userManager.users.get(args.name);
|
||||
@@ -137,13 +137,13 @@ async function handleBootstrapCommand(args, masterConfig) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(user.createToken(masterConfig.get("master.auth_secret")));
|
||||
console.log(user.createToken(controllerConfig.get("controller.auth_secret")));
|
||||
|
||||
} else if (subCommand === "generate-slave-token") {
|
||||
} else if (subCommand === "generate-host-token") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(jwt.sign(
|
||||
{ aud: "slave", slave: args.id },
|
||||
Buffer.from(masterConfig.get("master.auth_secret"), "base64")
|
||||
{ aud: "host", host: args.id },
|
||||
Buffer.from(controllerConfig.get("controller.auth_secret"), "base64")
|
||||
));
|
||||
|
||||
} else if (subCommand === "create-ctl-config") {
|
||||
@@ -156,8 +156,10 @@ async function handleBootstrapCommand(args, masterConfig) {
|
||||
let controlConfig = new libConfig.ControlConfig("control");
|
||||
await controlConfig.init();
|
||||
|
||||
controlConfig.set("control.master_url", Master.calculateMasterUrl(masterConfig));
|
||||
controlConfig.set("control.master_token", admin.createToken(masterConfig.get("master.auth_secret")));
|
||||
controlConfig.set("control.controller_url", Controller.calculateControllerUrl(controllerConfig));
|
||||
controlConfig.set(
|
||||
"control.controller_token", admin.createToken(controllerConfig.get("controller.auth_secret"))
|
||||
);
|
||||
|
||||
let content = JSON.stringify(controlConfig.serialize(), null, 4);
|
||||
if (args.output === "-") {
|
||||
@@ -176,13 +178,13 @@ async function initialize() {
|
||||
shouldRun: false,
|
||||
clusterLogger: null,
|
||||
pluginInfos: null,
|
||||
masterConfigPath: null,
|
||||
masterConfig: null,
|
||||
controllerConfigPath: null,
|
||||
controllerConfig: null,
|
||||
};
|
||||
|
||||
// argument parsing
|
||||
parameters.args = yargs
|
||||
.scriptName("master")
|
||||
.scriptName("controller")
|
||||
.usage("$0 <command> [options]")
|
||||
.option("log-level", {
|
||||
nargs: 1,
|
||||
@@ -199,8 +201,8 @@ async function initialize() {
|
||||
})
|
||||
.option("config", {
|
||||
nargs: 1,
|
||||
describe: "master config file to use",
|
||||
default: "config-master.json",
|
||||
describe: "controller config file to use",
|
||||
default: "config-controller.json",
|
||||
type: "string",
|
||||
})
|
||||
.option("plugin-list", {
|
||||
@@ -210,13 +212,13 @@ async function initialize() {
|
||||
type: "string",
|
||||
})
|
||||
.command("plugin", "Manage available plugins", libSharedCommands.pluginCommand)
|
||||
.command("config", "Manage Master config", libSharedCommands.configCommand)
|
||||
.command("config", "Manage Controller config", libSharedCommands.configCommand)
|
||||
.command("bootstrap", "Bootstrap access to cluster", yargs => {
|
||||
yargs
|
||||
.command("create-admin <name>", "Create a cluster admin")
|
||||
.command("generate-user-token <name>", "Generate authentication token for the given user")
|
||||
.command("generate-slave-token <id>", "Generate authentication token for the given slave", yargs => {
|
||||
yargs.positional("id", { describe: "ID of the slave", type: "number" });
|
||||
.command("generate-host-token <id>", "Generate authentication token for the given host", yargs => {
|
||||
yargs.positional("id", { describe: "ID of the host", type: "number" });
|
||||
})
|
||||
.command("create-ctl-config <name>", "Create clusterioctl config for the given user", yargs => {
|
||||
yargs.option("output", {
|
||||
@@ -226,7 +228,7 @@ async function initialize() {
|
||||
})
|
||||
.demandCommand(1, "You need to specify a command to run");
|
||||
})
|
||||
.command("run", "Run master server", yargs => {
|
||||
.command("run", "Run controller", yargs => {
|
||||
yargs.option("dev", { hidden: true, type: "boolean", nargs: 0 });
|
||||
yargs.option("dev-plugin", { hidden: true, type: "array" });
|
||||
})
|
||||
@@ -235,28 +237,6 @@ async function initialize() {
|
||||
.argv
|
||||
;
|
||||
|
||||
{
|
||||
// Migration from alpha-10 single file logs, note that we can't use
|
||||
// the logger here as it's not initalized yet.
|
||||
/* eslint-disable no-console */
|
||||
let clusterLogDirectory = path.join(parameters.args.logDirectory, "cluster");
|
||||
if (!await fs.pathExists(clusterLogDirectory) && await fs.pathExists("cluster.log")) {
|
||||
console.log("Migrating cluster log...");
|
||||
await fs.ensureDir(clusterLogDirectory);
|
||||
await libLoggingUtils.migrateLogs("cluster.log", clusterLogDirectory, "cluster-%DATE%.log");
|
||||
console.log("Migration complete, you should delete cluster.log now");
|
||||
}
|
||||
|
||||
let masterLogDirectory = path.join(parameters.args.logDirectory, "master");
|
||||
if (!await fs.pathExists(masterLogDirectory) && await fs.pathExists("master.log")) {
|
||||
console.log("Migrating master log...");
|
||||
await fs.ensureDir(masterLogDirectory);
|
||||
await libLoggingUtils.migrateLogs("master.log", masterLogDirectory, "master-%DATE%.log");
|
||||
console.log("Migration complete, you should delete master.log now");
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
// Combined log stream of the whole cluster.
|
||||
parameters.clusterLogger = winston.createLogger({
|
||||
format: winston.format.json(),
|
||||
@@ -269,11 +249,11 @@ async function initialize() {
|
||||
dirname: path.join(parameters.args.logDirectory, "cluster"),
|
||||
}));
|
||||
|
||||
// Log stream for the master server.
|
||||
// Log stream for the controller.
|
||||
logger.add(new winston.transports.DailyRotateFile({
|
||||
format: winston.format.json(),
|
||||
filename: "master-%DATE%.log",
|
||||
dirname: path.join(parameters.args.logDirectory, "master"),
|
||||
filename: "controller-%DATE%.log",
|
||||
dirname: path.join(parameters.args.logDirectory, "controller"),
|
||||
}));
|
||||
logger.add(new winston.transports.Stream({
|
||||
stream: parameters.clusterLogger,
|
||||
@@ -288,7 +268,7 @@ async function initialize() {
|
||||
|
||||
let command = parameters.args._[0];
|
||||
if (command === "run") {
|
||||
logger.info(`Starting Clusterio master ${version}`);
|
||||
logger.info(`Starting Clusterio controller ${version}`);
|
||||
parameters.shouldRun = true;
|
||||
}
|
||||
|
||||
@@ -313,39 +293,39 @@ async function initialize() {
|
||||
libConfig.registerPluginConfigGroups(parameters.pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
parameters.masterConfigPath = parameters.args.config;
|
||||
logger.info(`Loading config from ${parameters.masterConfigPath}`);
|
||||
parameters.masterConfig = new libConfig.MasterConfig("master");
|
||||
parameters.controllerConfigPath = parameters.args.config;
|
||||
logger.info(`Loading config from ${parameters.controllerConfigPath}`);
|
||||
parameters.controllerConfig = new libConfig.ControllerConfig("controller");
|
||||
try {
|
||||
await parameters.masterConfig.load(JSON.parse(await fs.readFile(parameters.masterConfigPath)));
|
||||
await parameters.controllerConfig.load(JSON.parse(await fs.readFile(parameters.controllerConfigPath)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.info("Config not found, initializing new config");
|
||||
await parameters.masterConfig.init();
|
||||
await parameters.controllerConfig.init();
|
||||
|
||||
} else {
|
||||
throw new libErrors.StartupError(`Failed to load ${parameters.masterConfigPath}: ${err.message}`);
|
||||
throw new libErrors.StartupError(`Failed to load ${parameters.controllerConfigPath}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parameters.masterConfig.get("master.auth_secret")) {
|
||||
logger.info("Generating new master authentication secret");
|
||||
if (!parameters.controllerConfig.get("controller.auth_secret")) {
|
||||
logger.info("Generating new controller authentication secret");
|
||||
let asyncRandomBytes = util.promisify(crypto.randomBytes);
|
||||
let bytes = await asyncRandomBytes(256);
|
||||
parameters.masterConfig.set("master.auth_secret", bytes.toString("base64"));
|
||||
parameters.controllerConfig.set("controller.auth_secret", bytes.toString("base64"));
|
||||
await libFileOps.safeOutputFile(
|
||||
parameters.masterConfigPath, JSON.stringify(parameters.masterConfig.serialize(), null, 4)
|
||||
parameters.controllerConfigPath, JSON.stringify(parameters.controllerConfig.serialize(), null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
if (command === "config") {
|
||||
await libSharedCommands.handleConfigCommand(
|
||||
parameters.args, parameters.masterConfig, parameters.masterConfigPath
|
||||
parameters.args, parameters.controllerConfig, parameters.controllerConfigPath
|
||||
);
|
||||
|
||||
} else if (command === "bootstrap") {
|
||||
await handleBootstrapCommand(parameters.args, parameters.masterConfig);
|
||||
await handleBootstrapCommand(parameters.args, parameters.controllerConfig);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
@@ -354,14 +334,14 @@ async function initialize() {
|
||||
async function startup() {
|
||||
// Set the process title, shows up as the title of the CMD window on windows
|
||||
// and as the process name in ps/top on linux.
|
||||
process.title = "clusterioMaster";
|
||||
process.title = "clusteriocontroller";
|
||||
|
||||
let { args, shouldRun, clusterLogger, pluginInfos, masterConfigPath, masterConfig } = await initialize();
|
||||
let { args, shouldRun, clusterLogger, pluginInfos, controllerConfigPath, controllerConfig } = await initialize();
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
master = new Master(clusterLogger, pluginInfos, masterConfigPath, masterConfig);
|
||||
controller = new Controller(clusterLogger, pluginInfos, controllerConfigPath, controllerConfig);
|
||||
|
||||
let secondSigint = false;
|
||||
process.on("SIGINT", () => {
|
||||
@@ -374,7 +354,7 @@ async function startup() {
|
||||
|
||||
secondSigint = true;
|
||||
logger.info("Caught interrupt signal, shutting down");
|
||||
master.stop();
|
||||
controller.stop();
|
||||
});
|
||||
let secondSigterm = false;
|
||||
process.on("SIGTERM", () => {
|
||||
@@ -387,15 +367,15 @@ async function startup() {
|
||||
|
||||
secondSigterm = true;
|
||||
logger.info("Caught termination signal, shutting down");
|
||||
master.stop();
|
||||
controller.stop();
|
||||
});
|
||||
|
||||
process.on("SIGHUP", () => {
|
||||
logger.info("Terminal closed, shutting down");
|
||||
master.stop();
|
||||
controller.stop();
|
||||
});
|
||||
|
||||
await master.start(args);
|
||||
await controller.start(args);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -413,9 +393,9 @@ I version of clusterio. Expect things to break. I
|
||||
startup().catch(err => {
|
||||
if (err instanceof libErrors.StartupError) {
|
||||
logger.fatal(`
|
||||
+----------------------------------+
|
||||
| Unable to to start master server |
|
||||
+----------------------------------+
|
||||
+-------------------------------+
|
||||
| Unable to to start controller |
|
||||
+-------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
} else if (err instanceof libErrors.PluginError) {
|
||||
@@ -428,7 +408,7 @@ ${err.original.stack}`
|
||||
} else {
|
||||
logger.fatal(`
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while starting master, please |
|
||||
| Unexpected error occured while starting controller, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@clusterio/master",
|
||||
"description": "Implementation of Clusterio master server",
|
||||
"name": "@clusterio/controller",
|
||||
"description": "Implementation of Clusterio controller",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "master.js",
|
||||
"main": "controller.js",
|
||||
"scripts": {
|
||||
"prepare": "webpack-cli --env production",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriomaster": "./master.js"
|
||||
"clusteriocontroller": "./controller.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -6,58 +6,58 @@ const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
|
||||
/**
|
||||
* Base class for master server connections
|
||||
* Base class for controller connections
|
||||
*
|
||||
* @extends module:lib/link.Link
|
||||
* @alias module:master/src/BaseConnection
|
||||
* @alias module:controller/src/BaseConnection
|
||||
*/
|
||||
class BaseConnection extends libLink.Link {
|
||||
constructor(target, connector, master) {
|
||||
/** @member {module:master/src/WsServerConnector} module:master/src/BaseConnection#connector */
|
||||
super("master", target, connector);
|
||||
this._master = master;
|
||||
constructor(target, connector, controller) {
|
||||
/** @member {module:controller/src/WsServerConnector} module:controller/src/BaseConnection#connector */
|
||||
super("controller", target, connector);
|
||||
this._controller = controller;
|
||||
this._disconnecting = false;
|
||||
libLink.attachAllMessages(this);
|
||||
for (let masterPlugin of this._master.plugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, masterPlugin);
|
||||
for (let controllerPlugin of this._controller.plugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, controllerPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
return await this._master.forwardRequestToInstance(request, message.data);
|
||||
return await this._controller.forwardRequestToInstance(request, message.data);
|
||||
}
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
let instance = this._controller.instances.get(message.data.instance_id);
|
||||
if (!instance) { return; }
|
||||
|
||||
let slaveId = instance.config.get("instance.assigned_slave");
|
||||
if (slaveId === null) { return; }
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) { return; }
|
||||
|
||||
let connection = this._master.wsServer.slaveConnections.get(slaveId);
|
||||
let connection = this._controller.wsServer.hostConnections.get(hostId);
|
||||
if (!connection || connection.closing) { return; }
|
||||
if (event.plugin && !connection.plugins.has(event.plugin)) { return; }
|
||||
|
||||
event.send(connection, message.data);
|
||||
}
|
||||
|
||||
async broadcastEventToSlaves(message, event) {
|
||||
for (let slaveConnection of this._master.wsServer.slaveConnections.values()) {
|
||||
async broadcastEventToHosts(message, event) {
|
||||
for (let hostConnection of this._controller.wsServer.hostConnections.values()) {
|
||||
// Do not broadcast back to the source
|
||||
if (slaveConnection === this) { continue; }
|
||||
if (slaveConnection.connector.closing) { continue; }
|
||||
if (event.plugin && !slaveConnection.plugins.has(event.plugin)) { continue; }
|
||||
if (hostConnection === this) { continue; }
|
||||
if (hostConnection.connector.closing) { continue; }
|
||||
if (event.plugin && !hostConnection.plugins.has(event.plugin)) { continue; }
|
||||
|
||||
event.send(slaveConnection, message.data);
|
||||
event.send(hostConnection, message.data);
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
await this.broadcastEventToSlaves(message, event);
|
||||
await this.broadcastEventToHosts(message, event);
|
||||
}
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
await libPlugin.invokeHook(this._master.plugins, "onPrepareSlaveDisconnect", this);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPrepareHostDisconnect", this);
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
@@ -87,7 +87,7 @@ class BaseConnection extends libLink.Link {
|
||||
|
||||
async getModPackRequestHandler(message) {
|
||||
let { id } = message.data;
|
||||
let modPack = this._master.modPacks.get(id);
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
@@ -95,11 +95,11 @@ class BaseConnection extends libLink.Link {
|
||||
}
|
||||
|
||||
async getDefaultModPackRequestHandler(message) {
|
||||
let id = this._master.config.get("master.default_mod_pack_id");
|
||||
let id = this._controller.config.get("controller.default_mod_pack_id");
|
||||
if (id === null) {
|
||||
throw new libErrors.RequestError("Default mod pack not set on master");
|
||||
throw new libErrors.RequestError("Default mod pack not set on controller");
|
||||
}
|
||||
let modPack = this._master.modPacks.get(id);
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Default mod pack configured (${id}) does not exist`);
|
||||
}
|
||||
+189
-189
@@ -21,19 +21,19 @@ const routes = require("./routes");
|
||||
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
|
||||
|
||||
const queryLogTime = new libPrometheus.Summary(
|
||||
"clusterio_master_query_log_duration_seconds",
|
||||
"clusterio_controller_query_log_duration_seconds",
|
||||
"Time in seconds log queries took to execute."
|
||||
);
|
||||
|
||||
/**
|
||||
* Represents the connection to a control link
|
||||
*
|
||||
* @extends module:master/src/BaseConnection
|
||||
* @alias module:master/src/ControlConnection
|
||||
* @extends module:controller/src/BaseConnection
|
||||
* @alias module:controller/src/ControlConnection
|
||||
*/
|
||||
class ControlConnection extends BaseConnection {
|
||||
constructor(registerData, connector, master, user) {
|
||||
super("control", connector, master);
|
||||
constructor(registerData, connector, controller, user) {
|
||||
super("control", connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._version = registerData.version;
|
||||
@@ -44,9 +44,9 @@ class ControlConnection extends BaseConnection {
|
||||
*/
|
||||
this.user = user;
|
||||
|
||||
this.slaveSubscriptions = {
|
||||
this.hostSubscriptions = {
|
||||
all: false,
|
||||
slave_ids: [],
|
||||
host_ids: [],
|
||||
};
|
||||
|
||||
this.instanceSubscriptions = {
|
||||
@@ -77,8 +77,8 @@ class ControlConnection extends BaseConnection {
|
||||
this.logTransport = null;
|
||||
this.logSubscriptions = {
|
||||
all: false,
|
||||
master: false,
|
||||
slave_ids: [],
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
};
|
||||
|
||||
@@ -92,101 +92,101 @@ class ControlConnection extends BaseConnection {
|
||||
logger.remove(this.logTransport);
|
||||
}
|
||||
if (this.ws_dumper) {
|
||||
this._master.debugEvents.off("message", this.ws_dumper);
|
||||
this._controller.debugEvents.off("message", this.ws_dumper);
|
||||
}
|
||||
});
|
||||
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
for (let masterPlugin of this._master.plugins.values()) {
|
||||
masterPlugin.onControlConnectionEvent(this, event);
|
||||
for (let controllerPlugin of this._controller.plugins.values()) {
|
||||
controllerPlugin.onControlConnectionEvent(this, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getMasterConfigRequestHandler() {
|
||||
return { serialized_config: this._master.config.serialize("control") };
|
||||
async getControllerConfigRequestHandler() {
|
||||
return { serialized_config: this._controller.config.serialize("control") };
|
||||
}
|
||||
|
||||
async setMasterConfigFieldRequestHandler(message) {
|
||||
this._master.config.set(message.data.field, message.data.value, "control");
|
||||
async setControllerConfigFieldRequestHandler(message) {
|
||||
this._controller.config.set(message.data.field, message.data.value, "control");
|
||||
}
|
||||
|
||||
async setMasterConfigPropRequestHandler(message) {
|
||||
async setControllerConfigPropRequestHandler(message) {
|
||||
let { field, prop, value } = message.data;
|
||||
this._master.config.setProp(field, prop, value, "control");
|
||||
this._controller.config.setProp(field, prop, value, "control");
|
||||
}
|
||||
|
||||
async listSlavesRequestHandler(message) {
|
||||
async listHostsRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let slave of this._master.slaves.values()) {
|
||||
for (let host of this._controller.hosts.values()) {
|
||||
list.push({
|
||||
agent: slave.agent,
|
||||
version: slave.version,
|
||||
id: slave.id,
|
||||
name: slave.name,
|
||||
public_address: slave.public_address || null,
|
||||
connected: this._master.wsServer.slaveConnections.has(slave.id),
|
||||
agent: host.agent,
|
||||
version: host.version,
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
public_address: host.public_address || null,
|
||||
connected: this._controller.wsServer.hostConnections.has(host.id),
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async setSlaveSubscriptionsRequestHandler(message) {
|
||||
this.slaveSubscriptions = message.data;
|
||||
async setHostSubscriptionsRequestHandler(message) {
|
||||
this.hostSubscriptions = message.data;
|
||||
}
|
||||
|
||||
slaveUpdated(slave, update) {
|
||||
hostUpdated(host, update) {
|
||||
if (
|
||||
this.slaveSubscriptions.all
|
||||
|| this.slaveSubscriptions.slave_ids.includes(slave.id)
|
||||
this.hostSubscriptions.all
|
||||
|| this.hostSubscriptions.host_ids.includes(host.id)
|
||||
) {
|
||||
libLink.messages.slaveUpdate.send(this, update);
|
||||
libLink.messages.hostUpdate.send(this, update);
|
||||
}
|
||||
}
|
||||
|
||||
generateSlaveToken(slaveId) {
|
||||
generateHostToken(hostId) {
|
||||
return jwt.sign(
|
||||
{ aud: "slave", slave: slaveId },
|
||||
Buffer.from(this._master.config.get("master.auth_secret"), "base64")
|
||||
{ aud: "host", host: hostId },
|
||||
Buffer.from(this._controller.config.get("controller.auth_secret"), "base64")
|
||||
);
|
||||
}
|
||||
|
||||
async generateSlaveTokenRequestHandler(message) {
|
||||
let slaveId = message.data.slave_id;
|
||||
if (slaveId === null) {
|
||||
slaveId = Math.random() * 2**31 | 0;
|
||||
async generateHostTokenRequestHandler(message) {
|
||||
let hostId = message.data.host_id;
|
||||
if (hostId === null) {
|
||||
hostId = Math.random() * 2**31 | 0;
|
||||
}
|
||||
return { token: this.generateSlaveToken(slaveId) };
|
||||
return { token: this.generateHostToken(hostId) };
|
||||
}
|
||||
|
||||
async createSlaveConfigRequestHandler(message) {
|
||||
let slaveConfig = new libConfig.SlaveConfig("control");
|
||||
await slaveConfig.init();
|
||||
async createHostConfigRequestHandler(message) {
|
||||
let hostConfig = new libConfig.HostConfig("control");
|
||||
await hostConfig.init();
|
||||
|
||||
slaveConfig.set("slave.master_url", this._master.getMasterUrl());
|
||||
hostConfig.set("host.controller_url", this._controller.getControllerUrl());
|
||||
if (message.data.id !== null) {
|
||||
slaveConfig.set("slave.id", message.data.id);
|
||||
hostConfig.set("host.id", message.data.id);
|
||||
}
|
||||
if (message.data.name !== null) {
|
||||
slaveConfig.set("slave.name", message.data.name);
|
||||
hostConfig.set("host.name", message.data.name);
|
||||
}
|
||||
if (message.data.generate_token) {
|
||||
this.user.checkPermission("core.slave.generate_token");
|
||||
slaveConfig.set("slave.master_token", this.generateSlaveToken(slaveConfig.get("slave.id")));
|
||||
this.user.checkPermission("core.host.generate_token");
|
||||
hostConfig.set("host.controller_token", this.generateHostToken(hostConfig.get("host.id")));
|
||||
}
|
||||
return { serialized_config: slaveConfig.serialize() };
|
||||
return { serialized_config: hostConfig.serialize() };
|
||||
}
|
||||
|
||||
async getInstanceRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let instance = this._master.getRequestInstance(id);
|
||||
let instance = this._controller.getRequestInstance(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
};
|
||||
@@ -194,11 +194,11 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async listInstancesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let instance of this._master.instances.values()) {
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
list.push({
|
||||
id: instance.config.get("instance.id"),
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
@@ -218,27 +218,27 @@ class ControlConnection extends BaseConnection {
|
||||
libLink.messages.instanceUpdate.send(this, {
|
||||
id: instance.config.get("instance.id"),
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// XXX should probably add a hook for slave reuqests?
|
||||
// XXX should probably add a hook for host reuqests?
|
||||
async createInstanceRequestHandler(message) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(message.data.serialized_config);
|
||||
|
||||
let instanceId = instanceConfig.get("instance.id");
|
||||
if (this._master.instances.has(instanceId)) {
|
||||
if (this._controller.instances.has(instanceId)) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} already exists`);
|
||||
}
|
||||
|
||||
// Add common settings for the Factorio server
|
||||
let settings = {
|
||||
"name": `${this._master.config.get("master.name")} - ${instanceConfig.get("instance.name")}`,
|
||||
"description": `Clusterio instance for ${this._master.config.get("master.name")}`,
|
||||
"name": `${this._controller.config.get("controller.name")} - ${instanceConfig.get("instance.name")}`,
|
||||
"description": `Clusterio instance for ${this._controller.config.get("controller.name")}`,
|
||||
"tags": ["clusterio"],
|
||||
"max_players": 0,
|
||||
"visibility": { "public": true, "lan": true },
|
||||
@@ -262,48 +262,48 @@ class ControlConnection extends BaseConnection {
|
||||
instanceConfig.set("factorio.settings", settings);
|
||||
|
||||
let instance = { config: instanceConfig, status: "unassigned" };
|
||||
this._master.instances.set(instanceId, instance);
|
||||
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, null);
|
||||
this._master.addInstanceHooks(instance);
|
||||
this._controller.instances.set(instanceId, instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, null);
|
||||
this._controller.addInstanceHooks(instance);
|
||||
}
|
||||
|
||||
async deleteInstanceRequestHandler(message, request) {
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
if (instance.config.get("instance.assigned_slave") !== null) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
if (instance.config.get("instance.assigned_host") !== null) {
|
||||
await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
this._master.instances.delete(message.data.instance_id);
|
||||
this._controller.instances.delete(message.data.instance_id);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
this._master.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
this._controller.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
async getInstanceConfigRequestHandler(message) {
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
return {
|
||||
serialized_config: instance.config.serialize("control"),
|
||||
};
|
||||
}
|
||||
|
||||
async updateInstanceConfig(instance) {
|
||||
let slaveId = instance.config.get("instance.assigned_slave");
|
||||
if (slaveId !== null) {
|
||||
let connection = this._master.wsServer.slaveConnections.get(slaveId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId !== null) {
|
||||
let connection = this._controller.wsServer.hostConnections.get(hostId);
|
||||
if (connection) {
|
||||
await libLink.messages.assignInstance.send(connection, {
|
||||
instance_id: instance.config.get("instance.id"),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setInstanceConfigFieldRequestHandler(message) {
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
if (message.data.field === "instance.assigned_slave") {
|
||||
throw new libErrors.RequestError("instance.assigned_slave must be set through the assign-slave interface");
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
if (message.data.field === "instance.assigned_host") {
|
||||
throw new libErrors.RequestError("instance.assigned_host must be set through the assign-host interface");
|
||||
}
|
||||
|
||||
if (message.data.field === "instance.id") {
|
||||
@@ -316,49 +316,49 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async setInstanceConfigPropRequestHandler(message) {
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
let { field, prop, value } = message.data;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
}
|
||||
|
||||
async assignInstanceCommandRequestHandler(message, request) {
|
||||
let { slave_id, instance_id } = message.data;
|
||||
let instance = this._master.getRequestInstance(instance_id);
|
||||
let { host_id, instance_id } = message.data;
|
||||
let instance = this._controller.getRequestInstance(instance_id);
|
||||
|
||||
// Check if target slave is connected
|
||||
let newSlaveConnection;
|
||||
if (slave_id !== null) {
|
||||
newSlaveConnection = this._master.wsServer.slaveConnections.get(slave_id);
|
||||
if (!newSlaveConnection) {
|
||||
// The case of the slave not getting the assign instance message
|
||||
// Check if target host is connected
|
||||
let newHostConnection;
|
||||
if (host_id !== null) {
|
||||
newHostConnection = this._controller.wsServer.hostConnections.get(host_id);
|
||||
if (!newHostConnection) {
|
||||
// The case of the host not getting the assign instance message
|
||||
// still have to be handled, so it's not a requirement that the
|
||||
// target slave be connected to the master while doing the
|
||||
// target host be connected to the controller while doing the
|
||||
// assignment, but it is IMHO a better user experience if this
|
||||
// is the case.
|
||||
throw new libErrors.RequestError("Target slave is not connected to the master server");
|
||||
throw new libErrors.RequestError("Target host is not connected to the controller");
|
||||
}
|
||||
}
|
||||
|
||||
// Unassign from currently assigned slave if it is connected.
|
||||
let currentAssignedSlave = instance.config.get("instance.assigned_slave");
|
||||
if (currentAssignedSlave !== null && slave_id !== currentAssignedSlave) {
|
||||
let oldSlaveConnection = this._master.wsServer.slaveConnections.get(currentAssignedSlave);
|
||||
if (oldSlaveConnection && !oldSlaveConnection.connector.closing) {
|
||||
await libLink.messages.unassignInstance.send(oldSlaveConnection, { instance_id });
|
||||
// Unassign from currently assigned host if it is connected.
|
||||
let currentAssignedHost = instance.config.get("instance.assigned_host");
|
||||
if (currentAssignedHost !== null && host_id !== currentAssignedHost) {
|
||||
let oldHostConnection = this._controller.wsServer.hostConnections.get(currentAssignedHost);
|
||||
if (oldHostConnection && !oldHostConnection.connector.closing) {
|
||||
await libLink.messages.unassignInstance.send(oldHostConnection, { instance_id });
|
||||
}
|
||||
}
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_slave", slave_id);
|
||||
if (slave_id !== null) {
|
||||
await libLink.messages.assignInstance.send(newSlaveConnection, {
|
||||
instance.config.set("instance.assigned_host", host_id);
|
||||
if (host_id !== null) {
|
||||
await libLink.messages.assignInstance.send(newHostConnection, {
|
||||
instance_id,
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
this._master.instanceUpdated(instance);
|
||||
this._controller.instanceUpdated(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,18 +377,18 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async downloadSaveRequestHandler(message) {
|
||||
let { instance_id, save } = message.data;
|
||||
let stream = await routes.createProxyStream(this._master.app);
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = save;
|
||||
|
||||
let ready = new Promise((resolve, reject) => {
|
||||
stream.events.on("source", resolve);
|
||||
stream.events.on("timeout", () => reject(
|
||||
new libErrors.RequestError("Timed out establishing stream from slave")
|
||||
new libErrors.RequestError("Timed out establishing stream from host")
|
||||
));
|
||||
});
|
||||
ready.catch(() => {});
|
||||
|
||||
await this._master.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save,
|
||||
@@ -409,35 +409,35 @@ class ControlConnection extends BaseConnection {
|
||||
if (instance_id === target_instance_id) {
|
||||
throw new libErrors.RequestError("Source and target instance may not be the same");
|
||||
}
|
||||
let sourceInstance = this._master.getRequestInstance(instance_id);
|
||||
let targetInstance = this._master.getRequestInstance(target_instance_id);
|
||||
let sourceSlaveId = sourceInstance.config.get("instance.assigned_slave");
|
||||
let targetSlaveId = targetInstance.config.get("instance.assigned_slave");
|
||||
if (sourceSlaveId === null) {
|
||||
throw new libErrors.RequestError("Source instance is not assigned a slave");
|
||||
let sourceInstance = this._controller.getRequestInstance(instance_id);
|
||||
let targetInstance = this._controller.getRequestInstance(target_instance_id);
|
||||
let sourceHostId = sourceInstance.config.get("instance.assigned_host");
|
||||
let targetHostId = targetInstance.config.get("instance.assigned_host");
|
||||
if (sourceHostId === null) {
|
||||
throw new libErrors.RequestError("Source instance is not assigned a host");
|
||||
}
|
||||
if (targetSlaveId === null) {
|
||||
throw new libErrors.RequestError("Target instance is not assigned a slave");
|
||||
if (targetHostId === null) {
|
||||
throw new libErrors.RequestError("Target instance is not assigned a host");
|
||||
}
|
||||
|
||||
// Let slave handle request if source and target is on the same slave.
|
||||
if (sourceSlaveId === targetSlaveId) {
|
||||
// Let host handle request if source and target is on the same host.
|
||||
if (sourceHostId === targetHostId) {
|
||||
return await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
|
||||
// Check connectivity
|
||||
let sourceSlaveConnection = this._master.wsServer.slaveConnections.get(sourceSlaveId);
|
||||
if (!sourceSlaveConnection || sourceSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Source slave is not connected to the master server");
|
||||
let sourceHostConnection = this._controller.wsServer.hostConnections.get(sourceHostId);
|
||||
if (!sourceHostConnection || sourceHostConnection.closing) {
|
||||
throw new libErrors.RequestError("Source host is not connected to the controller");
|
||||
}
|
||||
|
||||
let targetSlaveConnection = this._master.wsServer.slaveConnections.get(targetSlaveId);
|
||||
if (!targetSlaveConnection || targetSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Target slave is not connected to the master server");
|
||||
let targetHostConnection = this._controller.wsServer.hostConnections.get(targetHostId);
|
||||
if (!targetHostConnection || targetHostConnection.closing) {
|
||||
throw new libErrors.RequestError("Target host is not connected to the controller");
|
||||
}
|
||||
|
||||
// Create stream to proxy from target to source
|
||||
let stream = await routes.createProxyStream(this._master.app);
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.events.on("timeout", () => {
|
||||
if (stream.source) {
|
||||
stream.source.destroy();
|
||||
@@ -448,10 +448,10 @@ class ControlConnection extends BaseConnection {
|
||||
// Ignore errors if not listening for them to avoid crash.
|
||||
stream.events.on("error", () => { /* ignore */ });
|
||||
|
||||
// Establish push from source slave to stream, this is done first to
|
||||
// ensure the file size is known prior to the target slave pull.
|
||||
// Establish push from source host to stream, this is done first to
|
||||
// ensure the file size is known prior to the target host pull.
|
||||
await Promise.all([
|
||||
this._master.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save: source_save,
|
||||
@@ -459,8 +459,8 @@ class ControlConnection extends BaseConnection {
|
||||
events.once(stream.events, "source"),
|
||||
]);
|
||||
|
||||
// Establish pull from target slave to stream and wait for completion.
|
||||
let result = await this._master.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
// Establish pull from target host to stream and wait for completion.
|
||||
let result = await this._controller.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: target_instance_id,
|
||||
stream_id: stream.id,
|
||||
filename: target_save,
|
||||
@@ -468,7 +468,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
// Delete source save if this is not a copy
|
||||
if (!copy) {
|
||||
await this._master.forwardRequestToInstance(libLink.messages.deleteSave, {
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.deleteSave, {
|
||||
instance_id,
|
||||
save: source_save,
|
||||
});
|
||||
@@ -478,7 +478,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async listModPacksRequestHandler(message) {
|
||||
return { list: [...this._master.modPacks.values()].map(pack => pack.toJSON()) };
|
||||
return { list: [...this._controller.modPacks.values()].map(pack => pack.toJSON()) };
|
||||
}
|
||||
|
||||
async setModPackSubscriptionsRequestHandler(message) {
|
||||
@@ -487,31 +487,31 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async createModPackRequestHandler(message) {
|
||||
let modPack = new libData.ModPack(message.data.mod_pack);
|
||||
if (this._master.modPacks.has(modPack.id)) {
|
||||
if (this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} already exist`);
|
||||
}
|
||||
this._master.modPacks.set(modPack.id, modPack);
|
||||
this._master.modPackUpdated(modPack);
|
||||
this._controller.modPacks.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async updateModPackRequestHandler(message) {
|
||||
let modPack = new libData.ModPack(message.data.mod_pack);
|
||||
if (!this._master.modPacks.has(modPack.id)) {
|
||||
if (!this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} does not exist`);
|
||||
}
|
||||
this._master.modPacks.set(modPack.id, modPack);
|
||||
this._master.modPackUpdated(modPack);
|
||||
this._controller.modPacks.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async deleteModPackRequestHandler(message) {
|
||||
let { id } = message.data;
|
||||
let modPack = this._master.modPacks.get(id);
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
modPack.isDeleted = true;
|
||||
this._master.modPacks.delete(id);
|
||||
this._master.modPackUpdated(modPack);
|
||||
this._controller.modPacks.delete(id);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
modPackUpdated(modPack) {
|
||||
@@ -526,7 +526,7 @@ class ControlConnection extends BaseConnection {
|
||||
async getModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._master.mods.get(filename);
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
throw new libErrors.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
@@ -536,7 +536,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async listModsRequestHandler(message) {
|
||||
return { list: [...this._master.mods.values()].map(mod => mod.toJSON()) };
|
||||
return { list: [...this._controller.mods.values()].map(mod => mod.toJSON()) };
|
||||
}
|
||||
|
||||
static termsMatchesMod(terms, mod) {
|
||||
@@ -575,7 +575,7 @@ class ControlConnection extends BaseConnection {
|
||||
let factorioVersion = message.data.factorio_version;
|
||||
|
||||
let results = new Map();
|
||||
for (let mod of this._master.mods.values()) {
|
||||
for (let mod of this._controller.mods.values()) {
|
||||
if (
|
||||
mod.factorioVersion !== factorioVersion
|
||||
|| !ControlConnection.termsMatchesMod(query.terms, mod)
|
||||
@@ -634,13 +634,13 @@ class ControlConnection extends BaseConnection {
|
||||
async downloadModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._master.mods.get(filename);
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
throw new libErrors.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
let modPath = path.join(this._master.config.get("master.mods_directory"), mod.filename);
|
||||
let modPath = path.join(this._controller.config.get("controller.mods_directory"), mod.filename);
|
||||
|
||||
let stream = await routes.createProxyStream(this._master.app);
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = mod.filename;
|
||||
stream.source = fs.createReadStream(modPath);
|
||||
stream.mime = "application/zip";
|
||||
@@ -650,7 +650,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async deleteModRequestHandler(message) {
|
||||
await this._master.deleteMod(message.data.name, message.data.version);
|
||||
await this._controller.deleteMod(message.data.name, message.data.version);
|
||||
}
|
||||
|
||||
async setLogSubscriptionsRequestHandler(message) {
|
||||
@@ -668,29 +668,29 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
updateLogSubscriptions() {
|
||||
let { all, master, slave_ids, instance_ids } = this.logSubscriptions;
|
||||
if (all || master || slave_ids.length || instance_ids.length) {
|
||||
let { all, controller, host_ids, instance_ids } = this.logSubscriptions;
|
||||
if (all || controller || host_ids.length || instance_ids.length) {
|
||||
if (!this.logTransport) {
|
||||
this.logTransport = new libLoggingUtils.LinkTransport({ link: this });
|
||||
this._master.clusterLogger.add(this.logTransport);
|
||||
this._controller.clusterLogger.add(this.logTransport);
|
||||
}
|
||||
this.logTransport.filter = logFilter(this.logSubscriptions);
|
||||
|
||||
} else if (this.logTransport) {
|
||||
this._master.clusterLogger.remove(this.logTransport);
|
||||
this._controller.clusterLogger.remove(this.logTransport);
|
||||
this.logTransport = null;
|
||||
}
|
||||
}
|
||||
|
||||
async queryLogRequestHandler(message) {
|
||||
let observeDuration = queryLogTime.startTimer();
|
||||
let { all, master, slave_ids, instance_ids } = message.data;
|
||||
let { all, controller, host_ids, instance_ids } = message.data;
|
||||
|
||||
let log;
|
||||
if (!all && master && !slave_ids.length && !instance_ids.length) {
|
||||
log = await this._master.queryMasterLog(message.data);
|
||||
if (!all && controller && !host_ids.length && !instance_ids.length) {
|
||||
log = await this._controller.queryControllerLog(message.data);
|
||||
} else {
|
||||
log = await this._master.queryClusterLog(message.data);
|
||||
log = await this._controller.queryClusterLog(message.data);
|
||||
}
|
||||
|
||||
observeDuration();
|
||||
@@ -711,7 +711,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async listRolesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let role of this._master.userManager.roles.values()) {
|
||||
for (let role of this._controller.userManager.roles.values()) {
|
||||
list.push({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
@@ -723,17 +723,17 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async createRoleRequestHandler(message) {
|
||||
let lastId = Math.max.apply(null, [...this._master.userManager.roles.keys()]);
|
||||
let lastId = Math.max.apply(null, [...this._controller.userManager.roles.keys()]);
|
||||
|
||||
// Start at 5 to leave space for future default roles
|
||||
let id = Math.max(5, lastId+1);
|
||||
this._master.userManager.roles.set(id, new libUsers.Role({ id, ...message.data }));
|
||||
this._controller.userManager.roles.set(id, new libUsers.Role({ id, ...message.data }));
|
||||
return { id };
|
||||
}
|
||||
|
||||
async updateRoleRequestHandler(message) {
|
||||
let { id, name, description, permissions } = message.data;
|
||||
let role = this._master.userManager.roles.get(id);
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
}
|
||||
@@ -741,36 +741,36 @@ class ControlConnection extends BaseConnection {
|
||||
role.name = name;
|
||||
role.description = description;
|
||||
role.permissions = new Set(permissions);
|
||||
this._master.rolePermissionsUpdated(role);
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async grantDefaultRolePermissionsRequestHandler(message) {
|
||||
let role = this._master.userManager.roles.get(message.data.id);
|
||||
let role = this._controller.userManager.roles.get(message.data.id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${message.data.id} does not exist`);
|
||||
}
|
||||
|
||||
role.grantDefaultPermissions();
|
||||
this._master.rolePermissionsUpdated(role);
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async deleteRoleRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let role = this._master.userManager.roles.get(id);
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
}
|
||||
|
||||
this._master.userManager.roles.delete(id);
|
||||
for (let user of this._master.userManager.users.values()) {
|
||||
this._controller.userManager.roles.delete(id);
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
user.roles.delete(role);
|
||||
this._master.userPermissionsUpdated(user);
|
||||
this._controller.userPermissionsUpdated(user);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRequestHandler(message) {
|
||||
let name = message.data.name;
|
||||
let user = this._master.userManager.users.get(name);
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User ${name} does not exist`);
|
||||
}
|
||||
@@ -790,7 +790,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async listUsersRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let user of this._master.userManager.users.values()) {
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
list.push({
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
@@ -829,12 +829,12 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async createUserRequestHandler(message) {
|
||||
let user = this._master.userManager.createUser(message.data.name);
|
||||
this._master.userUpdated(user);
|
||||
let user = this._controller.userManager.createUser(message.data.name);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async revokeUserTokenRequestHandler(message) {
|
||||
let user = this._master.userManager.users.get(message.data.name);
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
@@ -843,23 +843,23 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
user.invalidateToken();
|
||||
for (let controlConnection of this._master.wsServer.controlConnections) {
|
||||
for (let controlConnection of this._controller.wsServer.controlConnections) {
|
||||
if (controlConnection.user.name === user.name) {
|
||||
controlConnection.connector.terminate();
|
||||
}
|
||||
}
|
||||
this._master.userUpdated(user);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async updateUserRolesRequestHandler(message) {
|
||||
let user = this._master.userManager.users.get(message.data.name);
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
|
||||
let resolvedRoles = new Set();
|
||||
for (let roleId of message.data.roles) {
|
||||
let role = this._master.userManager.roles.get(roleId);
|
||||
let role = this._controller.userManager.roles.get(roleId);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${roleId} does not exist`);
|
||||
}
|
||||
@@ -868,34 +868,34 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
user.roles = resolvedRoles;
|
||||
this._master.userPermissionsUpdated(user);
|
||||
this._master.userUpdated(user);
|
||||
this._controller.userPermissionsUpdated(user);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async setUserAdminRequestHandler(message) {
|
||||
let { name, create, admin } = message.data;
|
||||
let user = this._master.userManager.users.get(name);
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._master.userManager.createUser(name);
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
user.isAdmin = admin;
|
||||
this._master.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, admin }}, libLink.messages.adminlistUpdate);
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, admin }}, libLink.messages.adminlistUpdate);
|
||||
}
|
||||
|
||||
async setUserBannedRequestHandler(message) {
|
||||
let { name, create, banned, reason } = message.data;
|
||||
let user = this._master.userManager.users.get(name);
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._master.userManager.createUser(name);
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
@@ -903,45 +903,45 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
user.isBanned = banned;
|
||||
user.banReason = reason;
|
||||
this._master.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, banned, reason }}, libLink.messages.banlistUpdate);
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, banned, reason }}, libLink.messages.banlistUpdate);
|
||||
}
|
||||
|
||||
async setUserWhitelistedRequestHandler(message) {
|
||||
let { name, create, whitelisted } = message.data;
|
||||
let user = this._master.userManager.users.get(name);
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._master.userManager.createUser(name);
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
user.isWhitelisted = whitelisted;
|
||||
this._master.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, whitelisted }}, libLink.messages.whitelistUpdate);
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, whitelisted }}, libLink.messages.whitelistUpdate);
|
||||
}
|
||||
|
||||
async deleteUserRequestHandler(message) {
|
||||
let user = this._master.userManager.users.get(message.data.name);
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
|
||||
user.isDeleted = true;
|
||||
this._master.userManager.users.delete(message.data.name);
|
||||
this._master.userUpdated(user);
|
||||
this._controller.userManager.users.delete(message.data.name);
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
if (user.is_admin) {
|
||||
this.broadcastEventToSlaves({ data: { name, admin: false }}, libLink.messages.adminlistUpdate);
|
||||
this.broadcastEventToHosts({ data: { name, admin: false }}, libLink.messages.adminlistUpdate);
|
||||
}
|
||||
if (user.is_whitelisted) {
|
||||
this.broadcastEventToSlaves({ data: { name, whitelisted: false }}, libLink.messages.whitelistUpdate);
|
||||
this.broadcastEventToHosts({ data: { name, whitelisted: false }}, libLink.messages.whitelistUpdate);
|
||||
}
|
||||
if (user.is_banned) {
|
||||
this.broadcastEventToSlaves({ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate);
|
||||
this.broadcastEventToHosts({ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,7 +952,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
};
|
||||
this.connector._socket.clusterio_ignore_dump = true;
|
||||
this._master.debugEvents.on("message", this.ws_dumper);
|
||||
this._controller.debugEvents.on("message", this.ws_dumper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,21 +27,21 @@ const WsServer = require("./WsServer");
|
||||
|
||||
|
||||
const endpointDurationSummary = new libPrometheus.Summary(
|
||||
"clusterio_master_http_endpoint_duration_seconds",
|
||||
"clusterio_controller_http_endpoint_duration_seconds",
|
||||
"Time it took to respond to a an HTTP request",
|
||||
{ labels: ["route"] }
|
||||
);
|
||||
|
||||
const logSizeGauge = new libPrometheus.Gauge(
|
||||
"clusterio_master_log_bytes",
|
||||
"Size of all log files currently stored on the master."
|
||||
"clusterio_controller_log_bytes",
|
||||
"Size of all log files currently stored on the controller."
|
||||
);
|
||||
|
||||
/**
|
||||
* Manages all master related operations
|
||||
* @alias module:master/src/Master
|
||||
* Manages all controller related operations
|
||||
* @alias module:controller/src/Controller
|
||||
*/
|
||||
class Master {
|
||||
class Controller {
|
||||
constructor(clusterLogger, pluginInfos, configPath, config) {
|
||||
this.clusterLogger = clusterLogger;
|
||||
/**
|
||||
@@ -51,20 +51,20 @@ class Master {
|
||||
this.pluginInfos = pluginInfos;
|
||||
this.configPath = configPath;
|
||||
/**
|
||||
* Master config.
|
||||
* @type {module:lib/config.MasterConfig}
|
||||
* Controller config.
|
||||
* @type {module:lib/config.ControllerConfig}
|
||||
*/
|
||||
this.config = config;
|
||||
|
||||
this.app = express();
|
||||
this.app.locals.master = this;
|
||||
this.app.locals.controller = this;
|
||||
this.app.locals.streams = new Map();
|
||||
|
||||
/**
|
||||
* Mapping of slave id to slave info
|
||||
* Mapping of host id to host info
|
||||
* @type {Map<number, Object>}
|
||||
*/
|
||||
this.slaves = null;
|
||||
this.hosts = null;
|
||||
|
||||
/**
|
||||
* Mapping of instance id to instance info
|
||||
@@ -86,7 +86,7 @@ class Master {
|
||||
|
||||
/**
|
||||
* User and roles manager for the cluster
|
||||
* @type {module:master/src/UserManager}
|
||||
* @type {module:controller/src/UserManager}
|
||||
*/
|
||||
this.userManager = null;
|
||||
this.httpServer = null;
|
||||
@@ -96,13 +96,13 @@ class Master {
|
||||
|
||||
/**
|
||||
* Mapping of plugin name to loaded plugin
|
||||
* @type {Map<string, module:lib/plugin.BaseMasterPlugin>}
|
||||
* @type {Map<string, module:lib/plugin.BaseControllerPlugin>}
|
||||
*/
|
||||
this.plugins = new Map();
|
||||
|
||||
/**
|
||||
* WebSocket server
|
||||
* @type {module:master/src/WsServer}
|
||||
* @type {module:controller/src/WsServer}
|
||||
*/
|
||||
this.wsServer = new WsServer(this);
|
||||
|
||||
@@ -144,8 +144,8 @@ class Master {
|
||||
async logSize() {
|
||||
return (await Promise.all([
|
||||
libFileOps.directorySize(path.join(this.logDirectory, "cluster")),
|
||||
libFileOps.directorySize(path.join(this.logDirectory, "master")),
|
||||
libFileOps.directorySize(path.join(this.logDirectory, "slave")),
|
||||
libFileOps.directorySize(path.join(this.logDirectory, "controller")),
|
||||
libFileOps.directorySize(path.join(this.logDirectory, "host")),
|
||||
])).reduce((a, v) => a + v, 0);
|
||||
}
|
||||
|
||||
@@ -189,38 +189,38 @@ class Master {
|
||||
this.app.use(this.devMiddleware);
|
||||
}
|
||||
|
||||
let databaseDirectory = this.config.get("master.database_directory");
|
||||
let databaseDirectory = this.config.get("controller.database_directory");
|
||||
await fs.ensureDir(databaseDirectory);
|
||||
|
||||
this.slaves = await Master.loadSlaves(path.join(databaseDirectory, "slaves.json"));
|
||||
this.instances = await Master.loadInstances(path.join(databaseDirectory, "instances.json"));
|
||||
this.modPacks = await Master.loadModPacks(path.join(databaseDirectory, "mod-packs.json"));
|
||||
this.hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json"));
|
||||
this.instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json"));
|
||||
this.modPacks = await Controller.loadModPacks(path.join(databaseDirectory, "mod-packs.json"));
|
||||
this.userManager = new UserManager(this.config);
|
||||
await this.userManager.load(path.join(databaseDirectory, "users.json"));
|
||||
|
||||
let modsDirectory = this.config.get("master.mods_directory");
|
||||
let modsDirectory = this.config.get("controller.mods_directory");
|
||||
await fs.ensureDir(modsDirectory);
|
||||
this.mods = await Master.loadModInfos(modsDirectory);
|
||||
this.mods = await Controller.loadModInfos(modsDirectory);
|
||||
|
||||
this.config.on("fieldChanged", (group, field, prev) => {
|
||||
libPlugin.invokeHook(this.plugins, "onMasterConfigFieldChanged", group, field, prev);
|
||||
libPlugin.invokeHook(this.plugins, "onControllerConfigFieldChanged", group, field, prev);
|
||||
});
|
||||
for (let instance of this.instances.values()) {
|
||||
this.addInstanceHooks(instance);
|
||||
}
|
||||
|
||||
// Make sure we're actually going to listen on a port
|
||||
let httpPort = this.config.get("master.http_port");
|
||||
let httpsPort = this.config.get("master.https_port");
|
||||
let bindAddress = this.config.get("master.bind_address") || "";
|
||||
let httpPort = this.config.get("controller.http_port");
|
||||
let httpsPort = this.config.get("controller.https_port");
|
||||
let bindAddress = this.config.get("controller.bind_address") || "";
|
||||
if (!httpPort && !httpsPort) {
|
||||
logger.fatal("Error: at least one of http_port and https_port must be configured");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let tls_cert = this.config.get("master.tls_certificate");
|
||||
let tls_key = this.config.get("master.tls_private_key");
|
||||
let tls_cert = this.config.get("controller.tls_certificate");
|
||||
let tls_key = this.config.get("controller.tls_private_key");
|
||||
|
||||
if (httpsPort && (!tls_cert || !tls_key)) {
|
||||
throw new libErrors.StartupError(
|
||||
@@ -228,10 +228,10 @@ class Master {
|
||||
);
|
||||
}
|
||||
|
||||
Master.addAppRoutes(this.app, this.pluginInfos);
|
||||
Controller.addAppRoutes(this.app, this.pluginInfos);
|
||||
|
||||
if (!args.dev) {
|
||||
let manifest = await Master.loadJsonObject(path.join(__dirname, "..", "dist", "web", "manifest.json"));
|
||||
let manifest = await Controller.loadJsonObject(path.join(__dirname, "..", "dist", "web", "manifest.json"));
|
||||
if (!manifest["main.js"]) {
|
||||
logger.error("Missing main.js entry in dist/web/manifest.json");
|
||||
}
|
||||
@@ -272,12 +272,12 @@ class Master {
|
||||
logger.info(`Listening for HTTPS on port ${this.httpsServer.address().port}`);
|
||||
}
|
||||
|
||||
logger.info("Started master");
|
||||
logger.info("Started controller");
|
||||
this._state = "running";
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the master server.
|
||||
* Stops the controller.
|
||||
*
|
||||
* Save data and bring down the active connections. This is the reverse
|
||||
* of start and is valid at any point in time.
|
||||
@@ -299,7 +299,7 @@ class Master {
|
||||
async _stopInternal() {
|
||||
// This function should never throw.
|
||||
this._state = "stopping";
|
||||
logger.info("Stopping master");
|
||||
logger.info("Stopping controller");
|
||||
clearInterval(this.clusterLogBuildInterval);
|
||||
if (this.clusterLogIndex) {
|
||||
await this.clusterLogIndex.save();
|
||||
@@ -312,17 +312,17 @@ class Master {
|
||||
await new Promise((resolve, reject) => { this.devMiddleware.close(resolve); });
|
||||
}
|
||||
|
||||
let databaseDirectory = this.config.get("master.database_directory");
|
||||
if (this.slaves) {
|
||||
await Master.saveSlaves(path.join(databaseDirectory, "slaves.json"), this.slaves);
|
||||
let databaseDirectory = this.config.get("controller.database_directory");
|
||||
if (this.hosts) {
|
||||
await Controller.saveHosts(path.join(databaseDirectory, "hosts.json"), this.hosts);
|
||||
}
|
||||
|
||||
if (this.instances) {
|
||||
await Master.saveInstances(path.join(databaseDirectory, "instances.json"), this.instances);
|
||||
await Controller.saveInstances(path.join(databaseDirectory, "instances.json"), this.instances);
|
||||
}
|
||||
|
||||
if (this.modPacks) {
|
||||
await Master.saveModPacks(path.join(databaseDirectory, "mod-packs.json"), this.modPacks);
|
||||
await Controller.saveModPacks(path.join(databaseDirectory, "mod-packs.json"), this.modPacks);
|
||||
}
|
||||
|
||||
if (this.userManager) {
|
||||
@@ -344,7 +344,7 @@ class Master {
|
||||
logger.info("Goodbye");
|
||||
}
|
||||
|
||||
static async loadSlaves(filePath) {
|
||||
static async loadHosts(filePath) {
|
||||
let serialized;
|
||||
try {
|
||||
serialized = JSON.parse(await fs.readFile(filePath));
|
||||
@@ -365,8 +365,8 @@ class Master {
|
||||
return new Map(serialized);
|
||||
}
|
||||
|
||||
static async saveSlaves(filePath, slaves) {
|
||||
await libFileOps.safeOutputFile(filePath, JSON.stringify([...slaves.entries()], null, 4));
|
||||
static async saveHosts(filePath, hosts) {
|
||||
await libFileOps.safeOutputFile(filePath, JSON.stringify([...hosts.entries()], null, 4));
|
||||
}
|
||||
|
||||
static async loadInstances(filePath) {
|
||||
@@ -376,9 +376,9 @@ class Master {
|
||||
try {
|
||||
let serialized = JSON.parse(await fs.readFile(filePath));
|
||||
for (let serializedConfig of serialized) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(serializedConfig);
|
||||
let status = instanceConfig.get("instance.assigned_slave") === null ? "unassigned" : "unknown";
|
||||
let status = instanceConfig.get("instance.assigned_host") === null ? "unassigned" : "unknown";
|
||||
let instance = { config: instanceConfig, status };
|
||||
instances.set(instanceConfig.get("instance.id"), instance);
|
||||
}
|
||||
@@ -471,7 +471,7 @@ class Master {
|
||||
throw new Error(`Mod ${filename} does not exist`);
|
||||
}
|
||||
|
||||
let modsDirectory = this.config.get("master.mods_directory");
|
||||
let modsDirectory = this.config.get("controller.mods_directory");
|
||||
await fs.unlink(path.join(modsDirectory, filename));
|
||||
this.mods.delete(filename);
|
||||
mod.isDeleted = true;
|
||||
@@ -479,16 +479,16 @@ class Master {
|
||||
}
|
||||
|
||||
/**
|
||||
* Query master log
|
||||
* Query controller log
|
||||
*
|
||||
* @param {module:lib/logging_utils~QueryLogFilter} filter -
|
||||
* Filter to limit entries with. Note that only the master log can
|
||||
* Filter to limit entries with. Note that only the controller log can
|
||||
* be queried from this function.
|
||||
* @returns {Promise<Array<Object>>} log entries matching the filter
|
||||
*/
|
||||
async queryMasterLog(filter) {
|
||||
async queryControllerLog(filter) {
|
||||
return await libLoggingUtils.queryLog(
|
||||
path.join(this.logDirectory, "master"), filter,
|
||||
path.join(this.logDirectory, "controller"), filter,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,11 +530,11 @@ class Master {
|
||||
|
||||
// Add routes for the web interface
|
||||
for (let route of routes.webRoutes) {
|
||||
app.get(route, Master.serveWeb(route));
|
||||
app.get(route, Controller.serveWeb(route));
|
||||
}
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
for (let route of pluginInfo.routes || []) {
|
||||
app.get(route, Master.serveWeb(route));
|
||||
app.get(route, Controller.serveWeb(route));
|
||||
}
|
||||
|
||||
let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
|
||||
@@ -543,14 +543,14 @@ class Master {
|
||||
}
|
||||
}
|
||||
|
||||
slaveUpdated(slave) {
|
||||
hostUpdated(host) {
|
||||
let update = {
|
||||
agent: slave.agent,
|
||||
version: slave.version,
|
||||
id: slave.id,
|
||||
name: slave.name,
|
||||
public_address: slave.public_address || null,
|
||||
connected: this.wsServer.slaveConnections.has(slave.id),
|
||||
agent: host.agent,
|
||||
version: host.version,
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
public_address: host.public_address || null,
|
||||
connected: this.wsServer.hostConnections.has(host.id),
|
||||
};
|
||||
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
@@ -558,7 +558,7 @@ class Master {
|
||||
continue;
|
||||
}
|
||||
|
||||
controlConnection.slaveUpdated(slave, update);
|
||||
controlConnection.hostUpdated(host, update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ class Master {
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
try {
|
||||
let manifestPath = path.posix.join(pluginInfo.requirePath, "dist", "web", "manifest.json");
|
||||
pluginInfo.manifest = await Master.loadJsonObject(require.resolve(manifestPath), true);
|
||||
pluginInfo.manifest = await Controller.loadJsonObject(require.resolve(manifestPath), true);
|
||||
} catch (err) {
|
||||
logger.warn(`Unable to load dist/web/manifest.json for plugin ${pluginInfo.name}`);
|
||||
}
|
||||
@@ -693,15 +693,15 @@ class Master {
|
||||
continue;
|
||||
}
|
||||
|
||||
let MasterPluginClass = libPlugin.BaseMasterPlugin;
|
||||
let ControllerPluginClass = libPlugin.BaseControllerPlugin;
|
||||
try {
|
||||
if (pluginInfo.masterEntrypoint) {
|
||||
MasterPluginClass = await libPluginLoader.loadMasterPluginClass(pluginInfo);
|
||||
if (pluginInfo.controllerEntrypoint) {
|
||||
ControllerPluginClass = await libPluginLoader.loadControllerPluginClass(pluginInfo);
|
||||
}
|
||||
|
||||
let masterPlugin = new MasterPluginClass(pluginInfo, this, metrics, logger);
|
||||
await masterPlugin.init();
|
||||
this.plugins.set(pluginInfo.name, masterPlugin);
|
||||
let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics, logger);
|
||||
await controllerPlugin.init();
|
||||
this.plugins.set(pluginInfo.name, controllerPlugin);
|
||||
|
||||
} catch (err) {
|
||||
throw new libErrors.PluginError(pluginInfo.name, err);
|
||||
@@ -743,21 +743,21 @@ class Master {
|
||||
|
||||
|
||||
/**
|
||||
* Returns the URL needed to connect to the master server.
|
||||
* Returns the URL needed to connect to the controller.
|
||||
*
|
||||
* @returns {string} master URL.
|
||||
* @returns {string} controller URL.
|
||||
*/
|
||||
getMasterUrl() {
|
||||
return Master.calculateMasterUrl(this.config);
|
||||
getControllerUrl() {
|
||||
return Controller.calculateControllerUrl(this.config);
|
||||
}
|
||||
|
||||
static calculateMasterUrl(config) {
|
||||
let url = config.get("master.external_address");
|
||||
static calculateControllerUrl(config) {
|
||||
let url = config.get("controller.external_address");
|
||||
if (!url) {
|
||||
if (config.get("master.https_port")) {
|
||||
url = `https://localhost:${config.get("master.https_port")}/`;
|
||||
if (config.get("controller.https_port")) {
|
||||
url = `https://localhost:${config.get("controller.https_port")}/`;
|
||||
} else {
|
||||
url = `http://localhost:${config.get("master.http_port")}/`;
|
||||
url = `http://localhost:${config.get("controller.http_port")}/`;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
@@ -789,7 +789,7 @@ class Master {
|
||||
fs.readFile(path.join(__dirname, "..", "web", "index.html"), "utf8").then((content) => {
|
||||
res.type("text/html");
|
||||
res.send(content
|
||||
.replace(/__CLUSTER_NAME__/g, res.app.locals.master.config.get("master.name"))
|
||||
.replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name"))
|
||||
.replace(/__WEB_ROOT__/g, webRoot)
|
||||
.replace(/__STATIC_ROOT__/g, staticRoot)
|
||||
.replace(/__MAIN_BUNDLE__/g, mainBundle)
|
||||
@@ -801,9 +801,9 @@ class Master {
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the given request to the slave contaning the instance
|
||||
* Forward the given request to the host contaning the instance
|
||||
*
|
||||
* Finds the slave which the instance with the given instance ID is
|
||||
* Finds the host which the instance with the given instance ID is
|
||||
* currently assigned to and forwards the request to that instance.
|
||||
* The request data must have an instance_id parameter.
|
||||
*
|
||||
@@ -815,21 +815,21 @@ class Master {
|
||||
*/
|
||||
async forwardRequestToInstance(request, data) {
|
||||
let instance = this.getRequestInstance(data.instance_id);
|
||||
let slaveId = instance.config.get("instance.assigned_slave");
|
||||
if (slaveId === null) {
|
||||
throw new libErrors.RequestError("Instance is not assigned to a slave");
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
throw new libErrors.RequestError("Instance is not assigned to a host");
|
||||
}
|
||||
|
||||
let connection = this.wsServer.slaveConnections.get(slaveId);
|
||||
let connection = this.wsServer.hostConnections.get(hostId);
|
||||
if (!connection) {
|
||||
throw new libErrors.RequestError("Slave containing instance is not connected");
|
||||
throw new libErrors.RequestError("Host containing instance is not connected");
|
||||
}
|
||||
if (request.plugin && !connection.plugins.has(request.plugin)) {
|
||||
throw new libErrors.RequestError(`Slave containing instance does not have ${request.plugin} plugin`);
|
||||
throw new libErrors.RequestError(`Host containing instance does not have ${request.plugin} plugin`);
|
||||
}
|
||||
|
||||
return await request.send(connection, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Master;
|
||||
module.exports = Controller;
|
||||
+61
-59
@@ -9,14 +9,14 @@ const BaseConnection = require("./BaseConnection");
|
||||
|
||||
|
||||
/**
|
||||
* Represents the connection to a slave
|
||||
* Represents the connection to a host
|
||||
*
|
||||
* @extends module:master/src/BaseConnection
|
||||
* @alias module:master/src/SlaveConnection
|
||||
* @extends module:controller/src/BaseConnection
|
||||
* @alias module:controller/src/HostConnection
|
||||
*/
|
||||
class SlaveConnection extends BaseConnection {
|
||||
constructor(registerData, connector, master) {
|
||||
super("slave", connector, master);
|
||||
class HostConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller) {
|
||||
super("host", connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._id = registerData.id;
|
||||
@@ -25,7 +25,7 @@ class SlaveConnection extends BaseConnection {
|
||||
this.plugins = new Map(Object.entries(registerData.plugins));
|
||||
this._checkPluginVersions();
|
||||
|
||||
this._master.slaves.set(this._id, {
|
||||
this._controller.hosts.set(this._id, {
|
||||
agent: this._agent,
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
@@ -37,75 +37,75 @@ class SlaveConnection extends BaseConnection {
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
this.connector.on(event, () => {
|
||||
for (let plugin of this._master.plugins.values()) {
|
||||
plugin.onSlaveConnectionEvent(this, event);
|
||||
for (let plugin of this._controller.plugins.values()) {
|
||||
plugin.onHostConnectionEvent(this, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.connector.on("close", () => {
|
||||
// Update status to unknown for instances on this slave.
|
||||
for (let instance of this._master.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_slave") !== this._id) {
|
||||
// Update status to unknown for instances on this host.
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_host") !== this._id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "unknown";
|
||||
this._master.instanceUpdated(instance);
|
||||
libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
this._controller.instanceUpdated(instance);
|
||||
libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_checkPluginVersions() {
|
||||
let pluginInfos = new Map(this._master.pluginInfos.map(i => [i.name, i]));
|
||||
let pluginInfos = new Map(this._controller.pluginInfos.map(i => [i.name, i]));
|
||||
for (let [name, version] of this.plugins) {
|
||||
let info = pluginInfos.get(name);
|
||||
if (!info) {
|
||||
logger.warn(`Slave ${this._name} has plugin ${name} ${version} which the master does not have`);
|
||||
logger.warn(`Host ${this._name} has plugin ${name} ${version} which the controller does not have`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.version !== version) {
|
||||
logger.warn(
|
||||
`Slave ${this._name} has plugin ${name} ${version} which does not match the version of this ` +
|
||||
`plugin on the master (${info.version})`
|
||||
`Host ${this._name} has plugin ${name} ${version} which does not match the version of this ` +
|
||||
`plugin on the controller (${info.version})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let [name, info] of pluginInfos) {
|
||||
if (!this.plugins.has(name)) {
|
||||
logger.warn(`Slave ${this._name} is missing plugin ${name} ${info.version}`);
|
||||
logger.warn(`Host ${this._name} is missing plugin ${name} ${info.version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ID of the slave this connection is connected to
|
||||
* ID of the host this connection is connected to
|
||||
*
|
||||
* @returns {number} slave ID.
|
||||
* @returns {number} host ID.
|
||||
*/
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
async instanceStatusChangedEventHandler(message, event) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
let instance = this._controller.instances.get(message.data.instance_id);
|
||||
|
||||
// It's possible to get updates from an instance that does not exist
|
||||
// or is not assigned to the slave it originated from if it was
|
||||
// reassigned or deleted while the connection to the slave it was
|
||||
// or is not assigned to the host it originated from if it was
|
||||
// reassigned or deleted while the connection to the host it was
|
||||
// originally on was down at the time.
|
||||
if (!instance || instance.config.get("instance.assigned_slave") !== this._id) {
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
logger.warn(`Got bogus update for instance id ${message.data.instance_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// We may receive status changed where the status hasn't changed
|
||||
// from our perspective if the connection was down at the time it
|
||||
// changed. Slaves also send status updates on assignInstance which
|
||||
// changed. Hosts also send status updates on assignInstance which
|
||||
// for hacky reason is also used to push config changes and
|
||||
// restablish status after a connection loss.
|
||||
if (instance.status === message.data.status) {
|
||||
@@ -116,56 +116,58 @@ class SlaveConnection extends BaseConnection {
|
||||
instance.status = message.data.status;
|
||||
instance.game_port = message.data.game_port;
|
||||
logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
|
||||
this._master.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
this._controller.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
async updateInstancesRequestHandler(message) {
|
||||
// Push updated instance configs
|
||||
for (let instance of this._master.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_slave") === this._id) {
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_host") === this._id) {
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instance.config.get("instance.id"),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign instances the slave has but master does not
|
||||
// Assign instances the host has but controller does not
|
||||
for (let instanceData of message.data.instances) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
await instanceConfig.load(instanceData.serialized_config, "slave");
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(instanceData.serialized_config, "host");
|
||||
|
||||
let masterInstance = this._master.instances.get(instanceConfig.get("instance.id"));
|
||||
if (masterInstance) {
|
||||
let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id"));
|
||||
if (controllerInstance) {
|
||||
// Check if this instance is assigned somewhere else.
|
||||
if (masterInstance.config.get("instance.assigned_slave") !== this._id) {
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this._id) {
|
||||
await libLink.messages.unassignInstance.send(this, {
|
||||
instance_id: masterInstance.config.get("instance.id"),
|
||||
instance_id: controllerInstance.config.get("instance.id"),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Already have this instance, update state instead
|
||||
if (masterInstance.status !== instanceData.status) {
|
||||
let prev = masterInstance.status;
|
||||
masterInstance.status = instanceData.status;
|
||||
if (controllerInstance.status !== instanceData.status) {
|
||||
let prev = controllerInstance.status;
|
||||
controllerInstance.status = instanceData.status;
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
this._master.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", masterInstance, prev);
|
||||
this._controller.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(
|
||||
this._controller.plugins, "onInstanceStatusChanged", controllerInstance, prev
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceConfig.set("instance.assigned_slave", this._id);
|
||||
instanceConfig.set("instance.assigned_host", this._id);
|
||||
let newInstance = { config: instanceConfig, status: instanceData.status };
|
||||
this._master.instances.set(instanceConfig.get("instance.id"), newInstance);
|
||||
this._master.addInstanceHooks(newInstance);
|
||||
this._controller.instances.set(instanceConfig.get("instance.id"), newInstance);
|
||||
this._controller.addInstanceHooks(newInstance);
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize("slave"),
|
||||
serialized_config: instanceConfig.serialize("host"),
|
||||
});
|
||||
await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", newInstance, null);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", newInstance, null);
|
||||
}
|
||||
|
||||
// Push lists to make sure they are in sync.
|
||||
@@ -173,7 +175,7 @@ class SlaveConnection extends BaseConnection {
|
||||
let banlist = [];
|
||||
let whitelist = [];
|
||||
|
||||
for (let user of this._master.userManager.users.values()) {
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
if (user.isAdmin) {
|
||||
adminlist.push(user.name);
|
||||
}
|
||||
@@ -189,22 +191,22 @@ class SlaveConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async saveListUpdateEventHandler(message) {
|
||||
this._master.saveListUpdate(message.data);
|
||||
this._controller.saveListUpdate(message.data);
|
||||
}
|
||||
|
||||
async logMessageEventHandler(message) {
|
||||
this._master.clusterLogger.log({
|
||||
this._controller.clusterLogger.log({
|
||||
...message.data.info,
|
||||
slave_id: this._id,
|
||||
slave_name: this._name,
|
||||
host_id: this._id,
|
||||
host_name: this._name,
|
||||
});
|
||||
}
|
||||
|
||||
async playerEventEventHandler(message) {
|
||||
let { instance_id, name, type, stats } = message.data;
|
||||
let user = this._master.userManager.users.get(name);
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
user = this._master.userManager.createUser(name);
|
||||
user = this._controller.userManager.createUser(name);
|
||||
}
|
||||
|
||||
if (type === "join") {
|
||||
@@ -214,12 +216,12 @@ class SlaveConnection extends BaseConnection {
|
||||
}
|
||||
user.instanceStats.set(instance_id, new PlayerStats(stats));
|
||||
user.recalculatePlayerStats();
|
||||
this._master.userUpdated(user);
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
delete message.data.stats;
|
||||
let instance = this._master.instances.get(instance_id);
|
||||
await libPlugin.invokeHook(this._master.plugins, "onPlayerEvent", instance, message.data);
|
||||
let instance = this._controller.instances.get(instance_id);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPlayerEvent", instance, message.data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SlaveConnection;
|
||||
module.exports = HostConnection;
|
||||
@@ -17,7 +17,7 @@ const https = require("https");
|
||||
* // in the shutdown logic
|
||||
* await serverCloser.close();
|
||||
*
|
||||
* @alias module:master/src/HttpCloser
|
||||
* @alias module:controller/src/HttpCloser
|
||||
*/
|
||||
class HttpCloser {
|
||||
/**
|
||||
@@ -6,7 +6,7 @@ const libUsers = require("@clusterio/lib/users");
|
||||
|
||||
/**
|
||||
* Manages users and roles
|
||||
* @alias module:master/src/UserManager
|
||||
* @alias module:controller/src/UserManager
|
||||
*/
|
||||
class UserManager {
|
||||
constructor(config) {
|
||||
@@ -79,7 +79,7 @@ class UserManager {
|
||||
throw new Error(`User '${name}' already exists`);
|
||||
}
|
||||
|
||||
let defaultRoleId = this._config.get("master.default_role_id");
|
||||
let defaultRoleId = this._config.get("controller.default_role_id");
|
||||
let user = new libUsers.User({ name, roles: [defaultRoleId] }, this.roles);
|
||||
this.users.set(name, user);
|
||||
return user;
|
||||
@@ -9,41 +9,41 @@ const libSchema = require("@clusterio/lib/schema");
|
||||
|
||||
const ControlConnection = require("./ControlConnection");
|
||||
const packageVersion = require("../package").version;
|
||||
const SlaveConnection = require("./SlaveConnection");
|
||||
const HostConnection = require("./HostConnection");
|
||||
const WsServerConnector = require("./WsServerConnector");
|
||||
|
||||
|
||||
const wsMessageCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_websocket_message_total",
|
||||
"How many messages have been received over WebSocket on the master server",
|
||||
"clusterio_controller_websocket_message_total",
|
||||
"How many messages have been received over WebSocket on the controller",
|
||||
{ labels: ["direction"] }
|
||||
);
|
||||
|
||||
const wsConnectionsCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_websocket_connections_total",
|
||||
"How many WebSocket connections have been initiated on the master server"
|
||||
"clusterio_controller_websocket_connections_total",
|
||||
"How many WebSocket connections have been initiated on the controller"
|
||||
);
|
||||
|
||||
const wsRejectedConnectionsCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_websocket_rejected_connections_total",
|
||||
"How many WebSocket connections have been rejected during the handshake on the master server"
|
||||
"clusterio_controller_websocket_rejected_connections_total",
|
||||
"How many WebSocket connections have been rejected during the handshake on the controller"
|
||||
);
|
||||
|
||||
/**
|
||||
* WebSocket server
|
||||
* @alias module:master/src/WsServer
|
||||
* @alias module:controller/src/WsServer
|
||||
*/
|
||||
class WsServer {
|
||||
constructor(master) {
|
||||
this.master = master;
|
||||
constructor(controller) {
|
||||
this.controller = controller;
|
||||
|
||||
this.stopAcceptingNewSessions = false;
|
||||
|
||||
/** @type {Array<module:master/src/ControlConnection>} */
|
||||
/** @type {Array<module:controller/src/ControlConnection>} */
|
||||
this.controlConnections = [];
|
||||
/** @type {Map<number, module:master/src/SlaveConnection>} */
|
||||
this.slaveConnections = new Map();
|
||||
/** @type {Map<number, module:master/src/WsServerConnector>} */
|
||||
/** @type {Map<number, module:controller/src/HostConnection>} */
|
||||
this.hostConnections = new Map();
|
||||
/** @type {Map<number, module:controller/src/WsServerConnector>} */
|
||||
this.activeConnectors = new Map();
|
||||
/** @type {Set<module:ws>} */
|
||||
this.pendingSockets = new Set();
|
||||
@@ -68,8 +68,8 @@ class WsServer {
|
||||
disconnectTasks.push(controlConnection.disconnect(1001, "Server Quit"));
|
||||
}
|
||||
|
||||
for (let slaveConnection of this.slaveConnections.values()) {
|
||||
disconnectTasks.push(slaveConnection.disconnect(1001, "Server Quit"));
|
||||
for (let hostConnection of this.hostConnections.values()) {
|
||||
disconnectTasks.push(hostConnection.disconnect(1001, "Server Quit"));
|
||||
}
|
||||
|
||||
logger.info(`WsServer | Waiting for ${disconnectTasks.length} connectors to close`);
|
||||
@@ -104,21 +104,21 @@ class WsServer {
|
||||
socket.on("message", (message) => {
|
||||
wsMessageCounter.labels("in").inc();
|
||||
if (!socket.clusterio_ignore_dump) {
|
||||
this.master.debugEvents.emit("message", { direction: "in", content: message });
|
||||
this.controller.debugEvents.emit("message", { direction: "in", content: message });
|
||||
}
|
||||
});
|
||||
let originalSend = socket.send;
|
||||
socket.send = (...args) => {
|
||||
wsMessageCounter.labels("out").inc();
|
||||
if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) {
|
||||
this.master.debugEvents.emit("message", { direction: "out", content: args[0] });
|
||||
this.controller.debugEvents.emit("message", { direction: "out", content: args[0] });
|
||||
}
|
||||
return originalSend.call(socket, ...args);
|
||||
};
|
||||
|
||||
// Start connection handshake.
|
||||
let loadedPlugins = {};
|
||||
for (let [name, plugin] of this.master.plugins) {
|
||||
for (let [name, plugin] of this.controller.plugins) {
|
||||
loadedPlugins[name] = plugin.info.version;
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ ${err.stack}`
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
data.session_token,
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: this.sessionAud }
|
||||
);
|
||||
|
||||
@@ -212,25 +212,27 @@ ${err.stack}`
|
||||
// Validate token
|
||||
let user;
|
||||
try {
|
||||
if (type === "register_slave") {
|
||||
if (type === "register_host") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "slave" }
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
{ audience: ["host", "slave"] }
|
||||
);
|
||||
|
||||
if (tokenPayload.slave !== data.id) {
|
||||
throw new Error("missmatched slave id");
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
if ((tokenPayload.host !== undefined ? tokenPayload.host : tokenPayload.slave) !== data.id) {
|
||||
throw new Error("missmatched host id");
|
||||
}
|
||||
|
||||
} else if (type === "register_control") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
user = this.master.userManager.users.get(tokenPayload.user);
|
||||
user = this.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
@@ -251,10 +253,10 @@ ${err.stack}`
|
||||
this.nextSessionId += 1;
|
||||
let sessionToken = jwt.sign(
|
||||
{ aud: this.sessionAud, sid: sessionId },
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
);
|
||||
let sessionTimeout = this.master.config.get("master.session_timeout");
|
||||
let heartbeatInterval = this.master.config.get("master.heartbeat_interval");
|
||||
let sessionTimeout = this.controller.config.get("controller.session_timeout");
|
||||
let heartbeatInterval = this.controller.config.get("controller.heartbeat_interval");
|
||||
let connector = new WsServerConnector(sessionId, sessionTimeout, heartbeatInterval);
|
||||
this.activeConnectors.set(sessionId, connector);
|
||||
connector.on("close", () => {
|
||||
@@ -262,39 +264,39 @@ ${err.stack}`
|
||||
});
|
||||
|
||||
let additionalReadyData = {};
|
||||
if (type === "register_slave") {
|
||||
let connection = this.slaveConnections.get(data.id);
|
||||
if (type === "register_host") {
|
||||
let connection = this.hostConnections.get(data.id);
|
||||
if (connection) {
|
||||
logger.verbose(`WsServer | disconnecting existing connection for slave ${data.id}`);
|
||||
logger.verbose(`WsServer | disconnecting existing connection for host ${data.id}`);
|
||||
await connection.disconnect(1008, "Registered from another connection");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`WsServer | registered slave ${data.name} (${data.id}) using agent ${data.agent} ${data.version}`
|
||||
`WsServer | registered host ${data.name} (${data.id}) using agent ${data.agent} ${data.version}`
|
||||
);
|
||||
if (data.agent === "Clusterio Slave" && data.version !== packageVersion) {
|
||||
if (data.agent === "Clusterio Host" && data.version !== packageVersion) {
|
||||
logger.warn(
|
||||
`Slave ${data.name} (${data.id}) connected using version ${data.version} which does not match ` +
|
||||
`the version of the master is currently running (${packageVersion}). It may not work as expected.`
|
||||
`Host ${data.name} (${data.id}) connected using version ${data.version} which does not match the ` +
|
||||
`version of the controller is currently running (${packageVersion}). It may not work as expected.`
|
||||
);
|
||||
}
|
||||
|
||||
connection = new SlaveConnection(data, connector, this.master);
|
||||
connection = new HostConnection(data, connector, this.controller);
|
||||
connector.on("close", () => {
|
||||
if (this.slaveConnections.get(data.id) === connection) {
|
||||
this.slaveConnections.delete(data.id);
|
||||
this.master.slaveUpdated(this.master.slaves.get(data.id));
|
||||
if (this.hostConnections.get(data.id) === connection) {
|
||||
this.hostConnections.delete(data.id);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
} else {
|
||||
logger.warn("Unlisted SlaveConnection closed");
|
||||
logger.warn("Unlisted HostConnection closed");
|
||||
}
|
||||
});
|
||||
this.slaveConnections.set(data.id, connection);
|
||||
this.master.slaveUpdated(this.master.slaves.get(data.id));
|
||||
this.hostConnections.set(data.id, connection);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
|
||||
|
||||
} else if (type === "register_control") {
|
||||
logger.verbose(`WsServer | registered control from ${req.socket.remoteAddress}`);
|
||||
let connection = new ControlConnection(data, connector, this.master, user);
|
||||
let connection = new ControlConnection(data, connector, this.controller, user);
|
||||
connector.on("close", () => {
|
||||
let index = this.controlConnections.indexOf(connection);
|
||||
if (index !== -1) {
|
||||
+2
-2
@@ -6,10 +6,10 @@ const { logger } = require("@clusterio/lib/logging");
|
||||
|
||||
|
||||
/**
|
||||
* Connector for master server connections
|
||||
* Connector for controller connections
|
||||
*
|
||||
* @extends module:lib/link.WebSocketBaseConnector
|
||||
* @alias module:master/src/WsServerConnector
|
||||
* @alias module:controller/src/WsServerConnector
|
||||
*/
|
||||
class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
constructor(sessionId, sessionTimeout, heartbeatInterval) {
|
||||
@@ -4,7 +4,7 @@ const libPrometheus = require("@clusterio/lib/prometheus");
|
||||
const util = require("util");
|
||||
|
||||
exports.endpointHitCounter = new libPrometheus.Counter(
|
||||
"clusterio_master_http_endpoint_hits_total",
|
||||
"clusterio_controller_http_endpoint_hits_total",
|
||||
"How many requests a particular HTTP endpoint has gotten",
|
||||
{ labels: ["route"], register: false }
|
||||
);
|
||||
@@ -40,7 +40,7 @@ function mergeSamples(destinationResult, sourceResult) {
|
||||
// Prometheus polling endpoint
|
||||
async function getMetrics(req, res, next) {
|
||||
let results = [];
|
||||
let pluginResults = await libPlugin.invokeHook(req.app.locals.master.plugins, "onMetrics");
|
||||
let pluginResults = await libPlugin.invokeHook(req.app.locals.controller.plugins, "onMetrics");
|
||||
for (let metricIterator of pluginResults) {
|
||||
for await (let metric of metricIterator) {
|
||||
results.push(metric);
|
||||
@@ -48,15 +48,15 @@ async function getMetrics(req, res, next) {
|
||||
}
|
||||
|
||||
let requests = [];
|
||||
let timeout = req.app.locals.master.config.get("master.metrics_timeout") * 1000;
|
||||
for (let slaveConnection of req.app.locals.master.wsServer.slaveConnections.values()) {
|
||||
if (!slaveConnection.connected) {
|
||||
let timeout = req.app.locals.controller.config.get("controller.metrics_timeout") * 1000;
|
||||
for (let hostConnection of req.app.locals.controller.wsServer.hostConnections.values()) {
|
||||
if (!hostConnection.connected) {
|
||||
continue;
|
||||
}
|
||||
requests.push(libHelpers.timeout(
|
||||
libLink.messages.getMetrics.send(slaveConnection).catch(err => {
|
||||
libLink.messages.getMetrics.send(hostConnection).catch(err => {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
logger.error(`Unexpected error gathering metrics from slave:\n${err.stack}`);
|
||||
logger.error(`Unexpected error gathering metrics from host:\n${err.stack}`);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
@@ -80,7 +80,7 @@ async function getMetrics(req, res, next) {
|
||||
resultMap.set(result.metric.name, result);
|
||||
|
||||
} else {
|
||||
// Merge metrics received by multiple slaves
|
||||
// Merge metrics received by multiple hosts
|
||||
mergeSamples(resultMap.get(result.metric.name), result);
|
||||
}
|
||||
}
|
||||
@@ -98,10 +98,10 @@ async function getMetrics(req, res, next) {
|
||||
|
||||
function getPlugins(req, res) {
|
||||
let plugins = [];
|
||||
for (let pluginInfo of req.app.locals.master.pluginInfos) {
|
||||
for (let pluginInfo of req.app.locals.controller.pluginInfos) {
|
||||
let name = pluginInfo.name;
|
||||
let loaded = req.app.locals.master.plugins.has(name);
|
||||
let enabled = loaded && req.app.locals.master.config.group(pluginInfo.name).get("load_plugin");
|
||||
let loaded = req.app.locals.controller.plugins.has(name);
|
||||
let enabled = loaded && req.app.locals.controller.config.group(pluginInfo.name).get("load_plugin");
|
||||
let web = {};
|
||||
let devPlugins = req.app.locals.devPlugins;
|
||||
if (devPlugins && devPlugins.has(name)) {
|
||||
@@ -123,7 +123,7 @@ function getPlugins(req, res) {
|
||||
res.send(plugins);
|
||||
}
|
||||
|
||||
function validateSlaveToken(req, res, next) {
|
||||
function validateHostToken(req, res, next) {
|
||||
let token = req.header("x-access-token");
|
||||
if (!token) {
|
||||
res.sendStatus(401);
|
||||
@@ -133,8 +133,8 @@ function validateSlaveToken(req, res, next) {
|
||||
try {
|
||||
jwt.verify(
|
||||
token,
|
||||
Buffer.from(req.app.locals.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "slave" }
|
||||
Buffer.from(req.app.locals.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "host" }
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
@@ -155,10 +155,10 @@ function validateUserToken(req, res, next) {
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
token,
|
||||
Buffer.from(req.app.locals.master.config.get("master.auth_secret"), "base64"),
|
||||
Buffer.from(req.app.locals.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
let user = req.app.locals.master.userManager.users.get(tokenPayload.user);
|
||||
let user = req.app.locals.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
@@ -191,7 +191,7 @@ async function uploadExport(req, res) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
let modPack = res.app.locals.master.modPacks.get(modPackId);
|
||||
let modPack = res.app.locals.controller.modPacks.get(modPackId);
|
||||
if (!modPack) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
@@ -234,7 +234,7 @@ async function uploadExport(req, res) {
|
||||
|
||||
modPack.exportManifest = new libData.ExportManifest({ assets });
|
||||
modPack.fillDefaultSettings(settingPrototypes, logger);
|
||||
res.app.locals.master.modPackUpdated(modPack);
|
||||
res.app.locals.controller.modPackUpdated(modPack);
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
@@ -251,7 +251,7 @@ async function createProxyStream(app) {
|
||||
events: new events.EventEmitter(),
|
||||
timeout: setTimeout(() => {
|
||||
stream.events.emit("timeout");
|
||||
}, app.locals.master.config.get("master.proxy_stream_timeout") * 1000),
|
||||
}, app.locals.controller.config.get("controller.proxy_stream_timeout") * 1000),
|
||||
};
|
||||
stream.events.on("close", () => {
|
||||
clearTimeout(stream.timeout);
|
||||
@@ -356,13 +356,13 @@ async function uploadSave(req, res) {
|
||||
let timeout = new Promise((_, reject) => {
|
||||
proxyStream.events.on("timeout", () => {
|
||||
stream.resume();
|
||||
reject(new Error("Timed out establishing stream to slave"));
|
||||
reject(new Error("Timed out establishing stream to host"));
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
let result = await Promise.race([
|
||||
req.app.locals.master.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
req.app.locals.controller.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: instanceId,
|
||||
stream_id: proxyStream.id,
|
||||
filename,
|
||||
@@ -498,7 +498,7 @@ async function uploadMod(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modsDirectory = req.app.locals.master.config.get("master.mods_directory");
|
||||
const modsDirectory = req.app.locals.controller.config.get("controller.mods_directory");
|
||||
let tempFilename = filename.replace(/(\.zip)?$/, ".tmp.zip");
|
||||
try {
|
||||
|
||||
@@ -521,8 +521,8 @@ async function uploadMod(req, res) {
|
||||
|
||||
const modInfo = await libData.ModInfo.fromModFile(path.join(modsDirectory, tempFilename));
|
||||
await fs.rename(path.join(modsDirectory, tempFilename), path.join(modsDirectory, modInfo.filename));
|
||||
req.app.locals.master.mods.set(modInfo.filename, modInfo);
|
||||
req.app.locals.master.modUpdated(modInfo);
|
||||
req.app.locals.controller.mods.set(modInfo.filename, modInfo);
|
||||
req.app.locals.controller.modUpdated(modInfo);
|
||||
mods.push(modInfo.toJSON());
|
||||
|
||||
} catch (err) {
|
||||
@@ -609,7 +609,7 @@ function addRouteHandlers(app) {
|
||||
app.get("/metrics", (req, res, next) => getMetrics(req, res, next).catch(next));
|
||||
app.get("/api/plugins", getPlugins);
|
||||
app.put("/api/upload-export",
|
||||
validateSlaveToken,
|
||||
validateHostToken,
|
||||
(req, res, next) => uploadExport(req, res).catch(next)
|
||||
);
|
||||
app.put("/api/stream/:id", (req, res, next) => putStream(req, res).catch(next));
|
||||
@@ -624,12 +624,12 @@ function addRouteHandlers(app) {
|
||||
);
|
||||
}
|
||||
|
||||
// Routes used in the web interface and served by the master server
|
||||
// Routes used in the web interface and served by the controller
|
||||
const webRoutes = [
|
||||
"/",
|
||||
"/master",
|
||||
"/slaves",
|
||||
"/slaves/:id/view",
|
||||
"/controller",
|
||||
"/hosts",
|
||||
"/hosts/:id/view",
|
||||
"/instances",
|
||||
"/instances/:id/view",
|
||||
"/mods",
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "master",
|
||||
name: "controller",
|
||||
shared: {
|
||||
"@clusterio/lib": { singleton: true },
|
||||
"@clusterio/web_ui": { singleton: true },
|
||||
+210
-81
@@ -110,18 +110,18 @@ async function execFile(cmd, args) {
|
||||
});
|
||||
}
|
||||
|
||||
async function execMaster(args) {
|
||||
async function execController(args) {
|
||||
if (dev) {
|
||||
return await execFile("node", [path.join("packages", "master"), ...args]);
|
||||
return await execFile("node", [path.join("packages", "controller"), ...args]);
|
||||
}
|
||||
return await execFile(path.join("node_modules", ".bin", `clusteriomaster${scriptExt}`), args);
|
||||
return await execFile(path.join("node_modules", ".bin", `clusteriocontroller${scriptExt}`), args);
|
||||
}
|
||||
|
||||
async function execSlave(args) {
|
||||
async function execHost(args) {
|
||||
if (dev) {
|
||||
return await execFile("node", [path.join("packages", "slave"), ...args]);
|
||||
return await execFile("node", [path.join("packages", "host"), ...args]);
|
||||
}
|
||||
return await execFile(path.join("node_modules", ".bin", `clusterioslave${scriptExt}`), args);
|
||||
return await execFile(path.join("node_modules", ".bin", `clusteriohost${scriptExt}`), args);
|
||||
}
|
||||
|
||||
async function execCtl(args) {
|
||||
@@ -131,7 +131,7 @@ async function execCtl(args) {
|
||||
return await execFile(path.join("node_modules", ".bin", `clusterioctl${scriptExt}`), args);
|
||||
}
|
||||
|
||||
function validateSlaveToken(token) {
|
||||
function validateHostToken(token) {
|
||||
let parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new InstallError("Invalid token");
|
||||
@@ -144,7 +144,7 @@ function validateSlaveToken(token) {
|
||||
throw new InstallError("Invalid token");
|
||||
}
|
||||
|
||||
if (parsed.aud !== "slave" || !Number.isInteger(parsed.slave)) {
|
||||
if (parsed.aud !== "host" || !Number.isInteger(parsed.host)) {
|
||||
throw new InstallError("Invalid token");
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,127 @@ async function validateNotRoot(args) {
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateRename(args) {
|
||||
function rename(string) {
|
||||
return string.replace(/master/g, "controller").replace(/slave/g, "host");
|
||||
}
|
||||
|
||||
function renameConfig(config) {
|
||||
for (let group of config["groups"]) {
|
||||
group["name"] = rename(group["name"]);
|
||||
group["fields"] = Object.fromEntries(
|
||||
Object.entries(group["fields"]).map(([k, v]) => [rename(k), v])
|
||||
);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function migrateConfig(source, destination) {
|
||||
if (await fs.pathExists(source) && !await fs.pathExists(destination)) {
|
||||
await safeOutputFile(destination, JSON.stringify(
|
||||
renameConfig(JSON.parse(await fs.readFile(source))), null, 4
|
||||
));
|
||||
logger.info(`Migrated ${source} to ${destination}`);
|
||||
}
|
||||
}
|
||||
|
||||
await migrateConfig("config-master.json", "config-controller.json");
|
||||
await migrateConfig("config-slave.json", "config-host.json");
|
||||
|
||||
let instancesFile = path.join("database", "instances.json");
|
||||
if (await fs.pathExists(instancesFile)) {
|
||||
await safeOutputFile(instancesFile, JSON.stringify(
|
||||
JSON.parse(await fs.readFile(instancesFile)).map(renameConfig), null, 4
|
||||
));
|
||||
logger.info(`Migrated ${instancesFile}`);
|
||||
}
|
||||
|
||||
let usersFile = path.join("database", "users.json");
|
||||
if (await fs.pathExists(usersFile)) {
|
||||
let users = JSON.parse(await fs.readFile(usersFile));
|
||||
for (let role of users["roles"]) {
|
||||
role["permissions"] = role["permissions"].map(rename);
|
||||
}
|
||||
await safeOutputFile(usersFile, JSON.stringify(users, null, 4));
|
||||
logger.info(`Migrated ${usersFile}`);
|
||||
}
|
||||
|
||||
function renameLogLine(info) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(info).map(([k, v]) => [rename(k), v])
|
||||
);
|
||||
}
|
||||
|
||||
async function migrateLog(inputFile, outputFile) {
|
||||
let lineStream = new LineSplitter({ readableObjectMode: true });
|
||||
let fileStream = fs.createReadStream(inputFile);
|
||||
fileStream.pipe(lineStream);
|
||||
let lines = [];
|
||||
|
||||
for await (let inputLine of lineStream) {
|
||||
try {
|
||||
lines.push(Buffer.from(JSON.stringify(renameLogLine(JSON.parse(inputLine)))));
|
||||
lines.push(Buffer.from("\n"));
|
||||
} catch (err) {
|
||||
if (!(err instanceof SyntaxError)) {
|
||||
throw err;
|
||||
}
|
||||
logger.warn(`Invalid log line: ${inputLine.toString()}`);
|
||||
lines.push(inputLine);
|
||||
}
|
||||
}
|
||||
safeOutputFile(outputFile, Buffer.concat(lines));
|
||||
}
|
||||
|
||||
async function migrateLogsDir(inputLogs, outputLogs) {
|
||||
if (await fs.pathExists(inputLogs)) {
|
||||
logger.info(`Migrating ${inputLogs} to ${outputLogs}`);
|
||||
for (let logFile of await fs.readdir(inputLogs)) {
|
||||
if (!logFile.endsWith(".log")) {
|
||||
continue;
|
||||
}
|
||||
let inputFile = path.join(inputLogs, logFile);
|
||||
let outputFile = path.join(outputLogs, rename(logFile));
|
||||
if (!await fs.pathExists(outputFile)) {
|
||||
await migrateLog(inputFile, outputFile);
|
||||
logger.verbose(`Migrated ${inputFile} to ${outputFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await migrateLogsDir(path.join("logs", "master"), path.join("logs", "controller"));
|
||||
await migrateLogsDir(path.join("logs", "slave"), path.join("logs", "host"));
|
||||
if (
|
||||
await fs.pathExists(path.join("logs", "cluster"))
|
||||
&& !await fs.pathExists(path.join("logs", "cluster-prerename"))
|
||||
) {
|
||||
await fs.rename(path.join("logs", "cluster"), path.join("logs", "cluster-prerename"));
|
||||
await migrateLogsDir(path.join("logs", "cluster-prerename"), path.join("logs", "cluster"));
|
||||
}
|
||||
|
||||
if (!args.dev) {
|
||||
let pkg = JSON.parse(await fs.readFile("package.json"));
|
||||
if (pkg.dependencies) {
|
||||
let convert = ["@clusterio/master", "@clusterio/slave"];
|
||||
let uninstall = convert.filter(c => pkg.dependencies[c] !== undefined);
|
||||
let install = uninstall.map(rename);
|
||||
if (uninstall.length) {
|
||||
logger.info(`Replacing ${uninstall.join(" and ")}`);
|
||||
await execFile(`npm${scriptExt}`, ["install", ...install]);
|
||||
await execFile(`npm${scriptExt}`, ["uninstall", ...uninstall]);
|
||||
}
|
||||
}
|
||||
logger.info("Updating packages");
|
||||
await execFile(`npm${scriptExt}`, ["update"]);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Migration complete, you may now delete the following left over files and directories (if present):" +
|
||||
"\n- config-master.json\n- config-slave.json\n- logs/master\n- logs/slave\n- logs/cluster-prerename"
|
||||
);
|
||||
}
|
||||
|
||||
async function installClusterio(mode, plugins) {
|
||||
try {
|
||||
await fs.outputFile("package.json", JSON.stringify({
|
||||
@@ -194,11 +315,11 @@ async function installClusterio(mode, plugins) {
|
||||
}
|
||||
|
||||
let components = [];
|
||||
if (["standalone", "master"].includes(mode)) {
|
||||
components.push("@clusterio/master");
|
||||
if (["standalone", "controller"].includes(mode)) {
|
||||
components.push("@clusterio/controller");
|
||||
}
|
||||
if (["standalone", "slave"].includes(mode)) {
|
||||
components.push("@clusterio/slave");
|
||||
if (["standalone", "host"].includes(mode)) {
|
||||
components.push("@clusterio/host");
|
||||
}
|
||||
if (mode === "ctl") {
|
||||
components.push("@clusterio/ctl");
|
||||
@@ -250,22 +371,22 @@ async function groupIdToName(gid) {
|
||||
}
|
||||
|
||||
async function writeScripts(mode) {
|
||||
if (["standalone", "master"].includes(mode)) {
|
||||
if (["standalone", "controller"].includes(mode)) {
|
||||
if (process.platform === "win32") {
|
||||
await safeOutputFile(
|
||||
"run-master.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusteriomaster.cmd run\n"
|
||||
"run-controller.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusteriocontroller.cmd run\n"
|
||||
);
|
||||
} else {
|
||||
await safeOutputFile(
|
||||
"run-master.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusteriomaster run\n",
|
||||
"run-controller.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusteriocontroller run\n",
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await safeOutputFile(
|
||||
"systemd/clusteriomaster.service",
|
||||
"systemd/clusteriocontroller.service",
|
||||
`[Unit]
|
||||
Description=Clusterio Master
|
||||
Description=Clusterio Controller
|
||||
|
||||
[Service]
|
||||
User=${os.userInfo().username}
|
||||
@@ -273,7 +394,7 @@ Group=${await groupIdToName(os.userInfo().gid)}
|
||||
WorkingDirectory=${process.cwd()}
|
||||
KillMode=mixed
|
||||
KillSignal=SIGINT
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusteriomaster run --log-level=warn
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusteriocontroller run --log-level=warn
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -281,22 +402,22 @@ WantedBy=multi-user.target
|
||||
}
|
||||
}
|
||||
|
||||
if (["standalone", "slave"].includes(mode)) {
|
||||
if (["standalone", "host"].includes(mode)) {
|
||||
if (process.platform === "win32") {
|
||||
await safeOutputFile(
|
||||
"run-slave.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusterioslave.cmd run\n"
|
||||
"run-host.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusteriohost.cmd run\n"
|
||||
);
|
||||
} else {
|
||||
await safeOutputFile(
|
||||
"run-slave.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusterioslave run\n",
|
||||
"run-host.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusteriohost run\n",
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await safeOutputFile(
|
||||
"systemd/clusterioslave.service",
|
||||
"systemd/clusteriohost.service",
|
||||
`[Unit]
|
||||
Description=Clusterio Slave
|
||||
Description=Clusterio Host
|
||||
|
||||
[Service]
|
||||
User=${os.userInfo().username}
|
||||
@@ -304,7 +425,7 @@ Group=${await groupIdToName(os.userInfo().gid)}
|
||||
WorkingDirectory=${process.cwd()}
|
||||
KillMode=mixed
|
||||
KillSignal=SIGINT
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusterioslave run --log-level=warn
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusteriohost run --log-level=warn
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -324,16 +445,16 @@ async function inquirerMissingArgs(args) {
|
||||
message: "Operating mode to install",
|
||||
default: "standalone",
|
||||
choices: [
|
||||
{ name: "Standalone (install both master and slave on this computer)", value: "standalone" },
|
||||
{ name: "Master only", value: "master" },
|
||||
{ name: "Slave only", value: "slave" },
|
||||
{ name: "Standalone (install both controller and host on this computer)", value: "standalone" },
|
||||
{ name: "Controller only", value: "controller" },
|
||||
{ name: "Host only", value: "host" },
|
||||
{ name: "Ctl only", value: "ctl" },
|
||||
{ name: "Plugins only", value: "plugins" },
|
||||
],
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (["standalone", "master"].includes(answers.mode)) {
|
||||
if (["standalone", "controller"].includes(answers.mode)) {
|
||||
if (args.admin) { answers.admin = args.admin; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
@@ -350,42 +471,42 @@ async function inquirerMissingArgs(args) {
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (answers.mode === "slave") {
|
||||
if (args.slaveName) { answers.slaveName = args.slaveName; }
|
||||
if (answers.mode === "host") {
|
||||
if (args.hostName) { answers.hostName = args.hostName; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "slaveName",
|
||||
message: "Name of slave",
|
||||
name: "hostName",
|
||||
message: "Name of host",
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (["slave", "ctl"].includes(answers.mode)) {
|
||||
if (args.masterUrl) { answers.masterUrl = args.masterUrl; }
|
||||
if (["host", "ctl"].includes(answers.mode)) {
|
||||
if (args.controllerUrl) { answers.controllerUrl = args.controllerUrl; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "masterUrl",
|
||||
message: "Master server URL",
|
||||
name: "controllerUrl",
|
||||
message: "Controller URL",
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (args.masterToken) { answers.masterToken = args.masterToken; }
|
||||
if (args.controllerToken) { answers.controllerToken = args.controllerToken; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "masterToken",
|
||||
message: "Master authentication Token",
|
||||
name: "controllerToken",
|
||||
message: "Controller authentication Token",
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (answers.mode === "slave") {
|
||||
validateSlaveToken(answers.masterToken);
|
||||
if (answers.mode === "host") {
|
||||
validateHostToken(answers.controllerToken);
|
||||
}
|
||||
|
||||
if (["standalone", "slave"].includes(answers.mode)) {
|
||||
if (["standalone", "host"].includes(answers.mode)) {
|
||||
let myIp = "localhost";
|
||||
if (args.publicAddress) {
|
||||
answers.publicAddress = args.publicAddress;
|
||||
@@ -532,25 +653,28 @@ async function main() {
|
||||
})
|
||||
.option("mode", {
|
||||
nargs: 1, describe: "Operating mode to install",
|
||||
choices: ["standalone", "master", "slave", "ctl", "plugins"],
|
||||
choices: ["standalone", "controller", "host", "ctl", "plugins"],
|
||||
})
|
||||
.option("migrate-rename", {
|
||||
nargs: 0, describe: "Migrate from before slave/master rename", default: false, type: "boolean",
|
||||
})
|
||||
.option("admin", {
|
||||
nargs: 1, describe: "Admin account name [standalone/master]", type: "string",
|
||||
nargs: 1, describe: "Admin account name [standalone/controller]", type: "string",
|
||||
})
|
||||
.option("slave-name", {
|
||||
nargs: 1, describe: "Slave name [slave]", type: "string",
|
||||
.option("host-name", {
|
||||
nargs: 1, describe: "Host name [host]", type: "string",
|
||||
})
|
||||
.option("master-url", {
|
||||
nargs: 1, describe: "Master URL [slave/ctl]", type: "string",
|
||||
.option("controller-url", {
|
||||
nargs: 1, describe: "Controller URL [host/ctl]", type: "string",
|
||||
})
|
||||
.option("master-token", {
|
||||
nargs: 1, describe: "Master authentication token [slave/ctl]", type: "string",
|
||||
.option("controller-token", {
|
||||
nargs: 1, describe: "Controller authentication token [host/ctl]", type: "string",
|
||||
})
|
||||
.option("public-address", {
|
||||
nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/slave]", type: "string",
|
||||
nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/host]", type: "string",
|
||||
})
|
||||
.option("factorio-dir", {
|
||||
nargs: 1, describe: "Path to Factorio installation [standalone/slave]", type: "string",
|
||||
nargs: 1, describe: "Path to Factorio installation [standalone/host]", type: "string",
|
||||
})
|
||||
.option("plugins", {
|
||||
array: true, describe: "Plugins to install", type: "string",
|
||||
@@ -582,6 +706,11 @@ async function main() {
|
||||
await validateNotRoot(args);
|
||||
}
|
||||
|
||||
if (args.migrateRename) {
|
||||
await migrateRename(args);
|
||||
return;
|
||||
}
|
||||
|
||||
let answers = await inquirerMissingArgs(args);
|
||||
if (answers.downloadHeadless) {
|
||||
await downloadLinuxServer();
|
||||
@@ -594,47 +723,47 @@ async function main() {
|
||||
}
|
||||
|
||||
let adminToken = null;
|
||||
if (["standalone", "master"].includes(answers.mode)) {
|
||||
logger.info("Setting up master");
|
||||
await execMaster(["bootstrap", "create-admin", answers.admin]);
|
||||
let result = await execMaster(["bootstrap", "generate-user-token", answers.admin]);
|
||||
if (["standalone", "controller"].includes(answers.mode)) {
|
||||
logger.info("Setting up controller");
|
||||
await execController(["bootstrap", "create-admin", answers.admin]);
|
||||
let result = await execController(["bootstrap", "generate-user-token", answers.admin]);
|
||||
adminToken = result.stdout.split("\n").slice(-2)[0];
|
||||
}
|
||||
|
||||
if (answers.mode === "standalone") {
|
||||
logger.info("Setting up slave");
|
||||
await execSlave(["config", "set", "slave.name", "local"]);
|
||||
logger.info("Setting up host");
|
||||
await execHost(["config", "set", "host.name", "local"]);
|
||||
|
||||
let result = await execSlave(["config", "show", "slave.id"]);
|
||||
let slaveId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10);
|
||||
let result = await execHost(["config", "show", "host.id"]);
|
||||
let hostId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10);
|
||||
|
||||
result = await execMaster(["bootstrap", "generate-slave-token", slaveId]);
|
||||
let slaveToken = result.stdout.split("\n").slice(-2)[0];
|
||||
result = await execController(["bootstrap", "generate-host-token", hostId]);
|
||||
let hostToken = result.stdout.split("\n").slice(-2)[0];
|
||||
|
||||
await execSlave(["config", "set", "slave.master_token", slaveToken]);
|
||||
await execSlave(["config", "set", "slave.public_address", answers.publicAddress]);
|
||||
await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]);
|
||||
await execHost(["config", "set", "host.controller_token", hostToken]);
|
||||
await execHost(["config", "set", "host.public_address", answers.publicAddress]);
|
||||
await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]);
|
||||
}
|
||||
|
||||
if (answers.mode === "slave") {
|
||||
logger.info("Setting up slave");
|
||||
let slaveId = JSON.parse(Buffer.from(answers.masterToken.split(".")[1], "base64")).slave;
|
||||
await execSlave(["config", "set", "slave.id", slaveId]);
|
||||
await execSlave(["config", "set", "slave.name", answers.slaveName]);
|
||||
await execSlave(["config", "set", "slave.master_url", answers.masterUrl]);
|
||||
await execSlave(["config", "set", "slave.master_token", answers.masterToken]);
|
||||
await execSlave(["config", "set", "slave.public_address", answers.publicAddress]);
|
||||
await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]);
|
||||
if (answers.mode === "host") {
|
||||
logger.info("Setting up host");
|
||||
let hostId = JSON.parse(Buffer.from(answers.controllerToken.split(".")[1], "base64")).host;
|
||||
await execHost(["config", "set", "host.id", hostId]);
|
||||
await execHost(["config", "set", "host.name", answers.hostName]);
|
||||
await execHost(["config", "set", "host.controller_url", answers.controllerUrl]);
|
||||
await execHost(["config", "set", "host.controller_token", answers.controllerToken]);
|
||||
await execHost(["config", "set", "host.public_address", answers.publicAddress]);
|
||||
await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]);
|
||||
}
|
||||
|
||||
if (!dev && ["standalone", "master", "slave"].includes(answers.mode)) {
|
||||
if (!dev && ["standalone", "controller", "host"].includes(answers.mode)) {
|
||||
logger.info("Writing run scripts");
|
||||
await writeScripts(answers.mode);
|
||||
}
|
||||
|
||||
if (answers.mode === "ctl") {
|
||||
await execCtl(["control-config", "set", "control.master_url", answers.masterUrl]);
|
||||
await execCtl(["control-config", "set", "control.master_token", answers.masterToken]);
|
||||
await execCtl(["control-config", "set", "control.controller_url", answers.controllerUrl]);
|
||||
await execCtl(["control-config", "set", "control.controller_token", answers.controllerToken]);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
@@ -37,7 +37,7 @@ For example, installing the Subspace Storage plugin:
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
npx clusterioctl plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin remove <name>`
|
||||
@@ -51,7 +51,7 @@ For example, uninstalling the Subspace Storage plugin:
|
||||
npx clusterioctl plugin remove subspace_storage
|
||||
npm uninstall @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin list`
|
||||
@@ -62,7 +62,7 @@ Lists the plugins set up to be available by name followed by path.
|
||||
### `config-control`
|
||||
|
||||
Manage the ctl configuration.
|
||||
This allows setting the url and token to the master server to manage.
|
||||
This allows setting the url and token to the controller to manage.
|
||||
|
||||
|
||||
#### `config-control set <config-entry> [value]`
|
||||
|
||||
+13
-13
@@ -24,7 +24,7 @@ const commands = require("./src/commands");
|
||||
|
||||
|
||||
/**
|
||||
* Connector for control connection to master server
|
||||
* Connector for control connection to controller
|
||||
* @private
|
||||
*/
|
||||
class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
@@ -46,22 +46,22 @@ class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
/**
|
||||
* Handles running the control
|
||||
*
|
||||
* Connects to the master server over WebSocket and sends commands to it.
|
||||
* Connects to the controller over WebSocket and sends commands to it.
|
||||
* @static
|
||||
*/
|
||||
class Control extends libLink.Link {
|
||||
|
||||
constructor(connector, controlConfig, tlsCa, controlPlugins) {
|
||||
super("control", "master", connector);
|
||||
super("control", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
/**
|
||||
* Control config used for connecting to the master.
|
||||
* Control config used for connecting to the controller.
|
||||
* @type {module:lib/config.ControlConfig}
|
||||
*/
|
||||
this.config = controlConfig;
|
||||
/**
|
||||
* Certificate authority used to validate TLS connections to the master.
|
||||
* Certificate authority used to validate TLS connections to the controller.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.tlsCa = tlsCa;
|
||||
@@ -83,7 +83,7 @@ class Control extends libLink.Link {
|
||||
|
||||
async accountUpdateEventHandler() { }
|
||||
|
||||
async slaveUpdateEventHandler() { }
|
||||
async hostUpdateEventHandler() { }
|
||||
|
||||
async instanceUpdateEventHandler() { }
|
||||
|
||||
@@ -97,13 +97,13 @@ class Control extends libLink.Link {
|
||||
|
||||
async setLogSubscriptions({
|
||||
all = false,
|
||||
master = false,
|
||||
slave_ids = [],
|
||||
controller = false,
|
||||
host_ids = [],
|
||||
instance_ids = [],
|
||||
max_level = null,
|
||||
}) {
|
||||
await libLink.messages.setLogSubscriptions.send(this, {
|
||||
all, master, slave_ids, instance_ids, max_level,
|
||||
all, controller, host_ids, instance_ids, max_level,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,8 +251,8 @@ async function startControl() {
|
||||
targetCommand = targetCommand.get(commandPath.shift());
|
||||
}
|
||||
|
||||
// The remaining commands require connecting to the master server.
|
||||
if (!controlConfig.get("control.master_url") || !controlConfig.get("control.master_token")) {
|
||||
// The remaining commands require connecting to the controller.
|
||||
if (!controlConfig.get("control.controller_url") || !controlConfig.get("control.controller_token")) {
|
||||
logger.error("Missing URL and/or token to connect with. See README.md for setting up access.");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -265,10 +265,10 @@ async function startControl() {
|
||||
}
|
||||
|
||||
let controlConnector = new ControlConnector(
|
||||
controlConfig.get("control.master_url"),
|
||||
controlConfig.get("control.controller_url"),
|
||||
controlConfig.get("control.max_reconnect_delay"),
|
||||
tlsCa,
|
||||
controlConfig.get("control.master_token")
|
||||
controlConfig.get("control.controller_token")
|
||||
);
|
||||
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
|
||||
try {
|
||||
|
||||
@@ -84,14 +84,14 @@ async function serializedConfigToString(serializedConfig, configGroup, disallowe
|
||||
return allConfigElements;
|
||||
}
|
||||
|
||||
const masterCommands = new libCommand.CommandTree({ name: "master", description: "Master management" });
|
||||
const masterConfigCommands = new libCommand.CommandTree({
|
||||
name: "config", alias: ["c"], description: "master config management",
|
||||
const controllerCommands = new libCommand.CommandTree({ name: "controller", description: "Controller management" });
|
||||
const controllerConfigCommands = new libCommand.CommandTree({
|
||||
name: "config", alias: ["c"], description: "controller config management",
|
||||
});
|
||||
masterConfigCommands.add(new libCommand.Command({
|
||||
definition: ["list", "List master configuration"],
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
definition: ["list", "List controller configuration"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getMasterConfig.send(control);
|
||||
let response = await libLink.messages.getControllerConfig.send(control);
|
||||
|
||||
for (let group of response.serialized_config.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
@@ -101,8 +101,8 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
masterConfigCommands.add(new libCommand.Command({
|
||||
definition: ["set <field> [value]", "Set field in master config", (yargs) => {
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
definition: ["set <field> [value]", "Set field in controller config", (yargs) => {
|
||||
yargs.positional("field", { describe: "Field to set", type: "string" });
|
||||
yargs.positional("value", { describe: "Value to set", type: "string" });
|
||||
yargs.options({
|
||||
@@ -115,15 +115,15 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
} else if (args.value === undefined) {
|
||||
args.value = "";
|
||||
}
|
||||
await libLink.messages.setMasterConfigField.send(control, {
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
field: args.field,
|
||||
value: args.value,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
masterConfigCommands.add(new libCommand.Command({
|
||||
definition: ["set-prop <field> <prop> [value]", "Set property of field in master config", (yargs) => {
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
definition: ["set-prop <field> <prop> [value]", "Set property of field in controller config", (yargs) => {
|
||||
yargs.positional("field", { describe: "Field to set", type: "string" });
|
||||
yargs.positional("prop", { describe: "Property to set", type: "string" });
|
||||
yargs.positional("value", { describe: "JSON parsed value to set", type: "string" });
|
||||
@@ -150,11 +150,11 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
}
|
||||
request.value = args.value;
|
||||
}
|
||||
await libLink.messages.setMasterConfigProp.send(control, request);
|
||||
await libLink.messages.setControllerConfigProp.send(control, request);
|
||||
},
|
||||
}));
|
||||
masterConfigCommands.add(new libCommand.Command({
|
||||
definition: ["edit [editor]", "Edit master configuration", (yargs) => {
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
definition: ["edit [editor]", "Edit controller configuration", (yargs) => {
|
||||
yargs.positional("editor", {
|
||||
describe: "Editor to use",
|
||||
type: "string",
|
||||
@@ -162,14 +162,16 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getMasterConfig.send(control);
|
||||
let response = await libLink.messages.getControllerConfig.send(control);
|
||||
let tmpFile = await libFileOps.getTempFile("ctl-", "-tmp", os.tmpdir());
|
||||
let editor = await getEditor(args.editor);
|
||||
if (editor === -1) {
|
||||
throw new libErrors.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars
|
||||
Try "ctl master config edit <editor of choice>"`);
|
||||
Try "ctl controller config edit <editor of choice>"`);
|
||||
}
|
||||
let allConfigElements = await serializedConfigToString(response.serialized_config, libConfig.MasterConfig, {});
|
||||
let allConfigElements = await serializedConfigToString(
|
||||
response.serialized_config, libConfig.ControllerConfig, {}
|
||||
);
|
||||
await fs.writeFile(tmpFile, allConfigElements, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
@@ -189,7 +191,7 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
for (let index in final) {
|
||||
if (index in final) {
|
||||
try {
|
||||
await libLink.messages.setMasterConfigField.send(control, {
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
field: index,
|
||||
value: final[index],
|
||||
});
|
||||
@@ -213,16 +215,16 @@ masterConfigCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
masterCommands.add(masterConfigCommands);
|
||||
controllerCommands.add(controllerConfigCommands);
|
||||
|
||||
|
||||
const masterPluginCommands = new libCommand.CommandTree({
|
||||
name: "plugin", alias: ["p"], description: "master plugin inspection",
|
||||
const controllerPluginCommands = new libCommand.CommandTree({
|
||||
name: "plugin", alias: ["p"], description: "controller plugin inspection",
|
||||
});
|
||||
masterPluginCommands.add(new libCommand.Command({
|
||||
definition: ["list", "List plugins on master"],
|
||||
controllerPluginCommands.add(new libCommand.Command({
|
||||
definition: ["list", "List plugins on controller"],
|
||||
handler: async function(args, control) {
|
||||
let url = new URL(control.config.get("control.master_url"));
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += "api/plugins";
|
||||
let response = await phin({
|
||||
url,
|
||||
@@ -232,42 +234,42 @@ masterPluginCommands.add(new libCommand.Command({
|
||||
print(asTable(response.body));
|
||||
},
|
||||
}));
|
||||
masterCommands.add(masterPluginCommands);
|
||||
controllerCommands.add(controllerPluginCommands);
|
||||
|
||||
|
||||
const slaveCommands = new libCommand.CommandTree({ name: "slave", description: "Slave management" });
|
||||
slaveCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List slaves connected to the master"],
|
||||
const hostCommands = new libCommand.CommandTree({ name: "host", description: "Host management" });
|
||||
hostCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List hosts connected to the controller"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listSlaves.send(control);
|
||||
let response = await libLink.messages.listHosts.send(control);
|
||||
print(asTable(response.list));
|
||||
},
|
||||
}));
|
||||
|
||||
slaveCommands.add(new libCommand.Command({
|
||||
definition: ["generate-token", "Generate token for a slave", (yargs) => {
|
||||
yargs.option("id", { type: "number", nargs: 1, describe: "Slave id" });
|
||||
hostCommands.add(new libCommand.Command({
|
||||
definition: ["generate-token", "Generate token for a host", (yargs) => {
|
||||
yargs.option("id", { type: "number", nargs: 1, describe: "Host id" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let slaveId = typeof args.id === "number" ? args.id : null;
|
||||
let response = await libLink.messages.generateSlaveToken.send(control, { slave_id: slaveId });
|
||||
let hostId = typeof args.id === "number" ? args.id : null;
|
||||
let response = await libLink.messages.generateHostToken.send(control, { host_id: hostId });
|
||||
print(response.token);
|
||||
},
|
||||
}));
|
||||
|
||||
slaveCommands.add(new libCommand.Command({
|
||||
definition: ["create-config", "Create slave config", (yargs) => {
|
||||
yargs.option("id", { type: "number", nargs: 1, describe: "Slave id", default: null });
|
||||
yargs.option("name", { type: "string", nargs: 1, describe: "Slave name", default: null });
|
||||
hostCommands.add(new libCommand.Command({
|
||||
definition: ["create-config", "Create host config", (yargs) => {
|
||||
yargs.option("id", { type: "number", nargs: 1, describe: "Host id", default: null });
|
||||
yargs.option("name", { type: "string", nargs: 1, describe: "Host name", default: null });
|
||||
yargs.option("generate-token", {
|
||||
type: "boolean", nargs: 0, describe: "Generate authentication token", default: false,
|
||||
});
|
||||
yargs.option("output", {
|
||||
type: "string", nargs: 1, describe: "Path to output config (- for stdout)", default: "config-slave.json",
|
||||
type: "string", nargs: 1, describe: "Path to output config (- for stdout)", default: "config-host.json",
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.createSlaveConfig.send(control, {
|
||||
let response = await libLink.messages.createHostConfig.send(control, {
|
||||
id: args.id, name: args.name, generate_token: args.generateToken,
|
||||
});
|
||||
|
||||
@@ -293,7 +295,7 @@ const instanceCommands = new libCommand.CommandTree({
|
||||
name: "instance", alias: ["i"], description: "Instance management",
|
||||
});
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List instances known to the master"],
|
||||
definition: [["list", "l"], "List instances known to the controller"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listInstances.send(control);
|
||||
print(asTable(response.list));
|
||||
@@ -315,7 +317,7 @@ instanceCommands.add(new libCommand.Command({
|
||||
instanceConfig.set("instance.id", args.id);
|
||||
}
|
||||
instanceConfig.set("instance.name", args.name);
|
||||
let serialized_config = instanceConfig.serialize("master");
|
||||
let serialized_config = instanceConfig.serialize("controller");
|
||||
await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
},
|
||||
}));
|
||||
@@ -426,9 +428,9 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
let editor = await getEditor(args.editor);
|
||||
if (editor === -1) {
|
||||
throw new libErrors.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars
|
||||
Try "ctl master config edit <editor of choice>"`);
|
||||
Try "ctl controller config edit <editor of choice>"`);
|
||||
}
|
||||
let disallowedList = {"instance.id": 0, "instance.assigned_slave": 0, "factorio.settings": 0};
|
||||
let disallowedList = {"instance.id": 0, "instance.assigned_host": 0, "factorio.settings": 0};
|
||||
let allConfigElements = await serializedConfigToString(
|
||||
response.serialized_config,
|
||||
libConfig.InstanceConfig,
|
||||
@@ -485,16 +487,16 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
instanceCommands.add(instanceConfigCommands);
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["assign <instance> [slave]", "Assign instance to a slave", (yargs) => {
|
||||
definition: ["assign <instance> [host]", "Assign instance to a host", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to assign", type: "string" });
|
||||
yargs.positional("slave", { describe: "Slave to assign to or unassign if none", type: "string" });
|
||||
yargs.positional("host", { describe: "Host to assign to or unassign if none", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let slaveId = args.slave ? await libCommand.resolveSlave(control, args.slave) : null;
|
||||
let hostId = args.host ? await libCommand.resolveHost(control, args.host) : null;
|
||||
await libLink.messages.assignInstanceCommand.send(control, {
|
||||
instance_id: instanceId,
|
||||
slave_id: slaveId,
|
||||
host_id: hostId,
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -614,7 +616,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
let content = await fs.readFile(args.filepath);
|
||||
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let url = new URL(control.config.get("control.master_url"));
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += "api/upload-save";
|
||||
url.searchParams.append("instance_id", instanceId);
|
||||
url.searchParams.append("filename", filename);
|
||||
@@ -622,7 +624,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
let result = await phin({
|
||||
url, method: "POST",
|
||||
headers: {
|
||||
"X-Access-Token": control.config.get("control.master_token"),
|
||||
"X-Access-Token": control.config.get("control.controller_token"),
|
||||
"Content-Type": "application/zip",
|
||||
},
|
||||
core: { ca: control.tlsCa },
|
||||
@@ -688,7 +690,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
save: args.save,
|
||||
});
|
||||
|
||||
let url = new URL(control.config.get("control.master_url"));
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += `api/stream/${result.stream_id}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
@@ -1233,14 +1235,14 @@ modCommands.add(new libCommand.Command({
|
||||
// phin doesn't support streaming requests :(
|
||||
let content = await fs.readFile(args.file);
|
||||
|
||||
let url = new URL(control.config.get("control.master_url"));
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += "api/upload-mod";
|
||||
url.searchParams.append("filename", filename);
|
||||
|
||||
let result = await phin({
|
||||
url, method: "POST",
|
||||
headers: {
|
||||
"X-Access-Token": control.config.get("control.master_token"),
|
||||
"X-Access-Token": control.config.get("control.controller_token"),
|
||||
"Content-Type": "application/zip",
|
||||
},
|
||||
core: { ca: control.tlsCa },
|
||||
@@ -1278,7 +1280,7 @@ modCommands.add(new libCommand.Command({
|
||||
version: args.modVersion,
|
||||
});
|
||||
|
||||
let url = new URL(control.config.get("control.master_url"));
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += `api/stream/${result.stream_id}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
@@ -1583,20 +1585,20 @@ logCommands.add(new libCommand.Command({
|
||||
definition: ["follow", "follow cluster log", (yargs) => {
|
||||
yargs.options({
|
||||
"all": { describe: "Follow the whole cluster log", nargs: 0, type: "boolean", default: false },
|
||||
"master": { describe: "Follow log of the master server", nargs: 0, type: "boolean", default: false },
|
||||
"slave": { describe: "Follow log of given slave", nargs: 1, type: "string", default: null },
|
||||
"controller": { describe: "Follow log of the controller", nargs: 0, type: "boolean", default: false },
|
||||
"host": { describe: "Follow log of given host", nargs: 1, type: "string", default: null },
|
||||
"instance": { describe: "Follow log of given instance", nargs: 1, type: "string", default: null },
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
if (!args.all && !args.master && !args.slave && !args.instance) {
|
||||
logger.error("At least one of --all, --master, --slave and --instance must be passed");
|
||||
if (!args.all && !args.controller && !args.host && !args.instance) {
|
||||
logger.error("At least one of --all, --controller, --host and --instance must be passed");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let instance_ids = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let slave_ids = args.slave ? [await libCommand.resolveSlave(control, args.slave)] : [];
|
||||
await control.setLogSubscriptions({ all: args.all, master: args.master, slave_ids, instance_ids });
|
||||
let host_ids = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
await control.setLogSubscriptions({ all: args.all, controller: args.controller, host_ids, instance_ids });
|
||||
control.keepOpen = true;
|
||||
},
|
||||
}));
|
||||
@@ -1605,8 +1607,8 @@ logCommands.add(new libCommand.Command({
|
||||
definition: ["query", "Query cluster log", (yargs) => {
|
||||
yargs.options({
|
||||
"all": { describe: "Query the whole cluster log", nargs: 0, type: "boolean", default: false },
|
||||
"master": { describe: "Query log of the master server", nargs: 0, type: "boolean", default: false },
|
||||
"slave": { describe: "Query log of given slave", nargs: 1, type: "string", default: null },
|
||||
"controller": { describe: "Query log of the controller", nargs: 0, type: "boolean", default: false },
|
||||
"host": { describe: "Query log of given host", nargs: 1, type: "string", default: null },
|
||||
"instance": { describe: "Query log of given instance", nargs: 1, type: "string", default: null },
|
||||
"max-level": { describe: "Maximum log level to return", nargs: 1, type: "string", default: null },
|
||||
"limit": { describe: "Max number of entries to return", nargs: 1, type: "number", default: 1000 },
|
||||
@@ -1614,17 +1616,17 @@ logCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
if (!args.all && !args.master && !args.slave && !args.instance) {
|
||||
logger.error("At least one of --all, --master, --slave and --instance must be passed");
|
||||
if (!args.all && !args.controller && !args.host && !args.instance) {
|
||||
logger.error("At least one of --all, --controller, --host and --instance must be passed");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let instance_ids = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let slave_ids = args.slave ? [await libCommand.resolveSlave(control, args.slave)] : [];
|
||||
let host_ids = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
let result = await libLink.messages.queryLog.send(control, {
|
||||
all: args.all,
|
||||
master: args.master,
|
||||
slave_ids,
|
||||
controller: args.controller,
|
||||
host_ids,
|
||||
instance_ids,
|
||||
max_level: args.maxLevel,
|
||||
limit: args.limit,
|
||||
@@ -1651,7 +1653,7 @@ logCommands.add(new libCommand.Command({
|
||||
|
||||
const debugCommands = new libCommand.CommandTree({ name: "debug", description: "Debugging utilities" });
|
||||
debugCommands.add(new libCommand.Command({
|
||||
definition: ["dump-ws", "Dump WebSocket messages sent and received by master", (yargs) => { }],
|
||||
definition: ["dump-ws", "Dump WebSocket messages sent and received by controller", (yargs) => { }],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.debugDumpWs.send(control);
|
||||
control.keepOpen = true;
|
||||
@@ -1660,8 +1662,8 @@ debugCommands.add(new libCommand.Command({
|
||||
|
||||
async function registerCommands(controlPlugins, yargs) {
|
||||
const rootCommands = new libCommand.CommandTree({ name: "clusterioctl", description: "Manage cluster" });
|
||||
rootCommands.add(masterCommands);
|
||||
rootCommands.add(slaveCommands);
|
||||
rootCommands.add(controllerCommands);
|
||||
rootCommands.add(hostCommands);
|
||||
rootCommands.add(instanceCommands);
|
||||
rootCommands.add(modPackCommands);
|
||||
rootCommands.add(modCommands);
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# Clusterio Slave
|
||||
# Clusterio Host
|
||||
|
||||
Node hosting Factorio servers in a Clusterio cluster.
|
||||
Clusterio slaves connect to the master server and waits for commands from the master server to start up and stop instances.
|
||||
A cluster can have any number of slaves in it located on different computers, and each slave can host any number of instances each of which is a Factorio server that talks with the rest of the cluster.
|
||||
Clusterio hosts connect to the controller and waits for commands from the controller to start up and stop instances.
|
||||
A cluster can have any number of hosts in it located on different computers, and each host can host any number of instances each of which is a Factorio server that talks with the rest of the cluster.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
npx clusterioslave <command>
|
||||
npx clusteriohost <command>
|
||||
|
||||
Common options:
|
||||
|
||||
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the slave.
|
||||
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the host.
|
||||
Defaults to `plugin-list.json` and will be created if it does not exist.
|
||||
See the `plugin` command for managing this list.
|
||||
|
||||
* `--config <file>` JSON file to use for storing configuration for the slave.
|
||||
Defaults to `config-slave.json` and will be created if it does not exist.
|
||||
* `--config <file>` JSON file to use for storing configuration for the host.
|
||||
Defaults to `config-host.json` and will be created if it does not exist.
|
||||
See the `config` command for inspecting and modifying the configuration.
|
||||
|
||||
|
||||
### `plugin <command>`
|
||||
|
||||
Configure plugins available to be loaded by the slave.
|
||||
Configure plugins available to be loaded by the host.
|
||||
The available plugins will be loaded unless they have been disabled in the configuration, see the config command for disabling plugins.
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ A relative path must start with ./ or ../ (or .\ and ..\ on Windows) otherwise i
|
||||
For example, installing the Subspace Storage plugin:
|
||||
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
npx clusterioslave plugin add @clusterio/plugin-subspace_storage
|
||||
npx clusteriohost plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin remove <name>`
|
||||
@@ -47,10 +47,10 @@ Removing and unistalling a plugin is usually not neccessary as the functions pro
|
||||
|
||||
For example, uninstalling the Subspace Storage plugin:
|
||||
|
||||
npx clusterioslave plugin remove subspace_storage
|
||||
npx clusteriohost plugin remove subspace_storage
|
||||
npm uninstall @clusterio/plugin-subspace_storage
|
||||
|
||||
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
|
||||
Since the `plugin-list.json` is shared between controller, host and ctl you usually only need to do this once per machine.
|
||||
|
||||
|
||||
#### `plugin list`
|
||||
@@ -60,8 +60,8 @@ Lists the plugins set up to be available by name followed by path.
|
||||
|
||||
### `config`
|
||||
|
||||
Manage the slave configuration offline.
|
||||
This should only be used when the slave is stopped, otherwise the config read might be out of date and config changes will be overwritten when the slave shuts down.
|
||||
Manage the host configuration offline.
|
||||
This should only be used when the host is stopped, otherwise the config read might be out of date and config changes will be overwritten when the host shuts down.
|
||||
|
||||
|
||||
#### `config set <config-entry> [value]`
|
||||
@@ -85,7 +85,7 @@ Lists up all configuration entries with their currently configured values.
|
||||
|
||||
### `run`
|
||||
|
||||
Runs the slave.
|
||||
Runs the host.
|
||||
|
||||
|
||||
## See Also
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Clusterio slave
|
||||
* Clusterio host
|
||||
*
|
||||
* Connects to the master server and hosts Factorio servers that can
|
||||
* Connects to the controller and hosts Factorio servers that can
|
||||
* communicate with the cluster. It is remotely controlled by {@link
|
||||
* module:master/master}.
|
||||
* module:controller/controller}.
|
||||
*
|
||||
* @module slave/slave
|
||||
* @module host/host
|
||||
* @author Danielv123, Hornwitser
|
||||
* @example
|
||||
* npx clusterioslave run
|
||||
* npx clusteriohost run
|
||||
*/
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
@@ -30,44 +30,44 @@ const libSharedCommands = require("@clusterio/lib/shared_commands");
|
||||
const { ConsoleTransport, FilteredTransport, levels, logger } = require("@clusterio/lib/logging");
|
||||
const libLoggingUtils = require("@clusterio/lib/logging_utils");
|
||||
|
||||
const Slave = require("./src/Slave");
|
||||
const Host = require("./src/Host");
|
||||
|
||||
|
||||
class SlaveConnector extends libLink.WebSocketClientConnector {
|
||||
constructor(slaveConfig, tlsCa, pluginInfos) {
|
||||
class HostConnector extends libLink.WebSocketClientConnector {
|
||||
constructor(hostConfig, tlsCa, pluginInfos) {
|
||||
super(
|
||||
slaveConfig.get("slave.master_url"),
|
||||
slaveConfig.get("slave.max_reconnect_delay"),
|
||||
hostConfig.get("host.controller_url"),
|
||||
hostConfig.get("host.max_reconnect_delay"),
|
||||
tlsCa
|
||||
);
|
||||
this.slaveConfig = slaveConfig;
|
||||
this.hostConfig = hostConfig;
|
||||
this.pluginInfos = pluginInfos;
|
||||
}
|
||||
|
||||
register() {
|
||||
logger.info("Connector | registering slave");
|
||||
logger.info("Connector | registering host");
|
||||
let plugins = {};
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
plugins[pluginInfo.name] = pluginInfo.version;
|
||||
}
|
||||
|
||||
this.sendHandshake("register_slave", {
|
||||
token: this.slaveConfig.get("slave.master_token"),
|
||||
agent: "Clusterio Slave",
|
||||
this.sendHandshake("register_host", {
|
||||
token: this.hostConfig.get("host.controller_token"),
|
||||
agent: "Clusterio Host",
|
||||
version,
|
||||
id: this.slaveConfig.get("slave.id"),
|
||||
name: this.slaveConfig.get("slave.name"),
|
||||
public_address: this.slaveConfig.get("slave.public_address"),
|
||||
id: this.hostConfig.get("host.id"),
|
||||
name: this.hostConfig.get("host.name"),
|
||||
public_address: this.hostConfig.get("host.public_address"),
|
||||
plugins,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function startSlave() {
|
||||
async function startHost() {
|
||||
// argument parsing
|
||||
const args = yargs
|
||||
.scriptName("slave")
|
||||
.scriptName("host")
|
||||
.usage("$0 <command> [options]")
|
||||
.option("log-level", {
|
||||
nargs: 1,
|
||||
@@ -84,8 +84,8 @@ async function startSlave() {
|
||||
})
|
||||
.option("config", {
|
||||
nargs: 1,
|
||||
describe: "slave config file to use",
|
||||
default: "config-slave.json",
|
||||
describe: "host config file to use",
|
||||
default: "config-host.json",
|
||||
type: "string",
|
||||
})
|
||||
.option("plugin-list", {
|
||||
@@ -95,32 +95,18 @@ async function startSlave() {
|
||||
type: "string",
|
||||
})
|
||||
.command("plugin", "Manage available plugins", libSharedCommands.pluginCommand)
|
||||
.command("config", "Manage Slave config", libSharedCommands.configCommand)
|
||||
.command("run", "Run slave")
|
||||
.command("config", "Manage Host config", libSharedCommands.configCommand)
|
||||
.command("run", "Run host")
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
.argv
|
||||
;
|
||||
|
||||
{
|
||||
// Migration from alpha-10 single file logs, note that we can't use
|
||||
// the logger here as it's not initalized yet.
|
||||
/* eslint-disable no-console */
|
||||
let slaveLogDirectory = path.join(args.logDirectory, "slave");
|
||||
if (!await fs.pathExists(slaveLogDirectory) && await fs.pathExists("slave.log")) {
|
||||
console.log("Migrating slave log...");
|
||||
await fs.ensureDir(slaveLogDirectory);
|
||||
await libLoggingUtils.migrateLogs("slave.log", slaveLogDirectory, "slave-%DATE%.log");
|
||||
console.log("Migration complete, you should delete slave.log now");
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
logger.add(new winston.transports.DailyRotateFile({
|
||||
format: winston.format.json(),
|
||||
filename: "slave-%DATE%.log",
|
||||
filename: "host-%DATE%.log",
|
||||
utc: true,
|
||||
dirname: path.join(args.logDirectory, "slave"),
|
||||
dirname: path.join(args.logDirectory, "host"),
|
||||
}));
|
||||
if (args.logLevel !== "none") {
|
||||
logger.add(new ConsoleTransport({
|
||||
@@ -133,7 +119,7 @@ async function startSlave() {
|
||||
|
||||
let command = args._[0];
|
||||
if (command === "run") {
|
||||
logger.info(`Starting Clusterio slave ${version}`);
|
||||
logger.info(`Starting Clusterio host ${version}`);
|
||||
}
|
||||
|
||||
logger.info(`Loading available plugins from ${args.pluginList}`);
|
||||
@@ -158,14 +144,14 @@ async function startSlave() {
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
logger.info(`Loading config from ${args.config}`);
|
||||
let slaveConfig = new libConfig.SlaveConfig("slave");
|
||||
let hostConfig = new libConfig.HostConfig("host");
|
||||
try {
|
||||
await slaveConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
await hostConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.info("Config not found, initializing new config");
|
||||
await slaveConfig.init();
|
||||
await hostConfig.init();
|
||||
|
||||
} else {
|
||||
throw new libErrors.StartupError(`Failed to load ${args.config}: ${err.message}`);
|
||||
@@ -173,49 +159,49 @@ async function startSlave() {
|
||||
}
|
||||
|
||||
if (command === "config") {
|
||||
await libSharedCommands.handleConfigCommand(args, slaveConfig, args.config);
|
||||
await libSharedCommands.handleConfigCommand(args, hostConfig, args.config);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here the command was run
|
||||
|
||||
await fs.ensureDir(slaveConfig.get("slave.instances_directory"));
|
||||
await fs.ensureDir(slaveConfig.get("slave.mods_directory"));
|
||||
await fs.ensureDir(hostConfig.get("host.instances_directory"));
|
||||
await fs.ensureDir(hostConfig.get("host.mods_directory"));
|
||||
await fs.ensureDir("modules");
|
||||
|
||||
// Set the process title, shows up as the title of the CMD window on windows
|
||||
// and as the process name in ps/top on linux.
|
||||
process.title = "clusterioSlave";
|
||||
process.title = "clusteriohost";
|
||||
|
||||
// make sure we have the master access token
|
||||
if (slaveConfig.get("slave.master_token") === "enter token here") {
|
||||
// make sure we have the controller access token
|
||||
if (hostConfig.get("host.controller_token") === "enter token here") {
|
||||
logger.fatal("ERROR invalid config!");
|
||||
logger.fatal(
|
||||
"Master server requires an access token for socket operations. As clusterio\n"+
|
||||
"slaves depends upon this, please set your token using the command npx\n"+
|
||||
"clusterioslave config set slave.master_token <token>. You can generate an\n"+
|
||||
"auth token using npx clusterioctl generate-slave-token."
|
||||
"Controller requires an access token for socket operations. As clusterio\n"+
|
||||
"hosts depends upon this, please set your token using the command npx\n"+
|
||||
"clusteriohost config set host.controller_token <token>. You can generate an\n"+
|
||||
"auth token using npx clusterioctl generate-host-token."
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure url ends with /
|
||||
if (!slaveConfig.get("slave.master_url").endsWith("/")) {
|
||||
if (!hostConfig.get("host.controller_url").endsWith("/")) {
|
||||
logger.fatal("ERROR invalid config!");
|
||||
logger.fatal("slave.master_url must end with '/'");
|
||||
logger.fatal("host.controller_url must end with '/'");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let tlsCa = null;
|
||||
let tlsCaPath = slaveConfig.get("slave.tls_ca");
|
||||
let tlsCaPath = hostConfig.get("host.tls_ca");
|
||||
if (tlsCaPath) {
|
||||
tlsCa = await fs.readFile(tlsCaPath);
|
||||
}
|
||||
|
||||
let slaveConnector = new SlaveConnector(slaveConfig, tlsCa, pluginInfos);
|
||||
let slave = new Slave(slaveConnector, slaveConfig, tlsCa, pluginInfos);
|
||||
let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos);
|
||||
let host = new Host(hostConnector, hostConfig, tlsCa, pluginInfos);
|
||||
|
||||
// Handle interrupts
|
||||
let secondSigint = false;
|
||||
@@ -229,7 +215,7 @@ async function startSlave() {
|
||||
|
||||
secondSigint = true;
|
||||
logger.info("Caught interrupt signal, shutting down");
|
||||
slave.shutdown();
|
||||
host.shutdown();
|
||||
});
|
||||
let secondSigterm = false;
|
||||
process.on("SIGTERM", () => {
|
||||
@@ -242,19 +228,19 @@ async function startSlave() {
|
||||
|
||||
secondSigterm = true;
|
||||
logger.info("Caught termination signal, shutting down");
|
||||
slave.shutdown();
|
||||
host.shutdown();
|
||||
});
|
||||
process.on("SIGHUP", () => {
|
||||
logger.info("Terminal closed, shutting down");
|
||||
slave.shutdown();
|
||||
host.shutdown();
|
||||
});
|
||||
|
||||
slaveConnector.once("connect", () => {
|
||||
logger.add(new libLoggingUtils.LinkTransport({ link: slave }));
|
||||
hostConnector.once("connect", () => {
|
||||
logger.add(new libLoggingUtils.LinkTransport({ link: host }));
|
||||
});
|
||||
|
||||
await slaveConnector.connect();
|
||||
logger.info("Started slave");
|
||||
await hostConnector.connect();
|
||||
logger.info("Started host");
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
@@ -266,15 +252,15 @@ I version of clusterio. Expect things to break. I
|
||||
+==========================================================+
|
||||
`
|
||||
);
|
||||
startSlave().catch(err => {
|
||||
startHost().catch(err => {
|
||||
if (err instanceof libErrors.AuthenticationFailed) {
|
||||
logger.fatal(err.message);
|
||||
|
||||
} else if (err instanceof libErrors.StartupError) {
|
||||
logger.fatal(`
|
||||
+-----------------------------------+
|
||||
| Unable to to start clusterioslave |
|
||||
+-----------------------------------+
|
||||
+----------------------------------+
|
||||
| Unable to to start clusteriohost |
|
||||
+----------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
|
||||
@@ -289,7 +275,7 @@ ${err.original.stack}`
|
||||
} else {
|
||||
logger.fatal(`
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while starting slave, please |
|
||||
| Unexpected error occured while starting host, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@clusterio/slave",
|
||||
"description": "Implementation of Clusterio slave server",
|
||||
"name": "@clusterio/host",
|
||||
"description": "Implementation of Clusterio host server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "slave.js",
|
||||
"main": "host.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusterioslave": "./slave.js"
|
||||
"clusteriohost": "./host.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -49,7 +49,7 @@ async function discoverInstances(instancesDir) {
|
||||
let instanceInfos = new Map();
|
||||
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
let instanceConfig = new libConfig.InstanceConfig("host");
|
||||
let configPath = path.join(instancesDir, entry.name, "instance.json");
|
||||
|
||||
try {
|
||||
@@ -81,16 +81,16 @@ async function discoverInstances(instancesDir) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles running the slave
|
||||
* Handles running the host
|
||||
*
|
||||
* Connects to the master server over the WebSocket and manages intsances.
|
||||
* @alias module:slave/src/Slave
|
||||
* Connects to the controller over the WebSocket and manages intsances.
|
||||
* @alias module:host/src/Host
|
||||
*/
|
||||
class Slave extends libLink.Link {
|
||||
class Host extends libLink.Link {
|
||||
// I don't like God classes, but the alternative of putting all this state
|
||||
// into global variables is not much better.
|
||||
constructor(connector, slaveConfig, tlsCa, pluginInfos) {
|
||||
super("slave", "master", connector);
|
||||
constructor(connector, hostConfig, tlsCa, pluginInfos) {
|
||||
super("host", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
this.pluginInfos = pluginInfos;
|
||||
@@ -98,10 +98,10 @@ class Slave extends libLink.Link {
|
||||
libPlugin.attachPluginMessages(this, { info: pluginInfo });
|
||||
}
|
||||
|
||||
this.config = slaveConfig;
|
||||
this.config = hostConfig;
|
||||
|
||||
/**
|
||||
* Certificate authority used to validate TLS connections to the master.
|
||||
* Certificate authority used to validate TLS connections to the controller.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.tlsCa = tlsCa;
|
||||
@@ -146,12 +146,12 @@ class Slave extends libLink.Link {
|
||||
if (this._disconnecting) {
|
||||
this._disconnecting = false;
|
||||
this.connector.connect().catch((err) => {
|
||||
logger.fatal(`Unexpected error reconnecting to master:\n${err.stack}`);
|
||||
logger.fatal(`Unexpected error reconnecting to controller:\n${err.stack}`);
|
||||
return this.shutdown();
|
||||
});
|
||||
|
||||
} else {
|
||||
logger.fatal("Master connection was unexpectedly closed");
|
||||
logger.fatal("Controller connection was unexpectedly closed");
|
||||
this.shutdown();
|
||||
}
|
||||
});
|
||||
@@ -159,7 +159,7 @@ class Slave extends libLink.Link {
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
libLink.messages.masterConnectionEvent.send(instanceConnection, { event });
|
||||
libLink.messages.controllerConnectionEvent.send(instanceConnection, { event });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -173,7 +173,7 @@ class Slave extends libLink.Link {
|
||||
throw new Error(`Instance folder was unepectedly invalid: name ${err.message}`);
|
||||
}
|
||||
|
||||
let instancesDir = this.config.get("slave.instances_directory");
|
||||
let instancesDir = this.config.get("host.instances_directory");
|
||||
for (let i = 0; i < 10; i++) { // Limit attempts in case this is somehow an infinite loop
|
||||
let candidateDir = path.join(instancesDir, await libFileOps.findUnusedName(instancesDir, name));
|
||||
try {
|
||||
@@ -306,22 +306,22 @@ class Slave extends libLink.Link {
|
||||
let { instance_id, serialized_config } = message.data;
|
||||
let instanceInfo = this.instanceInfos.get(instance_id);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true, "master");
|
||||
instanceInfo.config.update(serialized_config, true, "controller");
|
||||
logger.verbose(`Updated config for ${instanceInfo.path}`, this.instanceLogMeta(instance_id, instanceInfo));
|
||||
|
||||
} else {
|
||||
instanceInfo = this.discoveredInstanceInfos.get(instance_id);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true, "master");
|
||||
instanceInfo.config.update(serialized_config, true, "controller");
|
||||
|
||||
} else {
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
await instanceConfig.load(serialized_config, "master");
|
||||
let instanceConfig = new libConfig.InstanceConfig("host");
|
||||
await instanceConfig.load(serialized_config, "controller");
|
||||
|
||||
let instanceDir = await this._createNewInstanceDir(instanceConfig.get("instance.name"));
|
||||
|
||||
logger.info(`Creating ${instanceDir}`);
|
||||
await Instance.create(instanceDir, this.config.get("slave.factorio_directory"));
|
||||
await Instance.create(instanceDir, this.config.get("host.factorio_directory"));
|
||||
instanceInfo = {
|
||||
path: instanceDir,
|
||||
config: instanceConfig,
|
||||
@@ -335,8 +335,8 @@ class Slave extends libLink.Link {
|
||||
}
|
||||
|
||||
// Somewhat hacky, but in the event of a lost session the status is
|
||||
// resent on assigment since the master server sends an assigment
|
||||
// request for all the instances it knows should be on this slave.
|
||||
// resent on assigment since the controller sends an assigment
|
||||
// request for all the instances it knows should be on this host.
|
||||
let instanceConnection = this.instanceConnections.get(instance_id);
|
||||
libLink.messages.instanceStatusChanged.send(this, {
|
||||
instance_id,
|
||||
@@ -345,7 +345,7 @@ class Slave extends libLink.Link {
|
||||
|
||||
// save a copy of the instance config
|
||||
let warnedOutput = {
|
||||
_warning: "Changes to this file will be overwritten by the master server's copy.",
|
||||
_warning: "Changes to this file will be overwritten by the controller's copy.",
|
||||
...instanceInfo.config.serialize(),
|
||||
};
|
||||
await libFileOps.safeOutputFile(
|
||||
@@ -388,7 +388,7 @@ class Slave extends libLink.Link {
|
||||
* Initialize and connect an unloaded instance
|
||||
*
|
||||
* @param {number} instanceId - ID of instance to initialize.
|
||||
* @returns {Promise<module:slave/slave~InstanceConnection>} connection to instance.
|
||||
* @returns {Promise<module:host/host~InstanceConnection>} connection to instance.
|
||||
*/
|
||||
async _connectInstance(instanceId) {
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
@@ -399,7 +399,7 @@ class Slave extends libLink.Link {
|
||||
let [connectionClient, connectionServer] = libLink.VirtualConnector.makePair();
|
||||
let instanceConnection = new InstanceConnection(connectionServer, this, instanceId);
|
||||
let instance = new Instance(
|
||||
this, connectionClient, instanceInfo.path, this.config.get("slave.factorio_directory"), instanceInfo.config
|
||||
this, connectionClient, instanceInfo.path, this.config.get("host.factorio_directory"), instanceInfo.config
|
||||
);
|
||||
|
||||
this.instanceConnections.set(instanceId, instanceConnection);
|
||||
@@ -422,8 +422,8 @@ class Slave extends libLink.Link {
|
||||
for await (let result of libPrometheus.defaultRegistry.collect()) {
|
||||
if (result.metric.name.startsWith("process_")) {
|
||||
results.push(libPrometheus.serializeResult(result, {
|
||||
addLabels: { "slave_id": String(this.config.get("slave.id")) },
|
||||
metricName: result.metric.name.replace("process_", "clusterio_slave_"),
|
||||
addLabels: { "host_id": String(this.config.get("host.id")) },
|
||||
metricName: result.metric.name.replace("process_", "clusterio_host_"),
|
||||
}));
|
||||
|
||||
} else {
|
||||
@@ -601,7 +601,7 @@ class Slave extends libLink.Link {
|
||||
checkRequestSaveName(filename);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
|
||||
let url = new URL(this.config.get("slave.master_url"));
|
||||
let url = new URL(this.config.get("host.controller_url"));
|
||||
url.pathname += `api/stream/${stream_id}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
@@ -656,13 +656,15 @@ class Slave extends libLink.Link {
|
||||
throw err;
|
||||
}
|
||||
|
||||
let url = new URL(this.config.get("slave.master_url"));
|
||||
let url = new URL(this.config.get("host.controller_url"));
|
||||
url.pathname += `api/stream/${stream_id}`;
|
||||
phin({
|
||||
url, method: "PUT",
|
||||
core: { ca: this.tlsCa },
|
||||
data: content,
|
||||
}).catch(err => logger.error(`Error pushing save to master:\n${err.stack}`, this.instanceLogMeta(instance_id)));
|
||||
}).catch(err => {
|
||||
logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instance_id));
|
||||
});
|
||||
}
|
||||
|
||||
async exportDataRequestHandler(message, request) {
|
||||
@@ -704,15 +706,15 @@ class Slave extends libLink.Link {
|
||||
* Discover available instances
|
||||
*
|
||||
* Looks through the instances directory for instances and updates
|
||||
* the slave and master server with the new list of instances.
|
||||
* the host and controller with the new list of instances.
|
||||
*/
|
||||
async updateInstances() {
|
||||
this.discoveredInstanceInfos = await discoverInstances(this.config.get("slave.instances_directory"));
|
||||
this.discoveredInstanceInfos = await discoverInstances(this.config.get("host.instances_directory"));
|
||||
let list = [];
|
||||
for (let [instanceId, instanceInfo] of this.discoveredInstanceInfos) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
list.push({
|
||||
serialized_config: instanceInfo.config.serialize("master"),
|
||||
serialized_config: instanceInfo.config.serialize("controller"),
|
||||
status: instanceConnection ? instanceConnection.status : "stopped",
|
||||
});
|
||||
}
|
||||
@@ -743,7 +745,7 @@ class Slave extends libLink.Link {
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
await libLink.messages.prepareMasterDisconnect.send(instanceConnection);
|
||||
await libLink.messages.prepareControllerDisconnect.send(instanceConnection);
|
||||
}
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
@@ -771,7 +773,7 @@ class Slave extends libLink.Link {
|
||||
for (let instanceId of this.instanceConnections.keys()) {
|
||||
await this.stopInstance(instanceId);
|
||||
}
|
||||
await this.connector.close(1000, "Slave Shutdown");
|
||||
await this.connector.close(1000, "Host Shutdown");
|
||||
|
||||
// Clear silly interval in pidfile library.
|
||||
pidusage.clear();
|
||||
@@ -779,7 +781,7 @@ class Slave extends libLink.Link {
|
||||
setBlocking(true);
|
||||
logger.error(`
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while shutting down slave, please |
|
||||
| Unexpected error occured while shutting down host, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
@@ -790,7 +792,7 @@ ${err.stack}`
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connection to the master is connected, not in the dropped
|
||||
* True if the connection to the controller is connected, not in the dropped
|
||||
* state,and not in the process of disconnecting.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@@ -799,7 +801,7 @@ ${err.stack}`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Slave;
|
||||
module.exports = Host;
|
||||
|
||||
// For testing only
|
||||
module.exports._discoverInstances = discoverInstances;
|
||||
@@ -98,13 +98,13 @@ const serverSettingsActions = {
|
||||
|
||||
/**
|
||||
* Keeps track of the runtime parameters of an instance
|
||||
* @alias module:slave/src/Instance
|
||||
* @alias module:host/src/Instance
|
||||
*/
|
||||
class Instance extends libLink.Link {
|
||||
constructor(slave, connector, dir, factorioDir, instanceConfig) {
|
||||
super("instance", "slave", connector);
|
||||
constructor(host, connector, dir, factorioDir, instanceConfig) {
|
||||
super("instance", "host", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
this._slave = slave;
|
||||
this._host = host;
|
||||
this._dir = dir;
|
||||
|
||||
this.plugins = new Map();
|
||||
@@ -461,10 +461,10 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this._saveStats().catch(err => this.logger.error(`Error saving stats:\n${err.stack}`));
|
||||
}
|
||||
|
||||
async _loadPlugin(pluginInfo, slave) {
|
||||
async _loadPlugin(pluginInfo, host) {
|
||||
let pluginLoadStarted = Date.now();
|
||||
let InstancePluginClass = await libPluginLoader.loadInstancePluginClass(pluginInfo);
|
||||
let instancePlugin = new InstancePluginClass(pluginInfo, this, slave);
|
||||
let instancePlugin = new InstancePluginClass(pluginInfo, this, host);
|
||||
this.plugins.set(pluginInfo.name, instancePlugin);
|
||||
await instancePlugin.init();
|
||||
libPlugin.attachPluginMessages(this, instancePlugin);
|
||||
@@ -509,14 +509,14 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
if (
|
||||
!pluginInfo.instanceEntrypoint
|
||||
|| !this._slave.serverPlugins.has(pluginInfo.name)
|
||||
|| !this._host.serverPlugins.has(pluginInfo.name)
|
||||
|| !this.config.group(pluginInfo.name).get("load_plugin")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._loadPlugin(pluginInfo, this._slave);
|
||||
await this._loadPlugin(pluginInfo, this._host);
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
@@ -593,7 +593,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* a vanilla mods folder. If the instance mods directory does't exist
|
||||
* then it will be created.
|
||||
*
|
||||
* On Linux this creates symlinks to mods in the slave's mods folder, on
|
||||
* On Linux this creates symlinks to mods in the host's mods folder, on
|
||||
* Windows hard links are used instead due to symlinks being privileged.
|
||||
*/
|
||||
async syncMods() {
|
||||
@@ -608,7 +608,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
// TODO validate factorioVersion
|
||||
|
||||
if (!this._slave.config.get("slave.mods_directory_is_shared")) {
|
||||
if (!this._host.config.get("host.mods_directory_is_shared")) {
|
||||
throw new Error("Fetching mods is not implemented");
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
|
||||
// Add mods from mod the pack
|
||||
const modsDir = this._slave.config.get("slave.mods_directory");
|
||||
const modsDir = this._host.config.get("host.mods_directory");
|
||||
for (let mod of this.activeModPack.mods.values()) {
|
||||
const modFile = `${mod.name}_${mod.version}.zip`;
|
||||
const target = path.join(modsDir, modFile);
|
||||
@@ -668,7 +668,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this.logger.verbose("Writing server-adminlist.json");
|
||||
libFileOps.safeOutputFile(
|
||||
this.server.writePath("server-adminlist.json"),
|
||||
JSON.stringify([...this._slave.adminlist], null, 4)
|
||||
JSON.stringify([...this._host.adminlist], null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -676,7 +676,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this.logger.verbose("Writing server-banlist.json");
|
||||
libFileOps.safeOutputFile(
|
||||
this.server.writePath("server-banlist.json"),
|
||||
JSON.stringify([...this._slave.banlist].map(
|
||||
JSON.stringify([...this._host.banlist].map(
|
||||
([username, reason]) => ({ username, reason })
|
||||
), null, 4),
|
||||
);
|
||||
@@ -686,7 +686,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this.logger.verbose("Writing server-whitelist.json");
|
||||
libFileOps.safeOutputFile(
|
||||
this.server.writePath("server-whitelist.json"),
|
||||
JSON.stringify([...this._slave.whitelist], null, 4)
|
||||
JSON.stringify([...this._host.whitelist], null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -897,7 +897,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
if (this.config.get("factorio.sync_whitelist")) {
|
||||
await this.sendRcon("/whitelist clear");
|
||||
for (let player of this._slave.whitelist) {
|
||||
for (let player of this._host.whitelist) {
|
||||
await this.sendRcon(`/whitelist ${player}`);
|
||||
}
|
||||
}
|
||||
@@ -962,12 +962,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.server.kill(true);
|
||||
}
|
||||
|
||||
async masterConnectionEventEventHandler(message) {
|
||||
await libPlugin.invokeHook(this.plugins, "onMasterConnectionEvent", message.data.event);
|
||||
async controllerConnectionEventEventHandler(message) {
|
||||
await libPlugin.invokeHook(this.plugins, "onControllerConnectionEvent", message.data.event);
|
||||
}
|
||||
|
||||
async prepareMasterDisconnectRequestHandler() {
|
||||
await libPlugin.invokeHook(this.plugins, "onPrepareMasterDisconnect");
|
||||
async prepareControllerDisconnectRequestHandler() {
|
||||
await libPlugin.invokeHook(this.plugins, "onPrepareControllerDisconnect");
|
||||
}
|
||||
|
||||
async getMetricsRequestHandler() {
|
||||
@@ -1072,16 +1072,16 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
let zip = await libFactorio.exportData(this.server);
|
||||
|
||||
let content = await zip.generateAsync({ type: "nodebuffer" });
|
||||
let url = new URL(this._slave.config.get("slave.master_url"));
|
||||
let url = new URL(this._host.config.get("host.controller_url"));
|
||||
url.pathname += "api/upload-export";
|
||||
url.searchParams.set("mod_pack_id", this.activeModPack.id);
|
||||
let response = await phin({
|
||||
url, method: "PUT",
|
||||
data: content,
|
||||
core: { ca: this._slave.tlsCa },
|
||||
core: { ca: this._host.tlsCa },
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"x-access-token": this._slave.config.get("slave.master_token"),
|
||||
"x-access-token": this._host.config.get("host.controller_token"),
|
||||
},
|
||||
});
|
||||
if (response.statusCode !== 200) {
|
||||
+18
-18
@@ -5,29 +5,29 @@ const libErrors = require("@clusterio/lib/errors");
|
||||
|
||||
|
||||
class InstanceConnection extends libLink.Link {
|
||||
constructor(connector, slave, instanceId) {
|
||||
super("slave", "instance", connector);
|
||||
this.slave = slave;
|
||||
constructor(connector, host, instanceId) {
|
||||
super("host", "instance", connector);
|
||||
this.host = host;
|
||||
this.instanceId = instanceId;
|
||||
this.plugins = new Map();
|
||||
this.status = "stopped";
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
for (let pluginInfo of slave.pluginInfos) {
|
||||
for (let pluginInfo of host.pluginInfos) {
|
||||
libPlugin.attachPluginMessages(this, { info: pluginInfo });
|
||||
}
|
||||
}
|
||||
|
||||
async forwardRequestToMaster(message, request) {
|
||||
return await request.send(this.slave, message.data);
|
||||
async forwardRequestToController(message, request) {
|
||||
return await request.send(this.host, message.data);
|
||||
}
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceConnection = this.slave.instanceConnections.get(instanceId);
|
||||
let instanceConnection = this.host.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
// Instance is probably on another slave
|
||||
return await this.forwardRequestToMaster(message, request);
|
||||
// Instance is probably on another host
|
||||
return await this.forwardRequestToController(message, request);
|
||||
}
|
||||
|
||||
if (request.plugin && !instanceConnection.plugins.has(request.plugin)) {
|
||||
@@ -40,10 +40,10 @@ class InstanceConnection extends libLink.Link {
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceConnection = this.slave.instanceConnections.get(instanceId);
|
||||
let instanceConnection = this.host.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
// Instance is probably on another slave
|
||||
await this.forwardEventToMaster(message, event);
|
||||
// Instance is probably on another host
|
||||
await this.forwardEventToController(message, event);
|
||||
return;
|
||||
}
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { return; }
|
||||
@@ -51,16 +51,16 @@ class InstanceConnection extends libLink.Link {
|
||||
event.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
async forwardEventToMaster(message, event) {
|
||||
if (!this.slave.connector.hasSession) {
|
||||
async forwardEventToController(message, event) {
|
||||
if (!this.host.connector.hasSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.send(this.slave, message.data);
|
||||
event.send(this.host, message.data);
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
for (let instanceConnection of this.slave.instanceConnections.values()) {
|
||||
for (let instanceConnection of this.host.instanceConnections.values()) {
|
||||
// Do not broadcast back to the source
|
||||
if (instanceConnection === this) { continue; }
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { continue; }
|
||||
@@ -76,9 +76,9 @@ class InstanceConnection extends libLink.Link {
|
||||
async instanceStatusChangedEventHandler(message, event) {
|
||||
this.status = message.data.status;
|
||||
if (this.status === "stopped") {
|
||||
this.slave.instanceConnections.delete(this.instanceId);
|
||||
this.host.instanceConnections.delete(this.instanceId);
|
||||
}
|
||||
this.forwardEventToMaster(message, event);
|
||||
this.forwardEventToController(message, event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Common Library for Clusterio
|
||||
|
||||
This package contains functionality shared between master, slave, ctl and/or plugins of Clusterio.
|
||||
This package contains functionality shared between controller, host, ctl and/or plugins of Clusterio.
|
||||
|
||||
See [the Clusterio repository](https://github.com/clusterio/clusterio) for more information on Clusterio.
|
||||
+21
-21
@@ -135,35 +135,35 @@ class CommandTree {
|
||||
|
||||
|
||||
/**
|
||||
* Resolve a string into a slave ID
|
||||
* Resolve a string into a host ID
|
||||
*
|
||||
* Resolves a string with either an slave name or an id into an integer with
|
||||
* the slave ID.
|
||||
* Resolves a string with either an host name or an id into an integer with
|
||||
* the host ID.
|
||||
*
|
||||
* @param {module:lib/link.Link} client - link to master server to query slave on.
|
||||
* @param {string} slaveName - string with name or id of slave.
|
||||
* @returns {Promise<number>} slave ID.
|
||||
* @param {module:lib/link.Link} client - link to controller to query host on.
|
||||
* @param {string} hostName - string with name or id of host.
|
||||
* @returns {Promise<number>} host ID.
|
||||
* @static
|
||||
*/
|
||||
async function resolveSlave(client, slaveName) {
|
||||
let slaveId;
|
||||
if (/^-?\d+$/.test(slaveName)) {
|
||||
slaveId = parseInt(slaveName, 10);
|
||||
async function resolveHost(client, hostName) {
|
||||
let hostId;
|
||||
if (/^-?\d+$/.test(hostName)) {
|
||||
hostId = parseInt(hostName, 10);
|
||||
} else {
|
||||
let response = await libLink.messages.listSlaves.send(client);
|
||||
for (let slave of response.list) {
|
||||
if (slave.name === slaveName) {
|
||||
slaveId = slave.id;
|
||||
let response = await libLink.messages.listHosts.send(client);
|
||||
for (let host of response.list) {
|
||||
if (host.name === hostName) {
|
||||
hostId = host.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slaveId === undefined) {
|
||||
throw new libErrors.CommandError(`No slave named ${slaveName}`);
|
||||
if (hostId === undefined) {
|
||||
throw new libErrors.CommandError(`No host named ${hostName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return slaveId;
|
||||
return hostId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +172,7 @@ async function resolveSlave(client, slaveName) {
|
||||
* Resolves a string with either an instance name or an id into an integer
|
||||
* with the instance ID.
|
||||
*
|
||||
* @param {module:lib/link.Link} client - link to master server to query instance on.
|
||||
* @param {module:lib/link.Link} client - link to controller to query instance on.
|
||||
* @param {string} instanceName - string with name or id of instance.
|
||||
* @returns {Promise<number>} instance ID.
|
||||
* @static
|
||||
@@ -205,7 +205,7 @@ async function resolveInstance(client, instanceName) {
|
||||
* with the mod pack ID.
|
||||
*
|
||||
* @param {module:lib/link.Link} client -
|
||||
* link to master server to query mod pack on.
|
||||
* link to controller to query mod pack on.
|
||||
* @param {string} modPackName - string with name or id of mod pack.
|
||||
* @returns {Promise<number>} mod pack ID.
|
||||
* @static
|
||||
@@ -237,7 +237,7 @@ async function resolveModPack(client, modPackName) {
|
||||
* Resolves a string with either a role name or an id into an object
|
||||
* representing the role.
|
||||
*
|
||||
* @param {module:lib/link.Link} client - link to master server to query role on.
|
||||
* @param {module:lib/link.Link} client - link to controller to query role on.
|
||||
* @param {string} roleName - string with name or id of role.
|
||||
* @returns {Promise<object>} Role info.
|
||||
* @static
|
||||
@@ -275,7 +275,7 @@ module.exports = {
|
||||
Command,
|
||||
CommandTree,
|
||||
|
||||
resolveSlave,
|
||||
resolveHost,
|
||||
resolveInstance,
|
||||
resolveModPack,
|
||||
retrieveRole,
|
||||
|
||||
@@ -5,35 +5,35 @@ const classes = require("./classes");
|
||||
|
||||
|
||||
/**
|
||||
* Master config group for {@link module:lib/config.MasterConfig}
|
||||
* Controller config group for {@link module:lib/config.ControllerConfig}
|
||||
* @extends module:lib/config.ConfigGroup
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class MasterGroup extends classes.ConfigGroup { }
|
||||
MasterGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterGroup.groupName = "master";
|
||||
MasterGroup.define({
|
||||
class ControllerGroup extends classes.ConfigGroup { }
|
||||
ControllerGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerGroup.groupName = "controller";
|
||||
ControllerGroup.define({
|
||||
name: "name",
|
||||
title: "Name",
|
||||
description: "Name of the cluster.",
|
||||
type: "string",
|
||||
initial_value: "Your Cluster",
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "mods_directory",
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods shared with the cluster are stored.",
|
||||
type: "string",
|
||||
initial_value: "mods",
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "database_directory",
|
||||
title: "Database directory",
|
||||
description: "Directory where item and configuration data is stored.",
|
||||
type: "string",
|
||||
initial_value: "database",
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "http_port",
|
||||
title: "HTTP Port",
|
||||
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
|
||||
@@ -42,7 +42,7 @@ MasterGroup.define({
|
||||
optional: true,
|
||||
initial_value: 8080,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "https_port",
|
||||
title: "HTTPS Port",
|
||||
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
|
||||
@@ -50,7 +50,7 @@ MasterGroup.define({
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "bind_address",
|
||||
title: "Bind Address",
|
||||
description: "IP address to bind the HTTP and HTTPS ports on.",
|
||||
@@ -58,14 +58,14 @@ MasterGroup.define({
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "external_address",
|
||||
title: "External Address",
|
||||
description: "Public facing address the master server is hosted on.",
|
||||
description: "Public facing address the controller is hosted on.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "tls_certificate",
|
||||
title: "TLS Certificate",
|
||||
description: "Path to the certificate to use for HTTPS.",
|
||||
@@ -73,7 +73,7 @@ MasterGroup.define({
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "tls_private_key",
|
||||
title: "TLS Private Key",
|
||||
description: "Path to the private key to use for HTTPS.",
|
||||
@@ -81,10 +81,10 @@ MasterGroup.define({
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
access: ["master"],
|
||||
ControllerGroup.define({
|
||||
access: ["controller"],
|
||||
name: "auth_secret",
|
||||
title: "Master Authentication Secret",
|
||||
title: "Controller Authentication Secret",
|
||||
description:
|
||||
"Secret used to generate and verify authentication tokens. " +
|
||||
"Should be a long string of random letters and numbers. " +
|
||||
@@ -93,42 +93,42 @@ MasterGroup.define({
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "heartbeat_interval",
|
||||
title: "Heartbeat Interval",
|
||||
description: "Interval heartbeats are sent out on WebSocket connections.",
|
||||
type: "number",
|
||||
initial_value: 15,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "session_timeout",
|
||||
title: "Session Timeout",
|
||||
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "metrics_timeout",
|
||||
title: "Metrics Timeout",
|
||||
description: "Timeout in seconds for metrics gathering from slaves.",
|
||||
description: "Timeout in seconds for metrics gathering from hosts.",
|
||||
type: "number",
|
||||
initial_value: 8,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "proxy_stream_timeout",
|
||||
title: "Proxy Stream Timeout",
|
||||
description: "Timeout in seconds for proxy streams to start flowing.",
|
||||
type: "number",
|
||||
initial_value: 15,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "default_mod_pack_id",
|
||||
title: "Default Mod Pack",
|
||||
description: "Mod pack used by default for instances.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
ControllerGroup.define({
|
||||
name: "default_role_id",
|
||||
title: "Default role",
|
||||
description: "ID of role assigned by default to new users.",
|
||||
@@ -136,107 +136,107 @@ MasterGroup.define({
|
||||
optional: true,
|
||||
initial_value: 1,
|
||||
});
|
||||
MasterGroup.finalize();
|
||||
ControllerGroup.finalize();
|
||||
|
||||
/**
|
||||
* Master Config class
|
||||
* Controller Config class
|
||||
* @extends module:lib/config.Config
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class MasterConfig extends classes.Config { }
|
||||
MasterConfig.registerGroup(MasterGroup);
|
||||
class ControllerConfig extends classes.Config { }
|
||||
ControllerConfig.registerGroup(ControllerGroup);
|
||||
|
||||
|
||||
/**
|
||||
* Slave config group for {@link module:lib/config.SlaveConfig}
|
||||
* Host config group for {@link module:lib/config.HostConfig}
|
||||
* @extends module:lib/config.ConfigGroup
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class SlaveGroup extends classes.ConfigGroup {}
|
||||
SlaveGroup.defaultAccess = ["master", "slave", "control"];
|
||||
SlaveGroup.groupName = "slave";
|
||||
SlaveGroup.define({
|
||||
class HostGroup extends classes.ConfigGroup {}
|
||||
HostGroup.defaultAccess = ["controller", "host", "control"];
|
||||
HostGroup.groupName = "host";
|
||||
HostGroup.define({
|
||||
name: "name",
|
||||
description: "Name of the slave",
|
||||
description: "Name of the host",
|
||||
type: "string",
|
||||
initial_value: "New Slave",
|
||||
initial_value: "New Host",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "id",
|
||||
description: "ID of the slave",
|
||||
description: "ID of the host",
|
||||
type: "number",
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "factorio_directory",
|
||||
description: "Path to directory to look for factorio installs",
|
||||
type: "string",
|
||||
initial_value: "factorio",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "mods_directory",
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods for instances are cached.",
|
||||
type: "string",
|
||||
initial_value: "mods",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "mods_directory_is_shared",
|
||||
title: "Mods Directory is Shared",
|
||||
description: "True if the mods directory is shared with the master server.",
|
||||
description: "True if the mods directory is shared with the controller.",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "instances_directory",
|
||||
description: "Path to directory to store instances in.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "instances",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "master_url",
|
||||
description: "URL to connect to the master server at",
|
||||
HostGroup.define({
|
||||
name: "controller_url",
|
||||
description: "URL to connect to the controller at",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "http://localhost:8080/",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "master_token",
|
||||
description: "Token to authenticate to master server with.",
|
||||
HostGroup.define({
|
||||
name: "controller_token",
|
||||
description: "Token to authenticate to controller with.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "enter token here",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "tls_ca",
|
||||
description: "Path to Certificate Authority to validate TLS connection to master against.",
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "public_address",
|
||||
description: "Public facing address players should connect to in order to join instances on this slave",
|
||||
description: "Public facing address players should connect to in order to join instances on this host",
|
||||
type: "string",
|
||||
initial_value: "localhost",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
HostGroup.define({
|
||||
name: "max_reconnect_delay",
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
SlaveGroup.finalize();
|
||||
HostGroup.finalize();
|
||||
|
||||
/**
|
||||
* Slave Config class
|
||||
* Host Config class
|
||||
* @extends module:lib/config.Config
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class SlaveConfig extends classes.Config { }
|
||||
SlaveConfig.registerGroup(SlaveGroup);
|
||||
class HostConfig extends classes.Config { }
|
||||
HostConfig.registerGroup(HostGroup);
|
||||
|
||||
|
||||
/**
|
||||
@@ -245,7 +245,7 @@ SlaveConfig.registerGroup(SlaveGroup);
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class InstanceGroup extends classes.ConfigGroup { }
|
||||
InstanceGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceGroup.groupName = "instance";
|
||||
InstanceGroup.define({
|
||||
name: "name",
|
||||
@@ -259,13 +259,13 @@ InstanceGroup.define({
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "assigned_slave",
|
||||
name: "assigned_host",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "auto_start",
|
||||
description: "Automatically start this instance when the slave hosting it is started up",
|
||||
description: "Automatically start this instance when the host hosting it is started up",
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
@@ -277,7 +277,7 @@ InstanceGroup.finalize();
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class FactorioGroup extends classes.ConfigGroup { }
|
||||
FactorioGroup.defaultAccess = ["master", "slave", "control"];
|
||||
FactorioGroup.defaultAccess = ["controller", "host", "control"];
|
||||
FactorioGroup.groupName = "factorio";
|
||||
FactorioGroup.define({
|
||||
name: "version",
|
||||
@@ -353,7 +353,7 @@ FactorioGroup.define({
|
||||
"require_user_verification", "tags", "visibility",
|
||||
],
|
||||
type: "object",
|
||||
initial_value: {}, // See create instance handler in master.
|
||||
initial_value: {}, // See create instance handler in controller.
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "verbose_logging",
|
||||
@@ -371,19 +371,19 @@ FactorioGroup.define({
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_adminlist",
|
||||
description: "Synchronize adminlist with master server",
|
||||
description: "Synchronize adminlist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_whitelist",
|
||||
description: "Synchronize whitelist with master server",
|
||||
description: "Synchronize whitelist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_banlist",
|
||||
description: "Synchronize banlist with master server",
|
||||
description: "Synchronize banlist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
@@ -415,20 +415,20 @@ class ControlGroup extends classes.ConfigGroup {}
|
||||
ControlGroup.defaultAccess = ["control"];
|
||||
ControlGroup.groupName = "control";
|
||||
ControlGroup.define({
|
||||
name: "master_url",
|
||||
description: "URL to connect to the master server at",
|
||||
name: "controller_url",
|
||||
description: "URL to connect to the controller at",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "master_token",
|
||||
description: "Token to authenticate to master server with.",
|
||||
name: "controller_token",
|
||||
description: "Token to authenticate to controller with.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "tls_ca",
|
||||
description: "Path to Certificate Authority to validate TLS connection to master against.",
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
@@ -472,16 +472,16 @@ function validateGroup(pluginInfo, groupName) {
|
||||
*/
|
||||
function registerPluginConfigGroups(pluginInfos) {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
if (pluginInfo.MasterConfigGroup) {
|
||||
validateGroup(pluginInfo, "MasterConfigGroup");
|
||||
MasterConfig.registerGroup(pluginInfo.MasterConfigGroup);
|
||||
if (pluginInfo.ControllerConfigGroup) {
|
||||
validateGroup(pluginInfo, "ControllerConfigGroup");
|
||||
ControllerConfig.registerGroup(pluginInfo.ControllerConfigGroup);
|
||||
|
||||
} else {
|
||||
class MasterConfigGroup extends classes.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = pluginInfo.name;
|
||||
MasterConfigGroup.finalize();
|
||||
MasterConfig.registerGroup(MasterConfigGroup);
|
||||
class ControllerConfigGroup extends classes.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = pluginInfo.name;
|
||||
ControllerConfigGroup.finalize();
|
||||
ControllerConfig.registerGroup(ControllerConfigGroup);
|
||||
}
|
||||
|
||||
if (pluginInfo.instanceEntrypoint) {
|
||||
@@ -491,7 +491,7 @@ function registerPluginConfigGroups(pluginInfos) {
|
||||
|
||||
} else {
|
||||
class InstanceConfigGroup extends classes.PluginConfigGroup { }
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceConfigGroup.groupName = pluginInfo.name;
|
||||
InstanceConfigGroup.finalize();
|
||||
InstanceConfig.registerGroup(InstanceConfigGroup);
|
||||
@@ -506,21 +506,21 @@ function registerPluginConfigGroups(pluginInfos) {
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
function finalizeConfigs() {
|
||||
MasterConfig.finalize();
|
||||
SlaveConfig.finalize();
|
||||
ControllerConfig.finalize();
|
||||
HostConfig.finalize();
|
||||
InstanceConfig.finalize();
|
||||
ControlConfig.finalize();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MasterGroup,
|
||||
SlaveGroup,
|
||||
ControllerGroup,
|
||||
HostGroup,
|
||||
InstanceGroup,
|
||||
FactorioGroup,
|
||||
ControlGroup,
|
||||
|
||||
MasterConfig,
|
||||
SlaveConfig,
|
||||
ControllerConfig,
|
||||
HostConfig,
|
||||
InstanceConfig,
|
||||
ControlConfig,
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const { integerModVersion, integerFactorioVersion, modVersionRegExp } = require(
|
||||
|
||||
|
||||
/**
|
||||
* Info about a mod available on the master server.
|
||||
* Info about a mod available on the controller.
|
||||
*
|
||||
* See https://wiki.factorio.com/Tutorial:Mod_structure#info.json for
|
||||
* details of the invidiual fields sourced from info.json.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Implementation of master databases
|
||||
* Implementation of controller databases
|
||||
*
|
||||
* @module lib/database
|
||||
* @author Hornwitser
|
||||
@@ -159,7 +159,7 @@ function checkCount(count) {
|
||||
/**
|
||||
* Item database
|
||||
*
|
||||
* Stores counts of items for the master server. Items that have not been
|
||||
* Stores counts of items for the controller. Items that have not been
|
||||
* stored in the database are treated as having a stored count of zero. When
|
||||
* serialized the database discards entries with a zero counts, and when
|
||||
* deserialized the content is verified.
|
||||
|
||||
@@ -34,8 +34,8 @@ async function generateExportMod(server) {
|
||||
clean: false,
|
||||
build: true,
|
||||
pack: true,
|
||||
// XXX fix path when moving lib/factorio to slave
|
||||
sourceDir: path.join(__dirname, "..", "..", "slave", "lua", "export"),
|
||||
// XXX fix path when moving lib/factorio to host
|
||||
sourceDir: path.join(__dirname, "..", "..", "host", "lua", "export"),
|
||||
outputDir: server.writePath("mods"),
|
||||
bumpPatch: false,
|
||||
factorioVersion: server.version.replace(/\.\d+$/, ""),
|
||||
|
||||
@@ -179,7 +179,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connector for master server clients
|
||||
* Connector for controller clients
|
||||
*
|
||||
* @extends module:lib/link.WebSocketBaseConnector
|
||||
* @memberof module:lib/link
|
||||
@@ -278,7 +278,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the master server
|
||||
* Connect to the controller
|
||||
*/
|
||||
async connect() {
|
||||
this._check("closed");
|
||||
@@ -294,7 +294,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
let url = new URL(this._url);
|
||||
url.pathname += "api/socket";
|
||||
|
||||
// Open WebSocket to master
|
||||
// Open WebSocket to controller
|
||||
logger.verbose(`Connector | connecting to ${url}`);
|
||||
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
@@ -447,7 +447,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
|
||||
let { type, data } = message;
|
||||
if (type === "hello") {
|
||||
logger.verbose(`Connector | received hello from master version ${data.version}`);
|
||||
logger.verbose(`Connector | received hello from controller version ${data.version}`);
|
||||
this.emit("hello", data);
|
||||
if (this._sessionToken) {
|
||||
logger.verbose("Connector | Attempting resume");
|
||||
@@ -460,7 +460,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
} else if (type === "ready") {
|
||||
logger.verbose("Connector | received ready from master");
|
||||
logger.verbose("Connector | received ready from controller");
|
||||
this._state = "connected";
|
||||
this._sessionToken = data.session_token;
|
||||
this._sessionTimeout = data.session_timeout;
|
||||
@@ -485,7 +485,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
this.emit("resume");
|
||||
|
||||
} else if (type === "invalidate") {
|
||||
logger.warn("Connector | session invalidated by master");
|
||||
logger.warn("Connector | session invalidated by controller");
|
||||
this._state = "connecting";
|
||||
this._seq = 1;
|
||||
this._lastReceivedSeq = null;
|
||||
|
||||
@@ -6,10 +6,10 @@ const libErrors = require("../errors");
|
||||
const { logger } = require("../logging");
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
// link: Either side of a master - client connection
|
||||
// link: Either side of a controller - client connection
|
||||
// connector: Adapter between a link and a socket connection
|
||||
// connection: the master server side of a link
|
||||
// client: the side that is not the master server of a link
|
||||
// connection: the controller side of a link
|
||||
// client: the side that is not the controller of a link
|
||||
// message: the complete object sent using the 'message' event
|
||||
// data: the data property of a message, essentially the payload.
|
||||
// type: the type property of a message, identifies what to expect in data.
|
||||
|
||||
+153
-151
@@ -67,11 +67,11 @@ class Request extends Message {
|
||||
* containing "<source>-<target>" specifications.
|
||||
* @param {?string} permission -
|
||||
* Permission required to send this request. Only applies to
|
||||
* requests sent from control to master.
|
||||
* requests sent from control to controller.
|
||||
* @param {?string} forwardTo -
|
||||
* Optional target to forward this request to. 'instance' add
|
||||
* instance_id into the requestProperties and forward to the given
|
||||
* instance. 'master' forwards it to the master server.
|
||||
* instance. 'controller' forwards it to the controller.
|
||||
* @param {?Array<string>} requestRequired -
|
||||
* List of properties that are required to be present in the data
|
||||
* payload of this request. Defaults to all.
|
||||
@@ -101,11 +101,13 @@ class Request extends Message {
|
||||
this.requestType = `${type}_request`;
|
||||
this.responseType = `${type}_response`;
|
||||
|
||||
if (permission === undefined && links.includes("control-master")) {
|
||||
throw new Error(`permission is required for ${this.type} request over control-master links`);
|
||||
if (permission === undefined && links.includes("control-controller")) {
|
||||
throw new Error(`permission is required for ${this.type} request over control-controller links`);
|
||||
}
|
||||
if (permission && !links.includes("control-master")) {
|
||||
throw new Error(`permission is not allowed on ${this.type} request as it is not over control-master link`);
|
||||
if (permission && !links.includes("control-controller")) {
|
||||
throw new Error(
|
||||
`permission is not allowed on ${this.type} request as it is not over control-controller link`
|
||||
);
|
||||
}
|
||||
|
||||
if (!requestRequired) {
|
||||
@@ -119,7 +121,7 @@ class Request extends Message {
|
||||
...requestProperties,
|
||||
};
|
||||
|
||||
} else if (forwardTo !== "master" && forwardTo !== null) {
|
||||
} else if (forwardTo !== "controller" && forwardTo !== null) {
|
||||
throw new Error(`Invalid forwardTo value ${forwardTo}`);
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ class Request extends Message {
|
||||
* If forwardTo was set to 'instance' the `forwardRequestToInstance`
|
||||
* method on the link is used as the default handler.
|
||||
*
|
||||
* If forwardTo was set to 'master' the `forwardRequesToMaster` method
|
||||
* If forwardTo was set to 'controller' the `forwardRequesToController` method
|
||||
* on the link is used as the default handler.
|
||||
*
|
||||
* Does nothing if the link is neither a source nor a target for this
|
||||
@@ -201,8 +203,8 @@ class Request extends Message {
|
||||
if (!handler) {
|
||||
if (this.forwardTo === "instance") {
|
||||
handler = link.forwardRequestToInstance;
|
||||
} else if (this.forwardTo === "master") {
|
||||
handler = link.forwardRequestToMaster;
|
||||
} else if (this.forwardTo === "controller") {
|
||||
handler = link.forwardRequestToController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +212,8 @@ class Request extends Message {
|
||||
throw new MissingLinkHandlerError(this.requestType, link.source, link.target);
|
||||
}
|
||||
|
||||
// Check permission if this is a handler for a control connection on the master
|
||||
if (this.permission !== null && `${link.source}-${link.target}` === "master-control") {
|
||||
// Check permission if this is a handler for a control connection on the controller
|
||||
if (this.permission !== null && `${link.source}-${link.target}` === "controller-control") {
|
||||
let origHandler = handler;
|
||||
handler = async function(message, request) {
|
||||
// Abuse this binding to get a hold of the ControlConnection instance
|
||||
@@ -276,16 +278,16 @@ class Request extends Message {
|
||||
let messages = {};
|
||||
|
||||
// Connection requests
|
||||
let wsLinks = ["master-control", "control-master", "master-slave", "slave-master"];
|
||||
let wsLinks = ["controller-control", "control-controller", "controller-host", "host-controller"];
|
||||
messages.prepareDisconnect = new Request({
|
||||
type: "prepare_disconnect",
|
||||
links: wsLinks,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
messages.prepareMasterDisconnect = new Request({
|
||||
type: "prepare_master_disconnect",
|
||||
links: ["slave-instance"],
|
||||
messages.prepareControllerDisconnect = new Request({
|
||||
type: "prepare_controller_disconnect",
|
||||
links: ["host-instance"],
|
||||
});
|
||||
|
||||
messages.ping = new Request({
|
||||
@@ -296,35 +298,35 @@ messages.ping = new Request({
|
||||
|
||||
messages.debugDumpWs = new Request({
|
||||
type: "debug_dump_ws",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.debug.dump_ws",
|
||||
});
|
||||
|
||||
|
||||
// Management requests
|
||||
messages.getMasterConfig = new Request({
|
||||
type: "get_master_config",
|
||||
links: ["control-master"],
|
||||
permission: "core.master.get_config",
|
||||
messages.getControllerConfig = new Request({
|
||||
type: "get_controller_config",
|
||||
links: ["control-controller"],
|
||||
permission: "core.controller.get_config",
|
||||
responseProperties: {
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.setMasterConfigField = new Request({
|
||||
type: "set_master_config_field",
|
||||
links: ["control-master"],
|
||||
permission: "core.master.update_config",
|
||||
messages.setControllerConfigField = new Request({
|
||||
type: "set_controller_config_field",
|
||||
links: ["control-controller"],
|
||||
permission: "core.controller.update_config",
|
||||
requestProperties: {
|
||||
"field": { type: "string" },
|
||||
"value": { type: "string" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.setMasterConfigProp = new Request({
|
||||
type: "set_master_config_prop",
|
||||
links: ["control-master"],
|
||||
permission: "core.master.update_config",
|
||||
messages.setControllerConfigProp = new Request({
|
||||
type: "set_controller_config_prop",
|
||||
links: ["control-controller"],
|
||||
permission: "core.controller.update_config",
|
||||
requestRequired: ["field", "prop"],
|
||||
requestProperties: {
|
||||
"field": { type: "string" },
|
||||
@@ -333,7 +335,7 @@ messages.setMasterConfigProp = new Request({
|
||||
},
|
||||
});
|
||||
|
||||
let slaveProperties = {
|
||||
let hostProperties = {
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
"name": { type: "string" },
|
||||
@@ -342,29 +344,29 @@ let slaveProperties = {
|
||||
"public_address": { type: ["null", "string"] },
|
||||
};
|
||||
|
||||
messages.listSlaves = new Request({
|
||||
type: "list_slaves",
|
||||
links: ["control-master"],
|
||||
permission: "core.slave.list",
|
||||
messages.listHosts = new Request({
|
||||
type: "list_hosts",
|
||||
links: ["control-controller"],
|
||||
permission: "core.host.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
required: ["agent", "version", "name", "id", "connected"],
|
||||
properties: slaveProperties,
|
||||
properties: hostProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
messages.setSlaveSubscriptions = new Request({
|
||||
type: "set_slave_subscriptions",
|
||||
links: ["control-master"],
|
||||
permission: "core.slave.subscribe",
|
||||
messages.setHostSubscriptions = new Request({
|
||||
type: "set_host_subscriptions",
|
||||
links: ["control-controller"],
|
||||
permission: "core.host.subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
"slave_ids": {
|
||||
"host_ids": {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
@@ -374,7 +376,7 @@ messages.setSlaveSubscriptions = new Request({
|
||||
let instanceProperties = {
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"assigned_slave": { type: ["null", "integer"] },
|
||||
"assigned_host": { type: ["null", "integer"] },
|
||||
"game_port": { type: ["null", "integer"] },
|
||||
"status": { enum: [
|
||||
"unknown", "unassigned", "stopped", "starting", "running", "stopping",
|
||||
@@ -384,7 +386,7 @@ let instanceProperties = {
|
||||
|
||||
messages.getInstance = new Request({
|
||||
type: "get_instance",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.get",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
@@ -394,14 +396,14 @@ messages.getInstance = new Request({
|
||||
|
||||
messages.listInstances = new Request({
|
||||
type: "list_instances",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
required: ["name", "id", "assigned_slave", "status"],
|
||||
required: ["name", "id", "assigned_host", "status"],
|
||||
properties: instanceProperties,
|
||||
},
|
||||
},
|
||||
@@ -410,7 +412,7 @@ messages.listInstances = new Request({
|
||||
|
||||
messages.setInstanceSubscriptions = new Request({
|
||||
type: "set_instance_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
@@ -421,22 +423,22 @@ messages.setInstanceSubscriptions = new Request({
|
||||
},
|
||||
});
|
||||
|
||||
messages.generateSlaveToken = new Request({
|
||||
type: "generate_slave_token",
|
||||
links: ["control-master"],
|
||||
permission: "core.slave.generate_token",
|
||||
messages.generateHostToken = new Request({
|
||||
type: "generate_host_token",
|
||||
links: ["control-controller"],
|
||||
permission: "core.host.generate_token",
|
||||
requestProperties: {
|
||||
"slave_id": { type: ["integer", "null"] },
|
||||
"host_id": { type: ["integer", "null"] },
|
||||
},
|
||||
responseProperties: {
|
||||
"token": { type: "string" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.createSlaveConfig = new Request({
|
||||
type: "create_slave_config",
|
||||
links: ["control-master"],
|
||||
permission: "core.slave.create_config",
|
||||
messages.createHostConfig = new Request({
|
||||
type: "create_host_config",
|
||||
links: ["control-controller"],
|
||||
permission: "core.host.create_config",
|
||||
requestProperties: {
|
||||
"id": { type: ["integer", "null"] },
|
||||
"name": { type: ["string", "null"] },
|
||||
@@ -449,7 +451,7 @@ messages.createSlaveConfig = new Request({
|
||||
|
||||
messages.createInstance = new Request({
|
||||
type: "create_instance",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.create",
|
||||
requestProperties: {
|
||||
"serialized_config": { type: "object" },
|
||||
@@ -458,7 +460,7 @@ messages.createInstance = new Request({
|
||||
|
||||
messages.getInstanceConfig = new Request({
|
||||
type: "get_instance_config",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.get_config",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
@@ -470,7 +472,7 @@ messages.getInstanceConfig = new Request({
|
||||
|
||||
messages.setInstanceConfigField = new Request({
|
||||
type: "set_instance_config_field",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.update_config",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
@@ -481,7 +483,7 @@ messages.setInstanceConfigField = new Request({
|
||||
|
||||
messages.setInstanceConfigProp = new Request({
|
||||
type: "set_instance_config_prop",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.update_config",
|
||||
requestRequired: ["instance_id", "field", "prop"],
|
||||
requestProperties: {
|
||||
@@ -494,17 +496,17 @@ messages.setInstanceConfigProp = new Request({
|
||||
|
||||
messages.assignInstanceCommand = new Request({
|
||||
type: "assign_instance_command",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.assign",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"slave_id": { type: ["integer", "null"] },
|
||||
"host_id": { type: ["integer", "null"] },
|
||||
},
|
||||
});
|
||||
|
||||
messages.startInstance = new Request({
|
||||
type: "start_instance",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.start",
|
||||
requestProperties: {
|
||||
"save": { type: ["string", "null"] },
|
||||
@@ -531,7 +533,7 @@ let saveList = {
|
||||
|
||||
messages.listSaves = new Request({
|
||||
type: "list_saves",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.save.list",
|
||||
forwardTo: "instance",
|
||||
responseProperties: {
|
||||
@@ -541,7 +543,7 @@ messages.listSaves = new Request({
|
||||
|
||||
messages.createSave = new Request({
|
||||
type: "create_save",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.save.create",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -554,7 +556,7 @@ messages.createSave = new Request({
|
||||
|
||||
messages.renameSave = new Request({
|
||||
type: "rename_save",
|
||||
links: ["control-master", "master-slave"],
|
||||
links: ["control-controller", "controller-host"],
|
||||
permission: "core.instance.save.rename",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -565,7 +567,7 @@ messages.renameSave = new Request({
|
||||
|
||||
messages.copySave = new Request({
|
||||
type: "copy_save",
|
||||
links: ["control-master", "master-slave"],
|
||||
links: ["control-controller", "controller-host"],
|
||||
permission: "core.instance.save.copy",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -576,7 +578,7 @@ messages.copySave = new Request({
|
||||
|
||||
messages.deleteSave = new Request({
|
||||
type: "delete_save",
|
||||
links: ["control-master", "master-slave"],
|
||||
links: ["control-controller", "controller-host"],
|
||||
permission: "core.instance.save.delete",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -586,7 +588,7 @@ messages.deleteSave = new Request({
|
||||
|
||||
messages.downloadSave = new Request({
|
||||
type: "download_save",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.save.download",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
@@ -599,7 +601,7 @@ messages.downloadSave = new Request({
|
||||
|
||||
messages.transferSave = new Request({
|
||||
type: "transfer_save",
|
||||
links: ["control-master", "master-slave"],
|
||||
links: ["control-controller", "controller-host"],
|
||||
permission: "core.instance.save.transfer",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
@@ -615,7 +617,7 @@ messages.transferSave = new Request({
|
||||
|
||||
messages.pullSave = new Request({
|
||||
type: "pull_save",
|
||||
links: ["master-slave"],
|
||||
links: ["controller-host"],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"stream_id": { type: "string" },
|
||||
@@ -628,7 +630,7 @@ messages.pullSave = new Request({
|
||||
|
||||
messages.pushSave = new Request({
|
||||
type: "push_save",
|
||||
links: ["master-slave"],
|
||||
links: ["controller-host"],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"stream_id": { type: "string" },
|
||||
@@ -638,7 +640,7 @@ messages.pushSave = new Request({
|
||||
|
||||
messages.setSaveListSubscriptions = new Request({
|
||||
type: "set_save_list_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.instance.save.list_subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
@@ -651,7 +653,7 @@ messages.setSaveListSubscriptions = new Request({
|
||||
|
||||
messages.loadScenario = new Request({
|
||||
type: "load_scenario",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.load_scenario",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -664,42 +666,42 @@ messages.loadScenario = new Request({
|
||||
|
||||
messages.exportData = new Request({
|
||||
type: "export_data",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.export_data",
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.extractPlayers = new Request({
|
||||
type: "extract_players",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.extract_players",
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.stopInstance = new Request({
|
||||
type: "stop_instance",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.stop",
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.killInstance = new Request({
|
||||
type: "kill_instance",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.kill",
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.deleteInstance = new Request({
|
||||
type: "delete_instance",
|
||||
links: ["control-master", "master-slave"],
|
||||
links: ["control-controller", "controller-host"],
|
||||
permission: "core.instance.delete",
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.sendRcon = new Request({
|
||||
type: "send_rcon",
|
||||
links: ["control-master", "master-slave", "slave-instance"],
|
||||
links: ["control-controller", "controller-host", "host-instance"],
|
||||
permission: "core.instance.send_rcon",
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
@@ -712,9 +714,9 @@ messages.sendRcon = new Request({
|
||||
|
||||
messages.getModPack = new Request({
|
||||
type: "get_mod_pack",
|
||||
links: ["control-master", "instance-slave", "slave-master"],
|
||||
links: ["control-controller", "instance-host", "host-controller"],
|
||||
permission: "core.mod_pack.get",
|
||||
forwardTo: "master",
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
},
|
||||
@@ -725,9 +727,9 @@ messages.getModPack = new Request({
|
||||
|
||||
messages.getDefaultModPack = new Request({
|
||||
type: "get_default_mod_pack",
|
||||
links: ["control-master", "instance-slave", "slave-master"],
|
||||
links: ["control-controller", "instance-host", "host-controller"],
|
||||
permission: "core.mod_pack.get",
|
||||
forwardTo: "master",
|
||||
forwardTo: "controller",
|
||||
responseProperties: {
|
||||
"mod_pack": libData.ModPack.jsonSchema,
|
||||
},
|
||||
@@ -735,7 +737,7 @@ messages.getDefaultModPack = new Request({
|
||||
|
||||
messages.listModPacks = new Request({
|
||||
type: "list_mod_packs",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod_pack.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
@@ -747,7 +749,7 @@ messages.listModPacks = new Request({
|
||||
|
||||
messages.createModPack = new Request({
|
||||
type: "create_mod_pack",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod_pack.create",
|
||||
requestProperties: {
|
||||
"mod_pack": libData.ModPack.jsonSchema,
|
||||
@@ -756,7 +758,7 @@ messages.createModPack = new Request({
|
||||
|
||||
messages.updateModPack = new Request({
|
||||
type: "update_mod_pack",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod_pack.update",
|
||||
requestProperties: {
|
||||
"mod_pack": libData.ModPack.jsonSchema,
|
||||
@@ -765,7 +767,7 @@ messages.updateModPack = new Request({
|
||||
|
||||
messages.deleteModPack = new Request({
|
||||
type: "delete_mod_pack",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod_pack.delete",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
@@ -774,7 +776,7 @@ messages.deleteModPack = new Request({
|
||||
|
||||
messages.setModPackSubscriptions = new Request({
|
||||
type: "set_mod_pack_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod_pack.subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
@@ -787,7 +789,7 @@ messages.setModPackSubscriptions = new Request({
|
||||
|
||||
messages.getMod = new Request({
|
||||
type: "get_mod",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.get",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -800,7 +802,7 @@ messages.getMod = new Request({
|
||||
|
||||
messages.listMods = new Request({
|
||||
type: "list_mods",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
@@ -812,7 +814,7 @@ messages.listMods = new Request({
|
||||
|
||||
messages.searchMods = new Request({
|
||||
type: "search_mods",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.search",
|
||||
requestRequired: ["query", "factorio_version", "page"],
|
||||
requestProperties: {
|
||||
@@ -848,7 +850,7 @@ messages.searchMods = new Request({
|
||||
|
||||
messages.setModSubscriptions = new Request({
|
||||
type: "set_mod_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
@@ -861,7 +863,7 @@ messages.setModSubscriptions = new Request({
|
||||
|
||||
messages.downloadMod = new Request({
|
||||
type: "download_mod",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.download",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -874,7 +876,7 @@ messages.downloadMod = new Request({
|
||||
|
||||
messages.deleteMod = new Request({
|
||||
type: "delete_mod",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.mod.delete",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -884,7 +886,7 @@ messages.deleteMod = new Request({
|
||||
|
||||
messages.listPermissions = new Request({
|
||||
type: "list_permissions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.permission.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
@@ -904,7 +906,7 @@ messages.listPermissions = new Request({
|
||||
|
||||
messages.listRoles = new Request({
|
||||
type: "list_roles",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.role.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
@@ -925,7 +927,7 @@ messages.listRoles = new Request({
|
||||
|
||||
messages.createRole = new Request({
|
||||
type: "create_role",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.role.create",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -939,7 +941,7 @@ messages.createRole = new Request({
|
||||
|
||||
messages.updateRole = new Request({
|
||||
type: "update_role",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.role.update",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
@@ -951,7 +953,7 @@ messages.updateRole = new Request({
|
||||
|
||||
messages.grantDefaultRolePermissions = new Request({
|
||||
type: "grant_default_role_permissions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.role.update",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
@@ -960,7 +962,7 @@ messages.grantDefaultRolePermissions = new Request({
|
||||
|
||||
messages.deleteRole = new Request({
|
||||
type: "delete_role",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.role.delete",
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
@@ -989,7 +991,7 @@ const userProperties = {
|
||||
|
||||
messages.getUser = new Request({
|
||||
type: "get_user",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.get",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1000,7 +1002,7 @@ messages.getUser = new Request({
|
||||
|
||||
messages.listUsers = new Request({
|
||||
type: "list_users",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.list",
|
||||
responseProperties: {
|
||||
"list": {
|
||||
@@ -1016,7 +1018,7 @@ messages.listUsers = new Request({
|
||||
|
||||
messages.setUserSubscriptions = new Request({
|
||||
type: "set_user_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.subscribe",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
@@ -1029,7 +1031,7 @@ messages.setUserSubscriptions = new Request({
|
||||
|
||||
messages.createUser = new Request({
|
||||
type: "create_user",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.create",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1038,7 +1040,7 @@ messages.createUser = new Request({
|
||||
|
||||
messages.revokeUserToken = new Request({
|
||||
type: "revoke_user_token",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.revoke_token",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1047,7 +1049,7 @@ messages.revokeUserToken = new Request({
|
||||
|
||||
messages.updateUserRoles = new Request({
|
||||
type: "update_user_roles",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.update_roles",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1057,7 +1059,7 @@ messages.updateUserRoles = new Request({
|
||||
|
||||
messages.setUserAdmin = new Request({
|
||||
type: "set_user_admin",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.set_admin",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1068,7 +1070,7 @@ messages.setUserAdmin = new Request({
|
||||
|
||||
messages.setUserWhitelisted = new Request({
|
||||
type: "set_user_whitelisted",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.set_whitelisted",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1079,7 +1081,7 @@ messages.setUserWhitelisted = new Request({
|
||||
|
||||
messages.setUserBanned = new Request({
|
||||
type: "set_user_banned",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.set_banned",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1091,7 +1093,7 @@ messages.setUserBanned = new Request({
|
||||
|
||||
messages.deleteUser = new Request({
|
||||
type: "delete_user",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.user.delete",
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1100,12 +1102,12 @@ messages.deleteUser = new Request({
|
||||
|
||||
messages.setLogSubscriptions = new Request({
|
||||
type: "set_log_subscriptions",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.log.follow",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
"master": { type: "boolean" },
|
||||
"slave_ids": {
|
||||
"controller": { type: "boolean" },
|
||||
"host_ids": {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
@@ -1119,12 +1121,12 @@ messages.setLogSubscriptions = new Request({
|
||||
|
||||
messages.queryLog = new Request({
|
||||
type: "query_log",
|
||||
links: ["control-master"],
|
||||
links: ["control-controller"],
|
||||
permission: "core.log.query",
|
||||
requestProperties: {
|
||||
"all": { type: "boolean" },
|
||||
"master": { type: "boolean" },
|
||||
"slave_ids": {
|
||||
"controller": { type: "boolean" },
|
||||
"host_ids": {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
@@ -1148,7 +1150,7 @@ messages.queryLog = new Request({
|
||||
// Internal requests
|
||||
messages.updateInstances = new Request({
|
||||
type: "update_instances",
|
||||
links: ["slave-master"],
|
||||
links: ["host-controller"],
|
||||
requestProperties: {
|
||||
"instances": {
|
||||
type: "array",
|
||||
@@ -1169,7 +1171,7 @@ messages.updateInstances = new Request({
|
||||
|
||||
messages.assignInstance = new Request({
|
||||
type: "assign_instance",
|
||||
links: ["master-slave"],
|
||||
links: ["controller-host"],
|
||||
requestProperties: {
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
@@ -1178,13 +1180,13 @@ messages.assignInstance = new Request({
|
||||
|
||||
messages.unassignInstance = new Request({
|
||||
type: "unassigne_instance",
|
||||
links: ["master-slave"],
|
||||
links: ["controller-host"],
|
||||
forwardTo: "instance",
|
||||
});
|
||||
|
||||
messages.getMetrics = new Request({
|
||||
type: "get_metrics",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
responseProperties: {
|
||||
"results": { type: "array" },
|
||||
},
|
||||
@@ -1213,14 +1215,14 @@ class Event extends Message {
|
||||
* containing "<source>-<target>" specifications.
|
||||
* @param {?string} forwardTo -
|
||||
* Optional target to forward this request to. Supported values are
|
||||
* 'master' and 'instance'. If set to 'instance' an instance_id
|
||||
* 'controller' and 'instance'. If set to 'instance' an instance_id
|
||||
* property is implicity added to eventProperties.
|
||||
* @param {?string} broadcastTo -
|
||||
* Optional target to broadcast this event to. Only 'instance' is
|
||||
* currently supported. An event will only be broadcast towards
|
||||
* links that are downstream, so sending an event set to instance
|
||||
* broadcast that's sent to a slave will only be broadcast to that
|
||||
* slave's instances.
|
||||
* broadcast that's sent to a host will only be broadcast to that
|
||||
* host's instances.
|
||||
* @param {?Array<string>} eventRequired -
|
||||
* List of properties required to be present in the event payload.
|
||||
* Defaults to all.
|
||||
@@ -1247,7 +1249,7 @@ class Event extends Message {
|
||||
...eventProperties,
|
||||
};
|
||||
|
||||
} else if (forwardTo !== "master" && forwardTo !== null) {
|
||||
} else if (forwardTo !== "controller" && forwardTo !== null) {
|
||||
throw new Error(`Invalid forwardTo value ${forwardTo}`);
|
||||
}
|
||||
|
||||
@@ -1275,7 +1277,7 @@ class Event extends Message {
|
||||
* Set a the handler for the event on the given link if it's a target
|
||||
* for the event.
|
||||
*
|
||||
* If forwardTo was set to 'master' the `forwardEventToMaster` method on
|
||||
* If forwardTo was set to 'controller' the `forwardEventToController` method on
|
||||
* the link is used as the default handler. If it was set to 'instance'
|
||||
* the `forwardEventToInstance` method on the link is used by default.
|
||||
*
|
||||
@@ -1297,15 +1299,15 @@ class Event extends Message {
|
||||
if (!handler) {
|
||||
if (this.forwardTo === "instance") {
|
||||
handler = link.forwardEventToInstance;
|
||||
} else if (this.forwardTo === "master") {
|
||||
handler = link.forwardEventToMaster;
|
||||
} else if (this.forwardTo === "controller") {
|
||||
handler = link.forwardEventToController;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.broadcastTo === "instance"
|
||||
&& [
|
||||
"instance-slave", "slave-master", "control-master", "master-slave",
|
||||
"instance-host", "host-controller", "control-controller", "controller-host",
|
||||
].includes(`${link.target}-${link.source}`)
|
||||
) {
|
||||
if (!handler) {
|
||||
@@ -1352,7 +1354,7 @@ class Event extends Message {
|
||||
|
||||
messages.debugWsMessage = new Event({
|
||||
type: "debug_ws_message",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventProperties: {
|
||||
"direction": { type: "string" },
|
||||
"content": { type: "string" },
|
||||
@@ -1361,7 +1363,7 @@ messages.debugWsMessage = new Event({
|
||||
|
||||
messages.accountUpdate = new Event({
|
||||
type: "account_update",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventRequired: [],
|
||||
eventProperties: {
|
||||
"roles": {
|
||||
@@ -1385,7 +1387,7 @@ messages.accountUpdate = new Event({
|
||||
|
||||
messages.logMessage = new Event({
|
||||
type: "log_message",
|
||||
links: ["slave-master", "master-control"],
|
||||
links: ["host-controller", "controller-control"],
|
||||
eventProperties: {
|
||||
"info": {
|
||||
type: "object",
|
||||
@@ -1398,16 +1400,16 @@ messages.logMessage = new Event({
|
||||
},
|
||||
});
|
||||
|
||||
messages.slaveUpdate = new Event({
|
||||
type: "slave_update",
|
||||
links: ["master-control"],
|
||||
eventProperties: slaveProperties,
|
||||
messages.hostUpdate = new Event({
|
||||
type: "host_update",
|
||||
links: ["controller-control"],
|
||||
eventProperties: hostProperties,
|
||||
});
|
||||
|
||||
|
||||
messages.instanceInitialized = new Event({
|
||||
type: "instance_initialized",
|
||||
links: ["instance-slave"],
|
||||
links: ["instance-host"],
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"plugins": {
|
||||
@@ -1418,7 +1420,7 @@ messages.instanceInitialized = new Event({
|
||||
});
|
||||
messages.instanceStatusChanged = new Event({
|
||||
type: "instance_status_changed",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
links: ["instance-host", "host-controller"],
|
||||
eventRequired: ["instance_id", "status"],
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
@@ -1431,14 +1433,14 @@ messages.instanceStatusChanged = new Event({
|
||||
|
||||
messages.instanceUpdate = new Event({
|
||||
type: "instance_update",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventProperties: instanceProperties,
|
||||
});
|
||||
|
||||
messages.saveListUpdate = new Event({
|
||||
type: "save_list_update",
|
||||
links: ["instance-slave", "slave-master", "master-control"],
|
||||
forwardTo: "master",
|
||||
links: ["instance-host", "host-controller", "controller-control"],
|
||||
forwardTo: "controller",
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"list": saveList,
|
||||
@@ -1447,7 +1449,7 @@ messages.saveListUpdate = new Event({
|
||||
|
||||
messages.modPackUpdate = new Event({
|
||||
type: "mod_pack_update",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventProperties: {
|
||||
"mod_pack": libData.ModPack.jsonSchema,
|
||||
},
|
||||
@@ -1455,7 +1457,7 @@ messages.modPackUpdate = new Event({
|
||||
|
||||
messages.modUpdate = new Event({
|
||||
type: "mod_update",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventProperties: {
|
||||
"mod": libData.ModInfo.jsonSchema,
|
||||
},
|
||||
@@ -1463,14 +1465,14 @@ messages.modUpdate = new Event({
|
||||
|
||||
messages.userUpdate = new Event({
|
||||
type: "user_update",
|
||||
links: ["master-control"],
|
||||
links: ["controller-control"],
|
||||
eventRequired: userRequired,
|
||||
eventProperties: userProperties,
|
||||
});
|
||||
|
||||
messages.masterConnectionEvent = new Event({
|
||||
type: "master_connection_event",
|
||||
links: ["slave-instance"],
|
||||
messages.controllerConnectionEvent = new Event({
|
||||
type: "controller_connection_event",
|
||||
links: ["host-instance"],
|
||||
eventProperties: {
|
||||
"event": { type: "string" },
|
||||
},
|
||||
@@ -1478,7 +1480,7 @@ messages.masterConnectionEvent = new Event({
|
||||
|
||||
messages.syncUserLists = new Event({
|
||||
type: "sync_user_lists",
|
||||
links: ["master-slave"],
|
||||
links: ["controller-host"],
|
||||
eventProperties: {
|
||||
"adminlist": {
|
||||
type: "array",
|
||||
@@ -1501,7 +1503,7 @@ messages.syncUserLists = new Event({
|
||||
|
||||
messages.banlistUpdate = new Event({
|
||||
type: "banlist_update",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"name": { type: "string" },
|
||||
@@ -1512,7 +1514,7 @@ messages.banlistUpdate = new Event({
|
||||
|
||||
messages.adminlistUpdate = new Event({
|
||||
type: "adminlist_update",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
eventProperties: {
|
||||
"name": { type: "string" },
|
||||
"admin": { type: "boolean" },
|
||||
@@ -1522,7 +1524,7 @@ messages.adminlistUpdate = new Event({
|
||||
|
||||
messages.whitelistUpdate = new Event({
|
||||
type: "whitelist_update",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
links: ["controller-host", "host-instance"],
|
||||
eventProperties: {
|
||||
"name": { type: "string" },
|
||||
"whitelisted": { type: "boolean" },
|
||||
@@ -1532,8 +1534,8 @@ messages.whitelistUpdate = new Event({
|
||||
|
||||
messages.playerEvent = new Event({
|
||||
type: "player_event",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
eventRequired: ["instance_id", "type", "name"],
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
|
||||
+13
-13
@@ -63,8 +63,8 @@ class WebConsoleFormat {
|
||||
|
||||
transform(info, options) {
|
||||
let src = " ";
|
||||
if (info.slave_id !== undefined) {
|
||||
src += `s:${info.slave_name} - `;
|
||||
if (info.host_id !== undefined) {
|
||||
src += `s:${info.host_name} - `;
|
||||
}
|
||||
if (info.instance_id !== undefined) {
|
||||
src += `i:${info.instance_name}`;
|
||||
@@ -88,12 +88,12 @@ class WebConsoleFormat {
|
||||
* @property {string} [max_level] -
|
||||
* Maximum log level to include. Higher levels are more verbose.
|
||||
* @property {boolean} [all] -
|
||||
* Include log entries from master, all slaves and all instances.
|
||||
* @property {boolean} [master] -
|
||||
* Include log entries from the master server.
|
||||
* @property {Array<number>} [slave_ids] -
|
||||
* Include log entries for the given slaves and instances of those
|
||||
* slaves by id.
|
||||
* Include log entries from controller, all hosts and all instances.
|
||||
* @property {boolean} [controller] -
|
||||
* Include log entries from the controller.
|
||||
* @property {Array<number>} [host_ids] -
|
||||
* Include log entries for the given hosts and instances of those
|
||||
* hosts by id.
|
||||
* @property {Array<number>} [instance_ids] -
|
||||
* Include log entries for the given instances by id.
|
||||
*/
|
||||
@@ -107,7 +107,7 @@ class WebConsoleFormat {
|
||||
* filter returning true for log entries that match it.
|
||||
* @static
|
||||
*/
|
||||
function logFilter({ all, master, slave_ids, instance_ids, max_level }) {
|
||||
function logFilter({ all, controller, host_ids, instance_ids, max_level }) {
|
||||
return info => {
|
||||
// Note: reversed to filter out undefined levels
|
||||
if (max_level && !(levels[info.level] <= levels[max_level])) {
|
||||
@@ -117,14 +117,14 @@ function logFilter({ all, master, slave_ids, instance_ids, max_level }) {
|
||||
if (all) {
|
||||
return true;
|
||||
}
|
||||
if (master && info.slave_id === undefined) {
|
||||
if (controller && info.host_id === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
slave_ids
|
||||
&& info.slave_id !== undefined
|
||||
host_ids
|
||||
&& info.host_id !== undefined
|
||||
&& info.instance_id === undefined
|
||||
&& slave_ids.includes(info.slave_id)
|
||||
&& host_ids.includes(info.host_id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ class TerminalFormat {
|
||||
if (info.instance_name) {
|
||||
src += `i:${info.instance_name} `;
|
||||
|
||||
} else if (info.slave_name) {
|
||||
src += `s:${info.slave_name} `;
|
||||
} else if (info.host_name) {
|
||||
src += `s:${info.host_name} `;
|
||||
}
|
||||
if (info.plugin) {
|
||||
src += `${info.plugin}: `;
|
||||
@@ -140,6 +140,7 @@ class LinkTransport extends Transport {
|
||||
}
|
||||
|
||||
const logFileGlob = /^[a-z]+-(\d{4}-\d{2}-\d{2})\.log$/;
|
||||
const logIndexVersion = 1;
|
||||
|
||||
/**
|
||||
* Keeps an index over a log directory to speed up queries to it
|
||||
@@ -150,15 +151,15 @@ class LogIndex {
|
||||
this.logDirectory = logDirectory;
|
||||
this.index = new Map();
|
||||
|
||||
if (serialized.version !== 0) {
|
||||
if (serialized.version !== logIndexVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let [file, serializedEntry] of Object.entries(serialized.files)) {
|
||||
this.index.set(file, {
|
||||
levels: new Set(serializedEntry.levels),
|
||||
master: serializedEntry.master,
|
||||
slave_ids: new Set(serializedEntry.slave_ids),
|
||||
controller: serializedEntry.controller,
|
||||
host_ids: new Set(serializedEntry.host_ids),
|
||||
instance_ids: new Set(serializedEntry.instance_ids),
|
||||
});
|
||||
}
|
||||
@@ -166,15 +167,15 @@ class LogIndex {
|
||||
|
||||
serialize() {
|
||||
let serialized = {
|
||||
version: 0,
|
||||
version: logIndexVersion,
|
||||
files: {},
|
||||
};
|
||||
|
||||
for (let [file, entry] of this.index) {
|
||||
serialized.files[file] = {
|
||||
levels: [...entry.levels],
|
||||
master: entry.master,
|
||||
slave_ids: [...entry.slave_ids],
|
||||
controller: entry.controller,
|
||||
host_ids: [...entry.host_ids],
|
||||
instance_ids: [...entry.instance_ids],
|
||||
};
|
||||
}
|
||||
@@ -224,8 +225,8 @@ class LogIndex {
|
||||
fileStream.pipe(lineStream);
|
||||
let entry = {
|
||||
levels: new Set(),
|
||||
master: false,
|
||||
slave_ids: new Set(),
|
||||
controller: false,
|
||||
host_ids: new Set(),
|
||||
instance_ids: new Set(),
|
||||
};
|
||||
for await (let line of lineStream) {
|
||||
@@ -234,16 +235,16 @@ class LogIndex {
|
||||
info = JSON.parse(line);
|
||||
} catch (err) {
|
||||
entry.levels.add("info");
|
||||
entry.master = true;
|
||||
entry.controller = true;
|
||||
continue;
|
||||
}
|
||||
if (info.level) {
|
||||
entry.levels.add(info.level);
|
||||
}
|
||||
if (info.slave_id === undefined) {
|
||||
entry.master = true;
|
||||
if (info.host_id === undefined) {
|
||||
entry.controller = true;
|
||||
} else if (info.instance_id === undefined) {
|
||||
entry.slave_ids.add(info.slave_id);
|
||||
entry.host_ids.add(info.host_id);
|
||||
}
|
||||
if (info.instance_id !== undefined) {
|
||||
entry.instance_ids.add(info.instance_id);
|
||||
@@ -279,7 +280,7 @@ class LogIndex {
|
||||
* @returns {boolean}
|
||||
* true if the file may contain entries included by the filter.
|
||||
*/
|
||||
filterIncludesFile(file, { max_level, all, master, instance_ids, slave_ids }) {
|
||||
filterIncludesFile(file, { max_level, all, controller, instance_ids, host_ids }) {
|
||||
let entry = this.index.get(file);
|
||||
if (!entry) {
|
||||
return true;
|
||||
@@ -298,10 +299,10 @@ class LogIndex {
|
||||
if (all) {
|
||||
return true;
|
||||
}
|
||||
if (master && entry.master) {
|
||||
if (controller && entry.controller) {
|
||||
return true;
|
||||
}
|
||||
if (slave_ids && slave_ids.some(id => entry.slave_ids.has(id))) {
|
||||
if (host_ids && host_ids.some(id => entry.host_ids.has(id))) {
|
||||
return true;
|
||||
}
|
||||
if (instance_ids && instance_ids.some(id => entry.instance_ids.has(id))) {
|
||||
@@ -321,12 +322,12 @@ class LogIndex {
|
||||
* @property {string} [max_level] -
|
||||
* Maximum log level to include. Higher levels are more verbose.
|
||||
* @property {boolean} [all] -
|
||||
* Include log entries from master, all slaves and all instances.
|
||||
* @property {boolean} [master] -
|
||||
* Include log entries from the master server.
|
||||
* @property {Array<number>} [slave_ids] -
|
||||
* Include log entries for the given slaves and instances of those
|
||||
* slaves by id.
|
||||
* Include log entries from controller, all hosts and all instances.
|
||||
* @property {boolean} [controller] -
|
||||
* Include log entries from the controller.
|
||||
* @property {Array<number>} [host_ids] -
|
||||
* Include log entries for the given hosts and instances of those
|
||||
* hosts by id.
|
||||
* @property {Array<number>} [instance_ids] -
|
||||
* Include log entries for the given instances by id.
|
||||
*/
|
||||
@@ -402,62 +403,6 @@ async function queryLog(logDirectory, filter, index) {
|
||||
return log;
|
||||
}
|
||||
|
||||
async function migrateLogs(log, directory, pattern) {
|
||||
const dateRegExp = /^\d{4}-\d{2}-\d{2}/;
|
||||
let lineStream = new libStream.LineSplitter({ readableObjectMode: true });
|
||||
let fileStream = fs.createReadStream(log);
|
||||
fileStream.pipe(lineStream);
|
||||
|
||||
let currentDate = null;
|
||||
let output = null;
|
||||
|
||||
async function switchOutput(newOutputPath) {
|
||||
if (output) {
|
||||
output.end();
|
||||
await finished(output);
|
||||
}
|
||||
|
||||
output = fs.createWriteStream(newOutputPath, { flags: "ax" });
|
||||
try {
|
||||
await events.once(output, "open");
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`writing ${newOutputPath}`);
|
||||
} catch (err) {
|
||||
if (err.code === "EEXIST") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Discarding logs for ${newDate} due to target logfile already existing`);
|
||||
output = null;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (let line of lineStream) {
|
||||
let info = {};
|
||||
try {
|
||||
info = JSON.parse(line);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
if (typeof info.timestamp === "string" && dateRegExp.test(info.timestamp)) {
|
||||
let newDate = info.timestamp.slice(0, 10);
|
||||
// Only update date forward in time.
|
||||
if (newDate !== currentDate && (!currentDate || newDate > currentDate)) {
|
||||
await switchOutput(path.join(directory, pattern.replace("%DATE%", newDate)));
|
||||
currentDate = newDate;
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
if (!output.write(Buffer.concat([line, Buffer.from("\n")]))) {
|
||||
await events.once(output, "drain");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnhandledErrors() {
|
||||
/* eslint-disable node/no-process-exit */
|
||||
process.on("uncaughtException", err => {
|
||||
@@ -478,7 +423,6 @@ module.exports = {
|
||||
LogIndex,
|
||||
handleUnhandledErrors,
|
||||
queryLog,
|
||||
migrateLogs,
|
||||
|
||||
// for testing only
|
||||
_formatServerOutput: formatServerOutput,
|
||||
|
||||
+102
-102
@@ -7,9 +7,9 @@
|
||||
const helpers = require("./helpers");
|
||||
|
||||
/**
|
||||
* Conceptual base for master and instance plugins.
|
||||
* Conceptual base for controller and instance plugins.
|
||||
* @typedef {
|
||||
* module:lib/plugin.BaseMasterPlugin
|
||||
* module:lib/plugin.BaseControllerPlugin
|
||||
* |module:lib/plugin.BaseInstancePlugin
|
||||
* |module:lib/plugin.BaseControlPlugin
|
||||
* } module:lib/plugin.BasePlugin
|
||||
@@ -19,7 +19,7 @@ const helpers = require("./helpers");
|
||||
* Base class for instance plugins
|
||||
*
|
||||
* Instance plugins are subclasses of this class which get instantiated by
|
||||
* the slave when it brings up an instance with the plugin enabled in the
|
||||
* the host when it brings up an instance with the plugin enabled in the
|
||||
* config. To be discovered the class must be exported under the name
|
||||
* `InstancePlugin` in the module specified by the `instanceEntrypoint` in
|
||||
* the plugin's info.js file.
|
||||
@@ -31,7 +31,7 @@ const helpers = require("./helpers");
|
||||
* @static
|
||||
*/
|
||||
class BaseInstancePlugin {
|
||||
constructor(info, instance, slave) {
|
||||
constructor(info, instance, host) {
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
@@ -39,19 +39,19 @@ class BaseInstancePlugin {
|
||||
|
||||
/**
|
||||
* Instance the plugin started for
|
||||
* @type {module:slave/src/Instance}
|
||||
* @type {module:host/src/Instance}
|
||||
*/
|
||||
this.instance = instance;
|
||||
|
||||
/**
|
||||
* Slave running the instance
|
||||
* Host running the instance
|
||||
*
|
||||
* With the exepction of accessing the slave's config you should
|
||||
* avoid ineracting with the slave object directly.
|
||||
* With the exepction of accessing the host's config you should
|
||||
* avoid ineracting with the host object directly.
|
||||
*
|
||||
* @type {module:slave/src/Slave}
|
||||
* @type {module:host/src/Host}
|
||||
*/
|
||||
this.slave = slave;
|
||||
this.host = host;
|
||||
|
||||
/**
|
||||
* Logger for this plugin
|
||||
@@ -150,23 +150,23 @@ class BaseInstancePlugin {
|
||||
async onOutput(parsed, line) { }
|
||||
|
||||
/**
|
||||
* Called when an event on the master connection happens
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the master has been established.
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the slave and the
|
||||
* master. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the master to an absolute minimum.
|
||||
* Invoked when a connection loss is detected between the host and the
|
||||
* controller. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped master connection will get queued up in
|
||||
* memory on the slave and sent all in one go when the connection is
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory on the host and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
@@ -176,29 +176,29 @@ class BaseInstancePlugin {
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the master has been closed. This
|
||||
* typically means the master server has shut down. Plugins should not
|
||||
* send any messages that goes to or via the master server after the
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param {string} event - one of connect, drop, resume and close
|
||||
*/
|
||||
onMasterConnectionEvent(event) { }
|
||||
onControllerConnectionEvent(event) { }
|
||||
|
||||
/**
|
||||
* Called when the master is preparing to disconnect from the slave
|
||||
* Called when the controller is preparing to disconnect from the host
|
||||
*
|
||||
* Invoked when the master server has requested the disconnection from
|
||||
* the slave. This typically happens when it is in the process of
|
||||
* Invoked when the controller has requested the disconnection from
|
||||
* the host. This typically happens when it is in the process of
|
||||
* shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the master, or are forwarded
|
||||
* via the master server after the prepare disconnect has been handled.
|
||||
* Plugins must stop sending messages to the controller, or are forwarded
|
||||
* via the controller after the prepare disconnect has been handled.
|
||||
*
|
||||
* @param {module:master/src/SlaveConnection} connection -
|
||||
* The connection to the slave preparing to disconnect.
|
||||
* @param {module:controller/src/HostConnection} connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareMasterDisconnect(connection) { }
|
||||
async onPrepareControllerDisconnect(connection) { }
|
||||
|
||||
/**
|
||||
* Called when a player joins or leaves the game
|
||||
@@ -284,25 +284,25 @@ class BaseInstancePlugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for master plugins
|
||||
* Base class for controller plugins
|
||||
*
|
||||
* Master plugins are subclasses of this class which get instantiated by
|
||||
* the master server on startup when the plugin is enabled in the config.
|
||||
* To be discovered the class must be exported under the name `MasterPlugin`
|
||||
* in the module specified by the `masterEntrypoint` in the plugin's info.js
|
||||
* Controller plugins are subclasses of this class which get instantiated by
|
||||
* the controller on startup when the plugin is enabled in the config.
|
||||
* To be discovered the class must be exported under the name `ControllerPlugin`
|
||||
* in the module specified by the `controllerEntrypoint` in the plugin's info.js
|
||||
* file.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
class BaseMasterPlugin {
|
||||
constructor(info, master, metrics, logger) {
|
||||
class BaseControllerPlugin {
|
||||
constructor(info, controller, metrics, logger) {
|
||||
this.info = info;
|
||||
|
||||
/**
|
||||
* Master server
|
||||
* @type {module:master/src/Master}
|
||||
* Controller
|
||||
* @type {module:controller/src/Controller}
|
||||
*/
|
||||
this.master = master;
|
||||
this.controller = controller;
|
||||
this.metrics = metrics;
|
||||
|
||||
/**
|
||||
@@ -323,13 +323,13 @@ class BaseMasterPlugin {
|
||||
/**
|
||||
* Called when the status of an instance changes
|
||||
*
|
||||
* Invoked when the master server has changed the status of an instance
|
||||
* or received notice from a slave that the status of an instance has
|
||||
* Invoked when the controller has changed the status of an instance
|
||||
* or received notice from a host that the status of an instance has
|
||||
* changed. The possible statuses that can be notified about are:
|
||||
* - `unassigned:`: Instance is no longer asssigned to a slave.
|
||||
* - `unknown`: Slave assigned to instance is offline.
|
||||
* - `unassigned:`: Instance is no longer asssigned to a host.
|
||||
* - `unknown`: Host assigned to instance is offline.
|
||||
* - `stopped`: Instance is no longer running or was just assigned to a
|
||||
* slave.
|
||||
* host.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
* - `running`: Instance startup completed and is now running.
|
||||
* - `stopping`: Instance is in the processing stopping.
|
||||
@@ -338,21 +338,21 @@ class BaseMasterPlugin {
|
||||
* icons and locale data.
|
||||
* - `deleted`: Instance was deleted.
|
||||
*
|
||||
* On master startup all known instances gets the status `unknown` if
|
||||
* they are assigned to a slave, when the slave then connects to the
|
||||
* master and informs of the current status of its instances this hook
|
||||
* On controller startup all known instances gets the status `unknown` if
|
||||
* they are assigned to a host, when the host then connects to the
|
||||
* controller and informs of the current status of its instances this hook
|
||||
* is invoked for all those instances. You can detect this situation by
|
||||
* checking if prev equals `unknown`. If the slave disconnects the
|
||||
* checking if prev equals `unknown`. If the host disconnects the
|
||||
* instances will again get the `unknown` status.
|
||||
*
|
||||
* When instances are created on the master they will notify of a status
|
||||
* When instances are created on the controller they will notify of a status
|
||||
* change with prev set to null. While the status of new instances is
|
||||
* in most cases `unassigned` it's possible for the created instance to
|
||||
* start with any state in some slave connection corner cases.
|
||||
* start with any state in some host connection corner cases.
|
||||
*
|
||||
* Note that it's possible for status change notification to get lost in
|
||||
* the case of network outages if reconnect fails to re-establish the
|
||||
* session between the master server and the slave.
|
||||
* session between the controller and the host.
|
||||
*
|
||||
* @param {Object} instance - the instance that changed.
|
||||
* @param {?string} prev - the previous status of the instance.
|
||||
@@ -360,17 +360,17 @@ class BaseMasterPlugin {
|
||||
async onInstanceStatusChanged(instance, prev) { }
|
||||
|
||||
/**
|
||||
* Called when the value of a master config field changed.
|
||||
* Called when the value of a controller config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on the master.
|
||||
* changed on the controller.
|
||||
*
|
||||
* @param {module:lib/config.ConfigGroup} group -
|
||||
* The group who's field got changed.
|
||||
* @param {string} field - Name of the field that changed.
|
||||
* @param {*} prev - The previous value of the field.
|
||||
*/
|
||||
async onMasterConfigFieldChanged(group, field, prev) { }
|
||||
async onControllerConfigFieldChanged(group, field, prev) { }
|
||||
|
||||
/**
|
||||
* Called when the value of an instance config field changed.
|
||||
@@ -404,28 +404,28 @@ class BaseMasterPlugin {
|
||||
async onMetrics() { }
|
||||
|
||||
/**
|
||||
* Called when the master server is shutting down
|
||||
* Called when the controller is shutting down
|
||||
*/
|
||||
async onShutdown() { }
|
||||
|
||||
/**
|
||||
* Called when an event on a slave connection happens
|
||||
* Called when an event on a host connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the slave has been established.
|
||||
* Invoked when a new connection to the host has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the master and a
|
||||
* slave. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the given slave connection to an absolute minimum.
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* host. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the given host connection to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped slave connection will get queued up in
|
||||
* memory on the master and sent all in one go when the connection is
|
||||
* Messages sent over a dropped host connection will get queued up in
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
@@ -435,13 +435,13 @@ class BaseMasterPlugin {
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the slave has been closed.
|
||||
* Invoked when the connection to the host has been closed.
|
||||
*
|
||||
* @param {module:master/src/SlaveConnection} connection -
|
||||
* @param {module:controller/src/HostConnection} connection -
|
||||
* The connection the event occured on.
|
||||
* @param {string} event - one of connect, drop, resume and close
|
||||
*/
|
||||
onSlaveConnectionEvent(connection, event) { }
|
||||
onHostConnectionEvent(connection, event) { }
|
||||
|
||||
/**
|
||||
* Called when an avent on a control connection happens
|
||||
@@ -455,13 +455,13 @@ class BaseMasterPlugin {
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the master and a
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* control. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the given control connection to an absolute
|
||||
* minimum.
|
||||
*
|
||||
* Messages sent over a dropped control connection will get queued up in
|
||||
* memory on the master and sent all in one go when the connection is
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
@@ -473,25 +473,25 @@ class BaseMasterPlugin {
|
||||
*
|
||||
* Invoked when the connection to the control has been closed.
|
||||
*
|
||||
* @param {module:master/src/ControlConnection} connection -
|
||||
* @param {module:controller/src/ControlConnection} connection -
|
||||
* The connection the event occured on.
|
||||
* @param {string} event - one of connect, drop, resume, and close.
|
||||
*/
|
||||
onControlConnectionEvent(connection, event) { }
|
||||
|
||||
/**
|
||||
* Called when a slave is preparing to disconnect from the master
|
||||
* Called when a host is preparing to disconnect from the controller
|
||||
*
|
||||
* Invoked when a slave has requested the disconnection from the master.
|
||||
* Invoked when a host has requested the disconnection from the controller.
|
||||
* This typically happens when it is in the process of shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the slave in question after the
|
||||
* Plugins must stop sending messages to the host in question after the
|
||||
* prepare disconnect has been handled.
|
||||
*
|
||||
* @param {module:master/src/SlaveConnection} connection -
|
||||
* The connection to the slave preparing to disconnect.
|
||||
* @param {module:controller/src/HostConnection} connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareSlaveDisconnect(connection) { }
|
||||
async onPrepareHostDisconnect(connection) { }
|
||||
|
||||
/**
|
||||
* Called when a mod pack is updated
|
||||
@@ -507,7 +507,7 @@ class BaseMasterPlugin {
|
||||
async onModPackUpdated(modPack) { }
|
||||
|
||||
/**
|
||||
* Called when a mod stored on the master server is updated
|
||||
* Called when a mod stored on the controller is updated
|
||||
*
|
||||
* Invoked when a mod has been added, updated or deleted from the pool
|
||||
* of shared mods stored on the cluster.
|
||||
@@ -536,22 +536,22 @@ class BaseMasterPlugin {
|
||||
async onPlayerEvent(instance, event) { }
|
||||
|
||||
/**
|
||||
* Broadcast event to all connected slaves
|
||||
* Broadcast event to all connected hosts
|
||||
*
|
||||
* Sends the given event to all slaves connected to the master server.
|
||||
* This does not include slaves that are in the process of closing the
|
||||
* Sends the given event to all hosts connected to the controller.
|
||||
* This does not include hosts that are in the process of closing the
|
||||
* connection, which typically happens when they are shutting down.
|
||||
*
|
||||
* @param {module:lib/link.Event} event - Event to send
|
||||
* @param {Object} data - Data ta pass with the event.
|
||||
*/
|
||||
broadcastEventToSlaves(event, data={}) {
|
||||
for (let slaveConnection of this.master.wsServer.slaveConnections.values()) {
|
||||
broadcastEventToHosts(event, data={}) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (
|
||||
!slaveConnection.connector.closing
|
||||
&& (!event.plugin || slaveConnection.plugins.has(event.plugin))
|
||||
!hostConnection.connector.closing
|
||||
&& (!event.plugin || hostConnection.plugins.has(event.plugin))
|
||||
) {
|
||||
event.send(slaveConnection, data);
|
||||
event.send(hostConnection, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,8 +660,8 @@ class BaseWebPlugin {
|
||||
this.info = info;
|
||||
|
||||
/**
|
||||
* Control link to the master server, not available until the
|
||||
* connect event in onMasterConnectionEvent is signaled.
|
||||
* Control link to the controller, not available until the
|
||||
* connect event in onControllerConnectionEvent is signaled.
|
||||
* @type {?module:lib/link.Link}
|
||||
*/
|
||||
this.control = null;
|
||||
@@ -700,13 +700,13 @@ class BaseWebPlugin {
|
||||
* component extra.
|
||||
*
|
||||
* @type {object}
|
||||
* @property {React.ComponentType} MasterPage -
|
||||
* Placed at the end of the master page.
|
||||
* @property {React.ComponentType} SlavesPage -
|
||||
* Placed at the end of the slaves list page.
|
||||
* @property {React.ComponentType} SlaveViewPage -
|
||||
* Placed at the end of each slave page. Takes a `slave` param
|
||||
* which is the slave the page is displayed for.
|
||||
* @property {React.ComponentType} ControllerPage -
|
||||
* Placed at the end of the controller page.
|
||||
* @property {React.ComponentType} HostsPage -
|
||||
* Placed at the end of the hosts list page.
|
||||
* @property {React.ComponentType} HostViewPage -
|
||||
* Placed at the end of each host page. Takes a `host` param
|
||||
* which is the host the page is displayed for.
|
||||
* @property {React.ComponentType} InstancesPage -
|
||||
* Placed at the end of the instance list page.
|
||||
* @property {React.ComponentType} InstanceViewPage -
|
||||
@@ -732,22 +732,22 @@ class BaseWebPlugin {
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when an event on the master connection happens
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the master has been established.
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the control link
|
||||
* and the master. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the master to an absolute minimum.
|
||||
* and the controller. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped master connection will get queued up in
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory in the browser and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
@@ -758,14 +758,14 @@ class BaseWebPlugin {
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the master has been closed. This
|
||||
* typically means the master server has shut down. Plugins should not
|
||||
* send any messages that goes to or via the master server after the
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param {string} event - one of connect, drop, resume and close
|
||||
*/
|
||||
onMasterConnectionEvent(event) { }
|
||||
onControllerConnectionEvent(event) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -840,7 +840,7 @@ async function invokeHook(plugins, hook, ...args) {
|
||||
|
||||
module.exports = {
|
||||
BaseInstancePlugin,
|
||||
BaseMasterPlugin,
|
||||
BaseControllerPlugin,
|
||||
BaseControlPlugin,
|
||||
BaseWebPlugin,
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load master plugin class of a plugin
|
||||
* Load controller plugin class of a plugin
|
||||
*
|
||||
* @param {Object} pluginInfo -
|
||||
* Plugin info object returned from {@link
|
||||
@@ -74,8 +74,8 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
|
||||
* @returns {Promise<function>} plugin class
|
||||
* @static
|
||||
*/
|
||||
async function loadMasterPluginClass(pluginInfo) {
|
||||
return loadPluginClass("masterEntrypoint", "MasterPlugin", libPlugin.BaseMasterPlugin, pluginInfo);
|
||||
async function loadControllerPluginClass(pluginInfo) {
|
||||
return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +106,7 @@ async function loadControlPluginClass(pluginInfo) {
|
||||
|
||||
module.exports = {
|
||||
loadPluginInfos,
|
||||
loadMasterPluginClass,
|
||||
loadControllerPluginClass,
|
||||
loadInstancePluginClass,
|
||||
loadControlPluginClass,
|
||||
};
|
||||
@@ -168,7 +168,7 @@ const clientHandshake = ajv.compile({
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "register_slave" },
|
||||
"type": { const: "register_host" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["token", "agent", "version", "name", "id", "plugins"],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Implementation of commands shared between master/slave/ctl.
|
||||
* Implementation of commands shared between controller/host/ctl.
|
||||
* @module lib/shared_commands
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
+28
-28
@@ -350,41 +350,41 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.control.connect",
|
||||
title: "Connect to WebSocket",
|
||||
description: "Connect to the master's WebSocket control interface.",
|
||||
description: "Connect to the controller's WebSocket control interface.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
|
||||
definePermission({
|
||||
name: "core.master.get_config",
|
||||
title: "Get master config",
|
||||
description: "Get the config of master.",
|
||||
name: "core.controller.get_config",
|
||||
title: "Get controller config",
|
||||
description: "Get the config of controller.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.master.update_config",
|
||||
title: "Modify master config",
|
||||
description: "Modify the master config or entries of the master config.",
|
||||
name: "core.controller.update_config",
|
||||
title: "Modify controller config",
|
||||
description: "Modify the controller config or entries of the controller config.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.slave.list",
|
||||
title: "List slaves",
|
||||
description: "Get the full list of slaves in the cluster.",
|
||||
name: "core.host.list",
|
||||
title: "List hosts",
|
||||
description: "Get the full list of hosts in the cluster.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
definePermission({
|
||||
name: "core.slave.subscribe",
|
||||
title: "Subscribe to slave updates",
|
||||
description: "Subscribe to be notified on updates on the details of slaves.",
|
||||
name: "core.host.subscribe",
|
||||
title: "Subscribe to host updates",
|
||||
description: "Subscribe to be notified on updates on the details of hosts.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
definePermission({
|
||||
name: "core.slave.generate_token",
|
||||
title: "Generate slave token",
|
||||
description: "Generate tokens for slaves to connect to the cluster with.",
|
||||
name: "core.host.generate_token",
|
||||
title: "Generate host token",
|
||||
description: "Generate tokens for hosts to connect to the cluster with.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.slave.create_config",
|
||||
title: "Create slave config",
|
||||
description: "Create slave configs via the master server.",
|
||||
name: "core.host.create_config",
|
||||
title: "Create host config",
|
||||
description: "Create host configs via the controller.",
|
||||
});
|
||||
|
||||
definePermission({
|
||||
@@ -408,7 +408,7 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.create",
|
||||
title: "Create instance",
|
||||
description: "Create new instances on the master server.",
|
||||
description: "Create new instances on the controller.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.get_config",
|
||||
@@ -423,7 +423,7 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.assign",
|
||||
title: "Assign instance",
|
||||
description: "Assign or reassign instances to a slave.",
|
||||
description: "Assign or reassign instances to a host.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.list",
|
||||
@@ -473,7 +473,7 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.export_data",
|
||||
title: "Export locale and icons",
|
||||
description: "Export the the locale and icons from an instance and upload it to the master.",
|
||||
description: "Export the the locale and icons from an instance and upload it to the controller.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.extract_players",
|
||||
@@ -553,13 +553,13 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.mod.list",
|
||||
title: "List mods",
|
||||
description: "Get the full list of mods stored on the master.",
|
||||
description: "Get the full list of mods stored on the controller.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
definePermission({
|
||||
name: "core.mod.search",
|
||||
title: "Search mods",
|
||||
description: "Search through the list of mods stored on the master.",
|
||||
description: "Search through the list of mods stored on the controller.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
definePermission({
|
||||
@@ -571,18 +571,18 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.mod.upload",
|
||||
title: "Upload mod",
|
||||
description: "Upload mods to the master.",
|
||||
description: "Upload mods to the controller.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.mod.download",
|
||||
title: "Download mod",
|
||||
description: "Download mods stored on the master.",
|
||||
description: "Download mods stored on the controller.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
definePermission({
|
||||
name: "core.mod.delete",
|
||||
title: "Delete mod",
|
||||
description: "Delete mods stored on the master.",
|
||||
description: "Delete mods stored on the controller.",
|
||||
});
|
||||
|
||||
definePermission({
|
||||
@@ -687,7 +687,7 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.debug.dump_ws",
|
||||
title: "Dump WebSocket",
|
||||
description: "Dump all WebSocket communicatation from the master.",
|
||||
description: "Dump all WebSocket communicatation from the controller.",
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -25,18 +25,18 @@ function ErrorCard(props) {
|
||||
|
||||
export default function App(props) {
|
||||
let [connected, setConnected] = useState(false);
|
||||
let [token, setToken] = useState(localStorage.getItem("master_token") || null);
|
||||
let [token, setToken] = useState(localStorage.getItem("controller_token") || null);
|
||||
let connector = props.control.connector;
|
||||
|
||||
function clearToken() {
|
||||
setToken(null);
|
||||
connector.token = null;
|
||||
localStorage.removeItem("master_token");
|
||||
localStorage.removeItem("controller_token");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onConnect() {
|
||||
localStorage.setItem("master_token", token);
|
||||
localStorage.setItem("controller_token", token);
|
||||
setConnected(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import { useSlaveList } from "../model/slave";
|
||||
import { useHostList } from "../model/host";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
|
||||
export default function AssignInstanceModal(props) {
|
||||
let [visible, setVisible] = useState(false);
|
||||
let [slaveList] = useSlaveList();
|
||||
let [hostList] = useHostList();
|
||||
let [applying, setApplying] = useState(false);
|
||||
let [form] = Form.useForm();
|
||||
let control = useContext(ControlContext);
|
||||
@@ -22,20 +22,20 @@ export default function AssignInstanceModal(props) {
|
||||
}
|
||||
|
||||
function handleAssign() {
|
||||
let slaveId = form.getFieldValue("slave");
|
||||
if (slaveId === undefined) {
|
||||
let hostId = form.getFieldValue("host");
|
||||
if (hostId === undefined) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slaveId === "null") {
|
||||
slaveId = null;
|
||||
if (hostId === "null") {
|
||||
hostId = null;
|
||||
}
|
||||
|
||||
setApplying(true);
|
||||
libLink.messages.assignInstanceCommand.send(control, {
|
||||
instance_id: props.id,
|
||||
slave_id: slaveId,
|
||||
host_id: hostId,
|
||||
}).then(() => {
|
||||
setVisible(false);
|
||||
if (props.onFinish) {
|
||||
@@ -66,24 +66,24 @@ export default function AssignInstanceModal(props) {
|
||||
destroyOnClose
|
||||
>
|
||||
<Paragraph style={{ maxWidth: "30em" }}>
|
||||
Select a Slave to assign this instance to. Assignment
|
||||
creates the necessary files on the slave to start up
|
||||
Select a Host to assign this instance to. Assignment
|
||||
creates the necessary files on the host to start up
|
||||
the instance. Note that reassigning an instance from
|
||||
one slave to another will not move the server save over.
|
||||
one host to another will not move the server save over.
|
||||
</Paragraph>
|
||||
<Form form={form} initialValues={{ slave: props.slaveId === null ? "null" : props.slaveId }}>
|
||||
<Form.Item name="slave" label="Slave">
|
||||
<Form form={form} initialValues={{ host: props.hostId === null ? "null" : props.hostId }}>
|
||||
<Form.Item name="host" label="Host">
|
||||
<Select>
|
||||
<Select.Option value={"null"}>
|
||||
<Typography.Text italic>Unassigned</Typography.Text>
|
||||
</Select.Option>
|
||||
{slaveList.map((slave) => <Select.Option
|
||||
key={slave["id"]}
|
||||
value={slave["id"]}
|
||||
disabled={!slave["connected"]}
|
||||
{hostList.map((host) => <Select.Option
|
||||
key={host["id"]}
|
||||
value={host["id"]}
|
||||
disabled={!host["connected"]}
|
||||
>
|
||||
{slave["name"]}
|
||||
{!slave["connected"] && " (offline)"}
|
||||
{host["name"]}
|
||||
{!host["connected"] && " (offline)"}
|
||||
</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
+5
-5
@@ -6,16 +6,16 @@ import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
|
||||
export default function MasterConfigTree(props) {
|
||||
export default function ControllerConfigTree(props) {
|
||||
let control = useContext(ControlContext);
|
||||
|
||||
async function retrieveConfig() {
|
||||
let result = await libLink.messages.getMasterConfig.send(control);
|
||||
let result = await libLink.messages.getControllerConfig.send(control);
|
||||
return result.serialized_config;
|
||||
}
|
||||
|
||||
async function setField(field, value) {
|
||||
await libLink.messages.setMasterConfigField.send(control, {
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
field,
|
||||
value: String(value),
|
||||
});
|
||||
@@ -30,11 +30,11 @@ export default function MasterConfigTree(props) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await libLink.messages.setMasterConfigProp.send(control, data);
|
||||
await libLink.messages.setControllerConfigProp.send(control, data);
|
||||
}
|
||||
|
||||
return <BaseConfigTree
|
||||
ConfigClass={libConfig.MasterConfig}
|
||||
ConfigClass={libConfig.ControllerConfig}
|
||||
retrieveConfig={retrieveConfig}
|
||||
setField={setField}
|
||||
setProp={setProp}
|
||||
+7
-7
@@ -4,22 +4,22 @@ import { Typography } from "antd";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import LogConsole from "./LogConsole";
|
||||
import { useAccount } from "../model/account";
|
||||
import MasterConfigTree from "./MasterConfigTree";
|
||||
import ControllerConfigTree from "./ControllerConfigTree";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
||||
export default function MasterPage() {
|
||||
export default function ControllerPage() {
|
||||
let account = useAccount();
|
||||
|
||||
return <PageLayout nav={[{ name: "Master" }]}>
|
||||
<h2>Master</h2>
|
||||
return <PageLayout nav={[{ name: "Controller" }]}>
|
||||
<h2>Controller</h2>
|
||||
{account.hasPermission("core.log.follow") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
<LogConsole master={true} />
|
||||
<LogConsole controller={true} />
|
||||
</>}
|
||||
{account.hasPermission("core.master.get_config") && <MasterConfigTree />}
|
||||
<PluginExtra component="MasterPage" />
|
||||
{account.hasPermission("core.controller.get_config") && <ControllerConfigTree />}
|
||||
<PluginExtra component="ControllerPage" />
|
||||
</PageLayout>;
|
||||
};
|
||||
+19
-19
@@ -6,61 +6,61 @@ import InstanceList from "./InstanceList";
|
||||
import LogConsole from "./LogConsole";
|
||||
import { useAccount } from "../model/account";
|
||||
import { useInstanceList } from "../model/instance";
|
||||
import { useSlave } from "../model/slave";
|
||||
import { useHost } from "../model/host";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
||||
export default function SlaveViewPage(props) {
|
||||
export default function HostViewPage(props) {
|
||||
let params = useParams();
|
||||
let slaveId = Number(params.id);
|
||||
let hostId = Number(params.id);
|
||||
let account = useAccount();
|
||||
let [instanceList] = useInstanceList();
|
||||
let [slave] = useSlave(slaveId);
|
||||
let [host] = useHost(hostId);
|
||||
|
||||
instanceList = instanceList.filter(instance => instance["assigned_slave"] === slaveId);
|
||||
instanceList = instanceList.filter(instance => instance["assigned_host"] === hostId);
|
||||
|
||||
let nav = [{ name: "Slaves", path: "/slaves" }, { name: slave.name || slaveId }];
|
||||
if (slave.loading) {
|
||||
let nav = [{ name: "Hosts", path: "/hosts" }, { name: host.name || hostId }];
|
||||
if (host.loading) {
|
||||
return <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
|
||||
}
|
||||
|
||||
if (slave.missing) {
|
||||
if (host.missing) {
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title={slaveId}
|
||||
title={hostId}
|
||||
/>
|
||||
<p>Slave with id {slaveId} was not found on the master server.</p>
|
||||
<p>Host with id {hostId} was not found on the controller.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title={slave.name || slaveId}
|
||||
title={host.name || hostId}
|
||||
/>
|
||||
|
||||
<Descriptions bordered size="small" column={{ xs: 1, sm: 2, xl: 4 }}>
|
||||
<Descriptions.Item label="Name">{slave["name"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Name">{host["name"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Connected">
|
||||
<Tag color={slave["connected"] ? "#389e0d" : "#cf1322"}>
|
||||
{slave["connected"] ? "Connected" : "Disconnected"}
|
||||
<Tag color={host["connected"] ? "#389e0d" : "#cf1322"}>
|
||||
{host["connected"] ? "Connected" : "Disconnected"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Agent">{slave["agent"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{slave["version"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Agent">{host["agent"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{host["version"]}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{account.hasPermission("core.instance.list") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
|
||||
<InstanceList instances={instanceList} size="small" hideAssignedSlave />
|
||||
<InstanceList instances={instanceList} size="small" hideAssignedHost />
|
||||
</>}
|
||||
{account.hasPermission("core.log.follow") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
<LogConsole slaves={[slaveId]} />
|
||||
<LogConsole hosts={[hostId]} />
|
||||
</>}
|
||||
<PluginExtra component="SlaveViewPage" slave={slave} />
|
||||
<PluginExtra component="HostViewPage" host={host} />
|
||||
</PageLayout>;
|
||||
}
|
||||
+37
-37
@@ -9,17 +9,17 @@ import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import { useSlaveList } from "../model/slave";
|
||||
import { useHostList } from "../model/host";
|
||||
import notify, { notifyErrorHandler } from "../util/notify";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
|
||||
|
||||
|
||||
function GenerateSlaveTokenButton(props) {
|
||||
function GenerateHostTokenButton(props) {
|
||||
let control = useContext(ControlContext);
|
||||
let [visible, setVisible] = useState(false);
|
||||
let [token, setToken] = useState(null);
|
||||
let [slaveId, setSlaveId] = useState(null);
|
||||
let [hostId, setHostId] = useState(null);
|
||||
let [form] = Form.useForm();
|
||||
let [pluginList, setPluginList] = useState([]);
|
||||
useEffect(() => {
|
||||
@@ -37,18 +37,18 @@ function GenerateSlaveTokenButton(props) {
|
||||
async function generateToken() {
|
||||
let id;
|
||||
let values = form.getFieldsValue();
|
||||
if (values.slaveId) {
|
||||
id = Number.parseInt(values.slaveId, 10);
|
||||
if (values.hostId) {
|
||||
id = Number.parseInt(values.hostId, 10);
|
||||
if (Number.isNaN(id)) {
|
||||
form.setFields([{ name: "slaveId", errors: ["Must be an integer"] }]);
|
||||
form.setFields([{ name: "hostId", errors: ["Must be an integer"] }]);
|
||||
return;
|
||||
}
|
||||
form.setFields([{ name: "slaveId", errors: [] }]);
|
||||
form.setFields([{ name: "hostId", errors: [] }]);
|
||||
}
|
||||
|
||||
let result = await libLink.messages.generateSlaveToken.send(control, { slave_id: id || null });
|
||||
let result = await libLink.messages.generateHostToken.send(control, { host_id: id || null });
|
||||
setToken(result.token);
|
||||
setSlaveId(id);
|
||||
setHostId(id);
|
||||
}
|
||||
|
||||
// Generate a new random
|
||||
@@ -65,7 +65,7 @@ function GenerateSlaveTokenButton(props) {
|
||||
onClick={() => { setVisible(true); }}
|
||||
>Generate Token</Button>
|
||||
<Modal
|
||||
title="Generate Slave Token"
|
||||
title="Generate Host Token"
|
||||
visible={visible}
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
@@ -76,7 +76,7 @@ function GenerateSlaveTokenButton(props) {
|
||||
width="700px"
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark="optional">
|
||||
<Form.Item name="slaveId" label="Slave ID">
|
||||
<Form.Item name="hostId" label="Host ID">
|
||||
<InputNumber onChange={() => {
|
||||
generateToken().catch(notifyErrorHandler("Error generating token"));
|
||||
}} />
|
||||
@@ -84,7 +84,7 @@ function GenerateSlaveTokenButton(props) {
|
||||
</Form>
|
||||
{token !== null && <>
|
||||
<Typography.Paragraph>
|
||||
Slave auth token:
|
||||
Host auth token:
|
||||
</Typography.Paragraph>
|
||||
<div className="codeblock">
|
||||
<CopyButton
|
||||
@@ -94,43 +94,43 @@ function GenerateSlaveTokenButton(props) {
|
||||
{token}
|
||||
</div>
|
||||
<Typography.Paragraph>
|
||||
You can set the token on an existing slave with the following command:
|
||||
You can set the token on an existing host with the following command:
|
||||
</Typography.Paragraph>
|
||||
<div className="codeblock">
|
||||
<CopyButton
|
||||
message={"Copied configuration commands to clipboard"}
|
||||
text={`npx clusterioslave config set slave.master_token ${token}
|
||||
npx clusterioslave config set slave.id ${slaveId}`}
|
||||
text={`npx clusteriohost config set host.controller_token ${token}
|
||||
npx clusteriohost config set host.id ${hostId}`}
|
||||
/>
|
||||
<p>npx clusterioslave config set slave.master_token <token></p>
|
||||
<p>npx clusterioslave config set slave.id <slaveId></p>
|
||||
<p>npx clusteriohost config set host.controller_token <token></p>
|
||||
<p>npx clusteriohost config set host.id <hostId></p>
|
||||
</div>
|
||||
<Typography.Paragraph>
|
||||
Example slave setup commands:
|
||||
Example host setup commands:
|
||||
</Typography.Paragraph>
|
||||
<div className="codeblock">
|
||||
<CopyButton
|
||||
message={"Copied slave setup commands to clipboard"}
|
||||
message={"Copied host setup commands to clipboard"}
|
||||
text={`
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
`+
|
||||
// eslint-disable-next-line max-len
|
||||
`npm init "@clusterio" -- --master-token ${token} --mode "slave" --download-headless --master-url ${document.location.origin}/ --slave-name "Slave ${slaveId || "?"}" --public-address localhost ${pluginString.length ? "--plugins" : ""} ${pluginString}`
|
||||
`npm init "@clusterio" -- --controller-token ${token} --mode "host" --download-headless --controller-url ${document.location.origin}/ --host-name "Host ${hostId || "?"}" --public-address localhost ${pluginString.length ? "--plugins" : ""} ${pluginString}`
|
||||
}/>
|
||||
<p>> mkdir clusterio</p>
|
||||
<p>> cd clusterio</p>
|
||||
<p>
|
||||
> npm init "@clusterio" --
|
||||
--master-token <span className="highlight">{token} </span>
|
||||
--mode "slave"
|
||||
--controller-token <span className="highlight">{token} </span>
|
||||
--mode "host"
|
||||
--download-headless
|
||||
--master-url <span className="highlight">{document.location.origin}/ </span>
|
||||
--slave-name <span className="highlight">"Slave {slaveId || "?"}" </span>
|
||||
--controller-url <span className="highlight">{document.location.origin}/ </span>
|
||||
--host-name <span className="highlight">"Host {hostId || "?"}" </span>
|
||||
--public-address <span className="highlight">localhost </span>
|
||||
{pluginString.length ? "--plugins" : ""} <span className="highlight">{pluginString}</span>
|
||||
</p>
|
||||
<p>> ./run-slave</p>
|
||||
<p>> ./run-host</p>
|
||||
</div>
|
||||
</>}
|
||||
</Modal>
|
||||
@@ -175,16 +175,16 @@ function CopyButton({ text, message }) {
|
||||
}
|
||||
|
||||
|
||||
export default function SlavesPage() {
|
||||
export default function HostsPage() {
|
||||
let account = useAccount();
|
||||
let history = useHistory();
|
||||
let [slaveList] = useSlaveList();
|
||||
let [hostList] = useHostList();
|
||||
|
||||
return <PageLayout nav={[{ name: "Slaves" }]}>
|
||||
return <PageLayout nav={[{ name: "Hosts" }]}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title="Slaves"
|
||||
extra={account.hasPermission("core.slave.generate_token") && <GenerateSlaveTokenButton />}
|
||||
title="Hosts"
|
||||
extra={account.hasPermission("core.host.generate_token") && <GenerateHostTokenButton />}
|
||||
/>
|
||||
<Table
|
||||
columns={[
|
||||
@@ -213,23 +213,23 @@ export default function SlavesPage() {
|
||||
{
|
||||
title: "Connected",
|
||||
key: "connected",
|
||||
render: slave => <Tag
|
||||
color={slave["connected"] ? "#389e0d" : "#cf1322"}
|
||||
render: host => <Tag
|
||||
color={host["connected"] ? "#389e0d" : "#cf1322"}
|
||||
>
|
||||
{slave["connected"] ? "Connected" : "Disconnected"}
|
||||
{host["connected"] ? "Connected" : "Disconnected"}
|
||||
</Tag>,
|
||||
sorter: (a, b) => a["connected"] - b["connected"],
|
||||
},
|
||||
]}
|
||||
dataSource={slaveList}
|
||||
rowKey={slave => slave["id"]}
|
||||
dataSource={hostList}
|
||||
rowKey={host => host["id"]}
|
||||
pagination={false}
|
||||
onRow={(record, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/slaves/${record.id}/view`);
|
||||
history.push(`/hosts/${record.id}/view`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<PluginExtra component="SlavesPage" />
|
||||
<PluginExtra component="HostsPage" />
|
||||
</PageLayout>;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { message, Button, Space, Table } from "antd";
|
||||
import CopyOutlined from "@ant-design/icons/CopyOutlined";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import { useSlaveList } from "../model/slave";
|
||||
import { useHostList } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import StartStopInstanceButton from "./StartStopInstanceButton";
|
||||
|
||||
@@ -13,26 +13,26 @@ const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "ba
|
||||
export default function InstanceList(props) {
|
||||
let account = useAccount();
|
||||
let history = useHistory();
|
||||
let [slaveList] = useSlaveList();
|
||||
let [hostList] = useHostList();
|
||||
|
||||
function slaveName(slaveId) {
|
||||
if (slaveId === null) {
|
||||
function hostName(hostId) {
|
||||
if (hostId === null) {
|
||||
return "";
|
||||
}
|
||||
let slave = slaveList.find(s => s.id === slaveId);
|
||||
if (slave) {
|
||||
return slave.name;
|
||||
let host = hostList.find(s => s.id === hostId);
|
||||
if (host) {
|
||||
return host.name;
|
||||
}
|
||||
return String(slaveId);
|
||||
return String(hostId);
|
||||
}
|
||||
|
||||
function instancePublicAddress(instance) {
|
||||
let slave = slaveList.find(s => s.id === instance.assigned_slave);
|
||||
if (slave && slave.public_address) {
|
||||
let host = hostList.find(s => s.id === instance.assigned_host);
|
||||
if (host && host.public_address) {
|
||||
if (instance.game_port) {
|
||||
return `${slave.public_address}:${instance.game_port}`;
|
||||
return `${host.public_address}:${instance.game_port}`;
|
||||
}
|
||||
return slave.public_address;
|
||||
return host.public_address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -45,10 +45,10 @@ export default function InstanceList(props) {
|
||||
sorter: (a, b) => strcmp(a["name"], b["name"]),
|
||||
},
|
||||
{
|
||||
title: "Assigned Slave",
|
||||
key: "assigned_slave",
|
||||
render: instance => slaveName(instance["assigned_slave"]),
|
||||
sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])),
|
||||
title: "Assigned Host",
|
||||
key: "assigned_host",
|
||||
render: instance => hostName(instance["assigned_host"]),
|
||||
sorter: (a, b) => strcmp(hostName(a["assigned_host"]), hostName(b["assigned_host"])),
|
||||
responsive: ["sm"],
|
||||
},
|
||||
{
|
||||
@@ -69,7 +69,7 @@ export default function InstanceList(props) {
|
||||
/>
|
||||
</> : "";
|
||||
},
|
||||
sorter: (a, b) => strcmp(slavePublicAddress(a["assigned_slave"]), slavePublicAddress(b["assigned_slave"])),
|
||||
sorter: (a, b) => strcmp(hostPublicAddress(a["assigned_host"]), hostPublicAddress(b["assigned_host"])),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
@@ -90,7 +90,7 @@ export default function InstanceList(props) {
|
||||
}] : []),
|
||||
];
|
||||
|
||||
if (props.hideAssignedSlave) {
|
||||
if (props.hideAssignedHost) {
|
||||
columns.splice(1, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import LoadScenarioModal from "./LoadScenarioModal";
|
||||
import SavesList from "./SavesList";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import { useInstance } from "../model/instance";
|
||||
import { useSlave } from "../model/slave";
|
||||
import { useHost } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
|
||||
const { Title } = Typography;
|
||||
@@ -28,20 +28,20 @@ const { Title } = Typography;
|
||||
function InstanceDescription(props) {
|
||||
let account = useAccount();
|
||||
|
||||
const { slave, instance } = props;
|
||||
let assigned = instance["assigned_slave"] !== null;
|
||||
const { host, instance } = props;
|
||||
let assigned = instance["assigned_host"] !== null;
|
||||
return <Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<Descriptions.Item label="Slave">
|
||||
<Descriptions.Item label="Host">
|
||||
{!assigned
|
||||
? <em>Unassigned</em>
|
||||
: slave["name"] || instance["assigned_slave"]
|
||||
: host["name"] || instance["assigned_host"]
|
||||
}
|
||||
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
|
||||
id={instance["id"]}
|
||||
slaveId={instance["assigned_slave"]}
|
||||
hostId={instance["assigned_host"]}
|
||||
buttonProps={{
|
||||
size: "small",
|
||||
style: { float: "Right" },
|
||||
@@ -64,7 +64,7 @@ export default function InstanceViewPage(props) {
|
||||
let account = useAccount();
|
||||
let control = useContext(ControlContext);
|
||||
let [instance] = useInstance(instanceId);
|
||||
let [slave] = useSlave(Number(instance["assigned_slave"]));
|
||||
let [host] = useHost(Number(instance["assigned_host"]));
|
||||
|
||||
let [exportingData, setExportingData] = useState(false);
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function InstanceViewPage(props) {
|
||||
<Alert
|
||||
message={instance["status"] === "deleted" ? "Instance has been deleted" : "Instance not found" }
|
||||
showIcon
|
||||
description={<>Instance with id {instanceId} was not found on the master server.</>}
|
||||
description={<>Instance with id {instanceId} was not found on the controller.</>}
|
||||
type="warning"
|
||||
action={
|
||||
<Button
|
||||
@@ -188,7 +188,7 @@ export default function InstanceViewPage(props) {
|
||||
title={instance["name"]}
|
||||
extra={instanceButtons}
|
||||
/>
|
||||
<InstanceDescription slave={slave} instance={instance} />
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
|
||||
{
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
|
||||
|
||||
@@ -28,7 +28,7 @@ function CreateInstanceButton(props) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("control");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", values.instanceName);
|
||||
let serialized_config = instanceConfig.serialize("master");
|
||||
let serialized_config = instanceConfig.serialize("controller");
|
||||
await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
setVisible(false);
|
||||
history.push(`/instances/${instanceConfig.get("instance.id")}/view`);
|
||||
|
||||
@@ -59,8 +59,8 @@ export default function LogConsole(props) {
|
||||
useEffect(() => {
|
||||
let logFilter = {
|
||||
all: props.all || false,
|
||||
master: props.master || false,
|
||||
slave_ids: props.slaves || [],
|
||||
controller: props.controller || false,
|
||||
host_ids: props.hosts || [],
|
||||
instance_ids: props.instances || [],
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function LogConsole(props) {
|
||||
return () => {
|
||||
control.offLog(logFilter, logHandler);
|
||||
};
|
||||
}, [props.all, props.master, (props.slaves || []).join(), (props.instances || []).join()]);
|
||||
}, [props.all, props.controller, (props.hosts || []).join(), (props.instances || []).join()]);
|
||||
|
||||
return <>
|
||||
<Paragraph code className="instance-console">
|
||||
|
||||
@@ -10,7 +10,7 @@ const { Paragraph } = Typography;
|
||||
|
||||
function TokenAuth(props) {
|
||||
return <>
|
||||
<Paragraph>If you have generated an authentication token from the master server, use that here.</Paragraph>
|
||||
<Paragraph>If you have generated an authentication token from the controller, use that here.</Paragraph>
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={(values) => { props.setToken(values.token.trim()); }}
|
||||
|
||||
@@ -13,9 +13,10 @@ export default function ModDetails(props) {
|
||||
|
||||
return <Descriptions className="borderless" bordered size="small" column={{ xs: 1, sm: 1, md: 1, lg: 2 }}>
|
||||
{mod.error && <Descriptions.Item label={<><ExclamationCircleOutlined/> Error</>} span={2}>
|
||||
{mod.error === "missing" && "The given version of this mod is missing from the master storage."}
|
||||
{mod.error === "missing" && "The given version of this mod is missing from the controller storage."}
|
||||
{mod.error === "bad_checksum"
|
||||
&& "Checksum of the mod in the master storage does not match the checksum provided by this mod pack."
|
||||
&& "Checksum of the mod in the controller storage does not match the checksum provided " +
|
||||
"by this mod pack."
|
||||
}
|
||||
</Descriptions.Item>}
|
||||
{props.actions && !screens.lg
|
||||
|
||||
@@ -670,7 +670,7 @@ export default function ModPackViewPage() {
|
||||
className="site-page-header"
|
||||
title="Mod Pack Not Found"
|
||||
/>
|
||||
<p>Mod pack with id {modPackId} was not found on the master server.</p>
|
||||
<p>Mod pack with id {modPackId} was not found on the controller.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function PluginViewPage() {
|
||||
if (!pluginMeta) {
|
||||
return <PageLayout nav={nav}>
|
||||
<h2>Plugin not found</h2>
|
||||
<p>Plugin with name {pluginName} was not found on the master server.</p>
|
||||
<p>Plugin with name {pluginName} was not found on the controller.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ export default function PluginViewPage() {
|
||||
message="Version missmatch detected"
|
||||
description={
|
||||
`The version of the web interface module for this plugin (${plugin.package.version}) `+
|
||||
`does not match the version running on the master server (${pluginMeta.version}), `+
|
||||
`does not match the version running on the controller (${pluginMeta.version}), `+
|
||||
"spurious errors may occur. This usually happens when the plugin is updated but the "+
|
||||
"master server has not been restarted yet, but may also be due to an outdated build."
|
||||
"controller has not been restarted yet, but may also be due to an outdated build."
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function RoleViewPage() {
|
||||
className="site-page-header"
|
||||
title="Role not found"
|
||||
/>
|
||||
<p>Role with id {roleId} was not found on the master server.</p>
|
||||
<p>Role with id {roleId} was not found on the controller.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function SavesList(props) {
|
||||
let [starting, setStarting] = useState(false);
|
||||
let [uploadingFiles, setUploadingFiles] = useState([]);
|
||||
|
||||
let slaveOffline = ["unassigned", "unknown"].includes(props.instance.status);
|
||||
let hostOffline = ["unassigned", "unknown"].includes(props.instance.status);
|
||||
const saveTable = <Table
|
||||
size="small"
|
||||
columns={[
|
||||
@@ -237,13 +237,13 @@ export default function SavesList(props) {
|
||||
}}
|
||||
>Load save</Button>}
|
||||
{account.hasPermission("core.instance.save.rename") && <RenameModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
disabled={hostOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.copy") && <CopyModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
disabled={hostOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.download") && <Button
|
||||
disabled={slaveOffline}
|
||||
disabled={hostOffline}
|
||||
onClick={() => {
|
||||
libLink.messages.downloadSave.send(
|
||||
control, { instance_id: props.instance.id, save: save.name }
|
||||
@@ -257,7 +257,7 @@ export default function SavesList(props) {
|
||||
}}
|
||||
>Download</Button>}
|
||||
{account.hasPermission("core.instance.save.transfer") && <TransferModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
disabled={hostOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.delete") && <Popconfirm
|
||||
title="Permanently delete save?"
|
||||
@@ -270,7 +270,7 @@ export default function SavesList(props) {
|
||||
).catch(notifyErrorHandler("Error deleting save"));
|
||||
}}
|
||||
>
|
||||
<Button danger disabled={slaveOffline}>Delete</Button>
|
||||
<Button danger disabled={hostOffline}>Delete</Button>
|
||||
</Popconfirm>}
|
||||
</Space>,
|
||||
}}
|
||||
@@ -309,7 +309,7 @@ export default function SavesList(props) {
|
||||
}
|
||||
|
||||
let uploadProps = {
|
||||
disabled: slaveOffline,
|
||||
disabled: hostOffline,
|
||||
name: "file",
|
||||
accept: ".zip",
|
||||
headers: {
|
||||
@@ -326,7 +326,7 @@ export default function SavesList(props) {
|
||||
return <div>
|
||||
<SectionHeader title="Saves" extra={<Space>
|
||||
{account.hasPermission("core.instance.save.upload") && <Upload {...uploadProps} >
|
||||
<Button disabled={slaveOffline}>Upload save</Button>
|
||||
<Button disabled={hostOffline}>Upload save</Button>
|
||||
</Upload>}
|
||||
{account.hasPermission("core.instance.save.create") && <CreateSaveModal instance={props.instance} />}
|
||||
</Space>} />
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function UserViewPage() {
|
||||
className="site-page-header"
|
||||
title="User not found"
|
||||
/>
|
||||
<p>User with name {userName} was not found on the master server.</p>
|
||||
<p>User with name {userName} was not found on the controller.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export { default as bootstrap } from "./bootstrap.jsx";
|
||||
export { default as notify, notifyErrorHandler } from "./util/notify.jsx";
|
||||
|
||||
export * from "./model/instance.jsx";
|
||||
export * from "./model/slave.jsx";
|
||||
export * from "./model/host.jsx";
|
||||
export * from "./model/locale.jsx";
|
||||
export * from "./model/item_metadata.jsx";
|
||||
export * from "./model/account.jsx";
|
||||
@@ -12,7 +12,7 @@ export { default as ControlContext } from "./components/ControlContext";
|
||||
|
||||
export { default as BaseConfigTree } from "./components/BaseConfigTree";
|
||||
export { default as InstanceConfigTree } from "./components/InstanceConfigTree";
|
||||
export { default as MasterConfigTree } from "./components/MasterConfigTree";
|
||||
export { default as ControllerConfigTree } from "./components/ControllerConfigTree";
|
||||
|
||||
export { default as LogConsole } from "./components/LogConsole";
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
|
||||
export function useHost(id) {
|
||||
let control = useContext(ControlContext);
|
||||
let [host, setHost] = useState({ loading: true });
|
||||
|
||||
function updateHost() {
|
||||
// XXX optimize by requesting only the host in question
|
||||
libLink.messages.listHosts.send(control).then(result => {
|
||||
let match = result.list.find(i => i.id === id);
|
||||
if (!match) {
|
||||
setHost({ missing: true });
|
||||
} else {
|
||||
setHost({ ...match, present: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!Number.isInteger(id)) {
|
||||
setHost({ missing: true });
|
||||
return undefined;
|
||||
}
|
||||
updateHost();
|
||||
|
||||
function updateHandler(newHost) {
|
||||
setHost({ ...newHost, present: true });
|
||||
}
|
||||
|
||||
control.onHostUpdate(id, updateHandler);
|
||||
return () => {
|
||||
control.offHostUpdate(id, updateHandler);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
return [host, updateHost];
|
||||
}
|
||||
|
||||
export function useHostList() {
|
||||
let control = useContext(ControlContext);
|
||||
let [hostList, setHostList] = useState([]);
|
||||
|
||||
function updateHostList() {
|
||||
libLink.messages.listHosts.send(control).then(result => {
|
||||
setHostList(result.list);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateHostList();
|
||||
|
||||
function updateHandler(newHost) {
|
||||
setHostList(oldList => {
|
||||
let newList = oldList.concat();
|
||||
let index = newList.findIndex(s => s.id === newHost.id);
|
||||
if (index !== -1) {
|
||||
newList[index] = newHost;
|
||||
} else {
|
||||
newList.push(newHost);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
|
||||
control.onHostUpdate(null, updateHandler);
|
||||
return () => {
|
||||
control.offHostUpdate(null, updateHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return [hostList];
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
|
||||
export function useSlave(id) {
|
||||
let control = useContext(ControlContext);
|
||||
let [slave, setSlave] = useState({ loading: true });
|
||||
|
||||
function updateSlave() {
|
||||
// XXX optimize by requesting only the slave in question
|
||||
libLink.messages.listSlaves.send(control).then(result => {
|
||||
let match = result.list.find(i => i.id === id);
|
||||
if (!match) {
|
||||
setSlave({ missing: true });
|
||||
} else {
|
||||
setSlave({ ...match, present: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!Number.isInteger(id)) {
|
||||
setSlave({ missing: true });
|
||||
return undefined;
|
||||
}
|
||||
updateSlave();
|
||||
|
||||
function updateHandler(newSlave) {
|
||||
setSlave({ ...newSlave, present: true });
|
||||
}
|
||||
|
||||
control.onSlaveUpdate(id, updateHandler);
|
||||
return () => {
|
||||
control.offSlaveUpdate(id, updateHandler);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
return [slave, updateSlave];
|
||||
}
|
||||
|
||||
export function useSlaveList() {
|
||||
let control = useContext(ControlContext);
|
||||
let [slaveList, setSlaveList] = useState([]);
|
||||
|
||||
function updateSlaveList() {
|
||||
libLink.messages.listSlaves.send(control).then(result => {
|
||||
setSlaveList(result.list);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateSlaveList();
|
||||
|
||||
function updateHandler(newSlave) {
|
||||
setSlaveList(oldList => {
|
||||
let newList = oldList.concat();
|
||||
let index = newList.findIndex(s => s.id === newSlave.id);
|
||||
if (index !== -1) {
|
||||
newList[index] = newSlave;
|
||||
} else {
|
||||
newList.push(newSlave);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
|
||||
control.onSlaveUpdate(null, updateHandler);
|
||||
return () => {
|
||||
control.offSlaveUpdate(null, updateHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return [slaveList];
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import MasterPage from "./components/MasterPage";
|
||||
import SlavesPage from "./components/SlavesPage";
|
||||
import SlaveViewPage from "./components/SlaveViewPage";
|
||||
import ControllerPage from "./components/ControllerPage";
|
||||
import HostsPage from "./components/HostsPage";
|
||||
import HostViewPage from "./components/HostViewPage";
|
||||
import InstancesPage from "./components/InstancesPage";
|
||||
import InstanceViewPage from "./components/InstanceViewPage";
|
||||
import ModPackViewPage from "./components/ModPackViewPage";
|
||||
@@ -17,20 +17,20 @@ import PluginViewPage from "./components/PluginViewPage";
|
||||
|
||||
export const pages = [
|
||||
{
|
||||
path: "/master",
|
||||
sidebarName: "Master",
|
||||
content: <MasterPage />,
|
||||
path: "/controller",
|
||||
sidebarName: "Controller",
|
||||
content: <ControllerPage />,
|
||||
},
|
||||
{
|
||||
path: "/slaves",
|
||||
sidebarName: "Slaves",
|
||||
permission: "core.slave.list",
|
||||
content: <SlavesPage />,
|
||||
path: "/hosts",
|
||||
sidebarName: "Hosts",
|
||||
permission: "core.host.list",
|
||||
content: <HostsPage />,
|
||||
},
|
||||
{
|
||||
path: "/slaves/:id/view",
|
||||
sidebarPath: "/slaves",
|
||||
content: <SlaveViewPage />,
|
||||
path: "/hosts/:id/view",
|
||||
sidebarPath: "/hosts",
|
||||
content: <HostViewPage />,
|
||||
},
|
||||
{
|
||||
path: "/instances",
|
||||
|
||||
@@ -4,7 +4,7 @@ import packageJson from "../../package.json";
|
||||
|
||||
|
||||
/**
|
||||
* Connector for control connection to master server
|
||||
* Connector for control connection to controller
|
||||
* @private
|
||||
*/
|
||||
export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
@@ -30,12 +30,12 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
/**
|
||||
* Handles running the control
|
||||
*
|
||||
* Connects to the master server over WebSocket and sends commands to it.
|
||||
* Connects to the controller over WebSocket and sends commands to it.
|
||||
* @static
|
||||
*/
|
||||
export class Control extends libLink.Link {
|
||||
constructor(connector, plugins) {
|
||||
super("control", "master", connector);
|
||||
super("control", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
/**
|
||||
@@ -67,7 +67,7 @@ export class Control extends libLink.Link {
|
||||
this.accountRoles = null;
|
||||
|
||||
this.accountUpdateHandlers = [];
|
||||
this.slaveUpdateHandlers = new Map();
|
||||
this.hostUpdateHandlers = new Map();
|
||||
this.instanceUpdateHandlers = new Map();
|
||||
this.saveListUpdateHandlers = new Map();
|
||||
this.modPackUpdateHandlers = new Map();
|
||||
@@ -79,8 +79,8 @@ export class Control extends libLink.Link {
|
||||
this.accountName = data.account.name;
|
||||
this.accountRoles = data.account.roles;
|
||||
this.emitAccountUpdate();
|
||||
this.updateSlaveSubscriptions().catch(err => logger.error(
|
||||
`Unexpected error updating slave subscriptions:\n${err.stack}`
|
||||
this.updateHostSubscriptions().catch(err => logger.error(
|
||||
`Unexpected error updating host subscriptions:\n${err.stack}`
|
||||
));
|
||||
this.updateInstanceSubscriptions().catch(err => logger.error(
|
||||
`Unexpected error updating instance subscriptions:\n${err.stack}`
|
||||
@@ -111,7 +111,7 @@ export class Control extends libLink.Link {
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
for (let plugin of this.plugins.values()) {
|
||||
plugin.onMasterConnectionEvent(event);
|
||||
plugin.onControllerConnectionEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -143,60 +143,60 @@ export class Control extends libLink.Link {
|
||||
this.accountUpdateHandlers.splice(index, 1);
|
||||
}
|
||||
|
||||
async slaveUpdateEventHandler(message) {
|
||||
async hostUpdateEventHandler(message) {
|
||||
let handlers = [].concat(
|
||||
this.slaveUpdateHandlers.get(null) || [],
|
||||
this.slaveUpdateHandlers.get(message.data.id) || [],
|
||||
this.hostUpdateHandlers.get(null) || [],
|
||||
this.hostUpdateHandlers.get(message.data.id) || [],
|
||||
);
|
||||
for (let handler of handlers) {
|
||||
handler(message.data);
|
||||
}
|
||||
}
|
||||
|
||||
async onSlaveUpdate(id, handler) {
|
||||
async onHostUpdate(id, handler) {
|
||||
if (id !== null && !Number.isInteger(id)) {
|
||||
throw new Error("Invalid slave id");
|
||||
throw new Error("Invalid host id");
|
||||
}
|
||||
|
||||
let handlers = this.slaveUpdateHandlers.get(id);
|
||||
let handlers = this.hostUpdateHandlers.get(id);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
this.slaveUpdateHandlers.set(id, handlers);
|
||||
this.hostUpdateHandlers.set(id, handlers);
|
||||
}
|
||||
|
||||
handlers.push(handler);
|
||||
|
||||
if (handlers.length === 1) {
|
||||
await this.updateSlaveSubscriptions();
|
||||
await this.updateHostSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
async offSlaveUpdate(id, handler) {
|
||||
let handlers = this.slaveUpdateHandlers.get(id);
|
||||
async offHostUpdate(id, handler) {
|
||||
let handlers = this.hostUpdateHandlers.get(id);
|
||||
if (!handlers || !handlers.length) {
|
||||
throw new Error(`No handlers for slave ${id} exists`);
|
||||
throw new Error(`No handlers for host ${id} exists`);
|
||||
}
|
||||
|
||||
let index = handlers.lastIndexOf(handler);
|
||||
if (index === -1) {
|
||||
throw new Error(`Given handler is not registered for slave ${id}`);
|
||||
throw new Error(`Given handler is not registered for host ${id}`);
|
||||
}
|
||||
|
||||
handlers.splice(index, 1);
|
||||
if (!handlers.length) {
|
||||
this.slaveUpdateHandlers.delete(id);
|
||||
await this.updateSlaveSubscriptions();
|
||||
this.hostUpdateHandlers.delete(id);
|
||||
await this.updateHostSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
async updateSlaveSubscriptions() {
|
||||
async updateHostSubscriptions() {
|
||||
if (!this.connector.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setSlaveSubscriptions.send(this, {
|
||||
all: this.slaveUpdateHandlers.has(null),
|
||||
slave_ids: [...this.slaveUpdateHandlers.keys()].filter(e => e !== null),
|
||||
await libLink.messages.setHostSubscriptions.send(this, {
|
||||
all: this.hostUpdateHandlers.has(null),
|
||||
host_ids: [...this.hostUpdateHandlers.keys()].filter(e => e !== null),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -535,17 +535,17 @@ export class Control extends libLink.Link {
|
||||
|
||||
let combinedFilter = {
|
||||
all: false,
|
||||
master: false,
|
||||
slave_ids: [],
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
};
|
||||
|
||||
for (let filter of this.logHandlers.keys()) {
|
||||
if (filter.all) { combinedFilter.all = true; }
|
||||
if (filter.master) { combinedFilter.master = true; }
|
||||
for (let slaveId of filter.slave_ids || []) {
|
||||
if (!combinedFilter.slave_ids.includes(slaveId)) {
|
||||
combinedFilter.slave_ids.push(slaveId);
|
||||
if (filter.controller) { combinedFilter.controller = true; }
|
||||
for (let hostId of filter.host_ids || []) {
|
||||
if (!combinedFilter.host_ids.includes(hostId)) {
|
||||
combinedFilter.host_ids.push(hostId);
|
||||
}
|
||||
}
|
||||
for (let instanceId of filter.instance_ids || []) {
|
||||
|
||||
@@ -8,6 +8,6 @@ Plugin sharing the in-game chat between instances.
|
||||
Run the following commands in the folder Clusterio is installed to:
|
||||
|
||||
npm install @clusterio/plugin-global_chat
|
||||
npx clusteriomaster plugin add @clusterio/plugin-global_chat
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-global_chat
|
||||
|
||||
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
|
||||
Substitute clusteriocontroller with clusteriohost or clusterioctl if this a dedicate host or ctl installation respectively.
|
||||
@@ -11,8 +11,8 @@ module.exports = {
|
||||
messages: {
|
||||
chat: new libLink.Event({
|
||||
type: "global_chat:chat",
|
||||
links: ["instance-slave", "slave-master", "master-slave", "slave-instance", "control-master"],
|
||||
forwardTo: "master",
|
||||
links: ["instance-host", "host-controller", "controller-host", "host-instance", "control-controller"],
|
||||
forwardTo: "controller",
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"instance_name": { type: "string" },
|
||||
|
||||
@@ -21,7 +21,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
onMasterConnectionEvent(event) {
|
||||
onControllerConnectionEvent(event) {
|
||||
if (event === "connect") {
|
||||
for (let message of this.messageQueue) {
|
||||
this.sendChat(message);
|
||||
@@ -45,7 +45,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
|
||||
async onOutput(output) {
|
||||
if (output.type === "action" && output.action === "CHAT") {
|
||||
if (this.slave.connector.connected) {
|
||||
if (this.host.connector.connected) {
|
||||
this.sendChat(output.message);
|
||||
} else {
|
||||
this.messageQueue.push(output.message);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("global_chat plugin", function() {
|
||||
let instancePlugin;
|
||||
|
||||
before(async function() {
|
||||
instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockSlave());
|
||||
instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockHost());
|
||||
await instancePlugin.init();
|
||||
});
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ When a server crashes while you are online there won't be time to immediately up
|
||||
Run the following commands in the folder Clusterio is installed to:
|
||||
|
||||
npm install @clusterio/plugin-inventory_sync
|
||||
npx clusteriomaster plugin add @clusterio/plugin-inventory_sync
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-inventory_sync
|
||||
|
||||
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicated slave or ctl installation respectively.
|
||||
Substitute clusteriocontroller with clusteriohost or clusterioctl if this a dedicated host or ctl installation respectively.
|
||||
|
||||
## Method of operation
|
||||
|
||||
@@ -23,17 +23,17 @@ This plugin does event based synchronization of inventories.
|
||||
The greatly simplified data flow is as follows:
|
||||
|
||||
1. Player joins server.
|
||||
2. Scenario script asks master for exclusive access to the player.
|
||||
3. Master grants exclusive access for the player to the instance.
|
||||
2. Scenario script asks controller for exclusive access to the player.
|
||||
3. Controller grants exclusive access for the player to the instance.
|
||||
4. Scenario script checks the access response and then acts according to the situation:
|
||||
|
||||
1. If the player has no inventory on the master then the current inventory becomes the synced inventory and the process is done.
|
||||
2. If the player inventory was previously uploaded and is the same as the one stored on the master then the current inventory becomes the synced inventory and the process is done.
|
||||
1. If the player has no inventory on the controller then the current inventory becomes the synced inventory and the process is done.
|
||||
2. If the player inventory was previously uploaded and is the same as the one stored on the controller then the current inventory becomes the synced inventory and the process is done.
|
||||
3. If the player inventory was previously uploaded but is not the same then the player inventory is deleted and the player is turned into a spectator.
|
||||
4. Otherwise the player inventory is kept.
|
||||
|
||||
5. Scenario script asks for the player inventory from the master.
|
||||
6. Master sends it to the scenario in a stream of chunks.
|
||||
5. Scenario script asks for the player inventory from the controller.
|
||||
6. Controller sends it to the scenario in a stream of chunks.
|
||||
7. Scenario displays and updates a progress bar as each chunk is received.
|
||||
8. Once all chunks have loaded the player's synced inventory is recreated from the data and the player can start playing.
|
||||
|
||||
@@ -41,5 +41,5 @@ When the player leaves the inventory is uploaded if it's a synced inventory and
|
||||
Should an error occur during this process the player is given the option to use a temporary inventory instead, which will be merged back into the synced inventory the next time the sync succeeds on that instance.
|
||||
|
||||
Communication between the server and instance goes over stdout or rcon, depending on the size of the data.
|
||||
Communication between the instance and the master server goes over websockets.
|
||||
Communication between the instance and the controller goes over websockets.
|
||||
Overall, we are able to achieve a latency between 3 and infinite ticks from server join, depending on the size of the inventory. The major limiter is rcon transfer speeds with larger inventories, especially if they contain blueprints.
|
||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user