Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1035389c9e | ||
|
|
8c62213a52 | ||
|
|
e7248bfd98 | ||
|
|
8c0e883b23 | ||
|
|
e3d8aa1a9b | ||
|
|
9aebeeac02 | ||
|
|
049ccee94e | ||
|
|
c59267d5ee | ||
|
|
c9ae964eee | ||
|
|
89cf89670f | ||
|
|
3b659116db | ||
|
|
ddd1bde1b4 | ||
|
|
715ab343bf | ||
|
|
7430212ac8 | ||
|
|
33efb32aa9 | ||
|
|
679eec05bc | ||
|
|
b019d44d0b | ||
|
|
efc71580ea | ||
|
|
90dacd98b2 | ||
|
|
f9a1e28074 | ||
|
|
de3f5da9d4 | ||
|
|
abe2acd0dc | ||
|
|
de48d807f1 | ||
|
|
22032ae4f8 | ||
|
|
3e617a81cf | ||
|
|
9005b6cef9 | ||
|
|
f4635a79f9 | ||
|
|
8a0c1e8a4b | ||
|
|
e3b662717b | ||
|
|
cbfc0adf3d | ||
|
|
4a29c13acb | ||
|
|
60e434e4ac | ||
|
|
d126e7f25a | ||
|
|
ecbce16c99 | ||
|
|
0576308187 | ||
|
|
f2c9cd0c62 | ||
|
|
f72995ec8b | ||
|
|
2ffb848fe6 | ||
|
|
e63f1246da | ||
|
|
dbe623c9a7 | ||
|
|
dbe01c6c23 | ||
|
|
29917fb85a | ||
|
|
1a1b4fb0d2 | ||
|
|
4027a4f310 | ||
|
|
37052e1f68 | ||
|
|
98f801e244 | ||
|
|
f9ef2ebbe1 | ||
|
|
a939e11641 | ||
|
|
a4a667ef06 | ||
|
|
87218e4803 | ||
|
|
40301f59b7 | ||
|
|
5db9cad7e5 | ||
|
|
e4bdf996f7 | ||
|
|
c1d0404f23 | ||
|
|
6bb2c50e5c | ||
|
|
8a85cfc8da | ||
|
|
86bd07666f | ||
|
|
a578719fc5 | ||
|
|
72e0b6a3b1 | ||
|
|
90a400b782 | ||
|
|
80eae94fe1 | ||
|
|
5daa492f68 | ||
|
|
b43a181bf4 | ||
|
|
a31ba31567 | ||
|
|
b03b201c20 | ||
|
|
af995428bc | ||
|
|
d79e30f9be | ||
|
|
f5228e6c2e | ||
|
|
ea77e09058 | ||
|
|
5ab4b95e3c | ||
|
|
8dc1415404 | ||
|
|
122bab303d | ||
|
|
160051fc3c | ||
|
|
500baffb2b | ||
|
|
4c77f5ac31 |
No files matched your search
@@ -26,7 +26,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["packages/web_ui/src/**/*.jsx", "plugins/*/web/**/*.jsx"],
|
||||
"files": ["packages/web_ui/src/**/*.jsx", "packages/lib/browser.js", "plugins/*/web/**/*.jsx"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
name: clusterio_docker_image_testing
|
||||
path: .
|
||||
- name: ls
|
||||
run: ls && ls clusterio_docker_image_testing && ls /home/runner/work/factorioClusterio && ls /home/runner/work/factorioClusterio/factorioClusterio
|
||||
run: ls && ls clusterio_docker_image_testing && ls /home/runner/work/clusterio && ls /home/runner/work/clusterio/clusterio
|
||||
- name: Load docker image
|
||||
run: docker load -i clusterio_docker_image_testing
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ before_install:
|
||||
script:
|
||||
- npx lerna bootstrap --hoist --no-ci
|
||||
- node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --output-dir temp/test/sharedMods
|
||||
- curl -o temp/test/sharedMods/subspace_storage_1.99.8.zip -L https://github.com/clusterio/factorioClusterioMod/releases/download/1.99.6/subspace_storage_1.99.8.zip
|
||||
- curl -o temp/test/sharedMods/subspace_storage_1.99.8.zip -L https://github.com/clusterio/subspace_storage/releases/download/1.99.6/subspace_storage_1.99.8.zip
|
||||
- npx lerna run build
|
||||
- npm run-script ci-cover
|
||||
- codecov -f coverage/lcov.info
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
- Added an overview of the installed plugins to the web interface.
|
||||
- Added clusterioctl command to list plugins on the master server.
|
||||
- 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
|
||||
|
||||
@@ -186,8 +187,8 @@
|
||||
- Fixed SIGINT being sent twice to Factorio server when interrupted by CTRL+C on Linux ([#217][#217]).
|
||||
- Fixed package.json incorrectly reporting the license as ISC.
|
||||
|
||||
[#217]: https://github.com/clusterio/factorioClusterio/issues/217
|
||||
[#229]: https://github.com/clusterio/factorioClusterio/issues/229
|
||||
[#217]: https://github.com/clusterio/clusterio/issues/217
|
||||
[#229]: https://github.com/clusterio/clusterio/issues/229
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
FROM node:12 as subspace_storage_builder
|
||||
RUN apt update && apt install -y git
|
||||
WORKDIR /
|
||||
RUN git clone https://github.com/clusterio/factorioClusterioMod.git
|
||||
WORKDIR /factorioClusterioMod
|
||||
RUN git checkout clusterio-2.0 \
|
||||
&& npm install \
|
||||
RUN git clone https://github.com/clusterio/subspace_storage.git
|
||||
WORKDIR /subspace_storage
|
||||
RUN npm install \
|
||||
&& node build
|
||||
|
||||
FROM node:12 as clusterio_builder
|
||||
@@ -24,7 +23,7 @@ RUN npm install \
|
||||
#RUN npm install @clusterio/plugin-subspace_storage
|
||||
#RUN npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
COPY --from=subspace_storage_builder /factorioClusterioMod/dist/ /clusterio/sharedMods/
|
||||
COPY --from=subspace_storage_builder /subspace_storage/dist/ /clusterio/sharedMods/
|
||||
|
||||
# Build submodules (web UI, libraries, plugins etc)
|
||||
RUN npx lerna run build
|
||||
|
||||
@@ -4,20 +4,16 @@
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# factorioClusterio
|
||||
# Clusterio
|
||||
|
||||
Discord for development/support/play: https://discord.gg/5XuDkje
|
||||
|
||||
## Important notice
|
||||
|
||||
This is the development branch for factorioClusterio 2.0 which is currently undergoing heavy restructuring and refactoring.
|
||||
Expect plugins and existing installations to frequently break when using this branch.
|
||||
If you don't want to be an alpha tester for 2.0 please use the stable [1.2.x branch][1.2.x] or [latest stable release](https://github.com/clusterio/factorioClusterio/releases/latest).
|
||||
Clusterio 2.0 is still in alpha, however the previous stable has been abandoned and is no longer supported.
|
||||
Despite being alpha it's reasonably stable now and there's no major breakages expected before a stable release of 2.0.
|
||||
If you are starting a new cluster it's highly recommended to use the 2.0 alpha.
|
||||
|
||||
Installation instructions below are for the unstable master branch.
|
||||
Go to the page for the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
|
||||
|
||||
[1.2.x]: https://github.com/clusterio/factorioClusterio/tree/1.2.x
|
||||
|
||||
### Ways to support me/the project:
|
||||
|
||||
@@ -32,17 +28,17 @@ Go to the page for the [1.2.x branch][1.2.x] for instructions on how to install
|
||||
* [Introduction](#introduction)
|
||||
* [Features](#features)
|
||||
* [Plugins](#plugins)
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
* [Windows setup](#windows-setup)
|
||||
* [MacOS setup](#macos-setup)
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Installation](#installation)
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
* [Windows setup](#windows-setup)
|
||||
* [MacOS setup](#macos-setup)
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Configure Master Server](#configure-master-server)
|
||||
* [Hosting Behind Proxy](#hosting-behind-proxy)
|
||||
* [Setting up an admin account](#setting-up-an-admin-account)
|
||||
* [Managing Factorio mods](#managing-factorio-mods)
|
||||
* [Setting up shared storage](#setting-up-shared-storage)
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Master Server](#master-server)
|
||||
* [Slaves](#slaves)
|
||||
* [Instances](#instances)
|
||||
* [Setting up remote slaves](#setting-up-remote-slaves)
|
||||
* [Setting up clusterioctl](#setting-up-clusterioctl)
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
|
||||
@@ -50,7 +46,7 @@ Go to the page for the [1.2.x branch][1.2.x] for instructions on how to install
|
||||
|
||||
Clusterio is a clustered Factorio server manager that provides the tooling for implementing cross server interactions in Factorio.
|
||||
It was previously best known for implementing cross server transfer and cloud storage of items via teleporter chests.
|
||||
But this functionality has been pulled out of Clusterio into its own plugin for Clusterio named [Subspace Storage](https://github.com/clusterio/factorioClusterioMod).
|
||||
But this functionality has been pulled out of Clusterio into its own plugin for Clusterio named [Subspace Storage](https://github.com/clusterio/subspace_storage).
|
||||
|
||||
By itself Clusterio doesn't change the gameplay in any way, you could even use Clusterio to manage completely vanilla Factorio servers.
|
||||
Plugins do the work of modding in the visible changes into the game, see the [Plugins section](#plugins) for ready made plugins you can install into a Clusterio cluster.
|
||||
@@ -76,8 +72,9 @@ 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.
|
||||
- [Subspace Storage](https://github.com/clusterio/factorioClusterioMod): Provide shared storage that can transport items between servers via teleport chests.
|
||||
- [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.
|
||||
|
||||
There's also plugins developed and maintained by the community:
|
||||
|
||||
@@ -90,26 +87,41 @@ Want to make your own plugin?
|
||||
Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for where to start.
|
||||
|
||||
|
||||
## Ubuntu setup
|
||||
## Installation
|
||||
|
||||
**Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
|
||||
Clusterio runs on Node.js v12 and up, it's distributed via npm and comes with a guided install script.
|
||||
If you alread have a recent Node.js installed you can set it up in a new directory with.
|
||||
|
||||
Clusterio runs on Node.js v12 and up.
|
||||
Node.js itself is not supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu.
|
||||
npm init "@clusterio"
|
||||
|
||||
Master and all slaves:
|
||||
Otherwise see below for OS specific instructions.
|
||||
|
||||
wget -qO - https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init -y
|
||||
npm install @clusterio/master @clusterio/slave @clusterio/ctl
|
||||
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
|
||||
downloads and installs nodejs, git and clusterio.
|
||||
To specify a version, change "latest" in the link to a version number like 0.14.21.
|
||||
### Ubuntu setup
|
||||
|
||||
1. Install Node.js v12 or higher.
|
||||
For 20.04 LTS or below the version of Node.js provided by the Ubuntu repos are too old and you will have to use the nodesource PPA, otherwise you may skip the first line.
|
||||
|
||||
wget -qO - https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
sudo apt install nodejs
|
||||
|
||||
2. Create a new directory and run the Clusterio installer:
|
||||
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init "@clusterio"
|
||||
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
|
||||
3. If you chose to use local factorio directory for the Factorio installation then download the headless build of Factorio and unpack it:
|
||||
|
||||
wget -O factorio.tar.xz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.xz
|
||||
|
||||
To specify a version of Factorio to download replace "latest" in the URL with a version number like "1.0.0".
|
||||
|
||||
4. Optionally copy the generated systemd service files in `systemd` folder to `/etc/systemd/system/`.
|
||||
|
||||
|
||||
**Ubuntu with Docker**
|
||||
|
||||
@@ -119,80 +131,50 @@ If you need it open an issue about it.
|
||||
<!--
|
||||
Clusterio has *very* limited support for using docker.
|
||||
|
||||
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
|
||||
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 slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -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
|
||||
|
||||
The -v flag is used to specify the instance directory.
|
||||
Your instances (save files etc) will be stored there.
|
||||
-->
|
||||
|
||||
## Windows setup
|
||||
### Windows setup
|
||||
|
||||
**Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
|
||||
1. Download and install the latest LTS release from http://nodejs.org.
|
||||
|
||||
**Requirements**
|
||||
2. Create a new empty directory for the installation and navigate into it.
|
||||
Open a PowerShell window in this new directory by Shift+Right clicking inside it and choosing "Open PowerShell window here" and then run the following command.
|
||||
|
||||
download and install nodeJS 12 from http://nodejs.org.
|
||||
Clusterio runs on Node.js v12 and up.
|
||||
npm init "@clusterio"
|
||||
|
||||
**Master**
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
|
||||
1. Open PowerShell or Command prompt in the directory you want to install to and run the following commands.
|
||||
3. If you chose to use local factorio directory for the Factorio installation then download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder.
|
||||
|
||||
|
||||
### MacOS Setup
|
||||
|
||||
1. Install the latest Node.js LTS release from http://nodejs.org or use brew (`brew install node`).
|
||||
|
||||
2. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
|
||||
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init -y
|
||||
npm install @clusterio/master @clusterio/slave @clusterio/ctl
|
||||
npm init "@clusterio"
|
||||
|
||||
2. Obtain Factorio by either of these two methods:
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
|
||||
- Via the stand alone version on from their website
|
||||
3. If you chose to use local factorio directory for the Factorio installation you will need to obtain and copy a mac version of Factorio and unpack it to to the `factorio` folder in your clusterio installation folder.
|
||||
|
||||
1. Create a new folder named "factorio" in the the factorioClusterio folder.
|
||||
|
||||
2. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download .
|
||||
### Installing Plugins
|
||||
|
||||
3. Open the zip file and drag the folder called "Factorio_x.y.z" into the factorio folder created in step 1.
|
||||
|
||||
- Via steam installation
|
||||
|
||||
1. Create a new folder named "factorio" in the the factorioClusterio folder.
|
||||
|
||||
2. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files.
|
||||
|
||||
3. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorio folder created in step 1.
|
||||
|
||||
## MacOS Setup
|
||||
|
||||
**Requirements**
|
||||
|
||||
Download and install nodeJS 12 from http://nodejs.org or brew (`brew install node`).
|
||||
Clusterio runs on Node.js v12 and up.
|
||||
|
||||
1. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
|
||||
|
||||
```bash
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init -y
|
||||
npm install @clusterio/master @clusterio/slave @clusterio/ctl
|
||||
```
|
||||
|
||||
2. Obtain Factorio:
|
||||
|
||||
Install Factorio and copy the factorio.app/Contents in your clusterio install folder as factorio.
|
||||
If you installed factorio on your mac you can run
|
||||
```bash
|
||||
cp -r /Applications/factorio.app/Contents/ factorio
|
||||
```
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
Installing plugins to make them work with Clusterio consists of two steps.
|
||||
First install the package via npm, for example
|
||||
For well known plugins you can select them during installation and no further steps are necessary to make them work.
|
||||
Installing plugins that are not offered by the installer consists of two steps.
|
||||
First install the package the plugin is provided by via npm, for example
|
||||
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
|
||||
@@ -202,7 +184,7 @@ Then tell clusterio that this plugin exists by adding it as a plugin.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -214,6 +196,11 @@ You can change the port used with the command
|
||||
|
||||
npx clusteriomaster config set master.http_port 1234
|
||||
|
||||
When changing the port you will also need to change the address slaves connect with.
|
||||
For the standalone installation mode you can use
|
||||
|
||||
npx clusterioslave config set slave.master_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
|
||||
|
||||
@@ -226,67 +213,63 @@ You can list the config of the master server with the `npx clusteriomaster confi
|
||||
See the [readme for @clusterio/master](/packages/master/README.md) for more information.
|
||||
|
||||
|
||||
### Setting up an admin account
|
||||
## Managing Factorio mods
|
||||
|
||||
Before you can manage the cluster you need to bootstrap an admin account for it.
|
||||
Replace `<username>` with your Factorio username (do not make up a new username here).
|
||||
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.
|
||||
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.
|
||||
|
||||
npx clusteriomaster bootstrap create-admin <username>
|
||||
npx clusteriomaster bootstrap create-ctl-config <username>
|
||||
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.
|
||||
|
||||
The first command creates a user account with the given name and promotes it to a cluster admin.
|
||||
The second one sets up a `config-control.json` config for clusterioctl to connect to the master server under the given user account.
|
||||
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.
|
||||
|
||||
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 following the installation and master configuration instructions you can use the following commands to run 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||
#### 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
|
||||
|
||||
|
||||
### Master Server
|
||||
## Setting up remote slaves
|
||||
|
||||
It's necessary to run the master server in order for anything to work.
|
||||
Once you've completed the setup run the following command to start it up:
|
||||
|
||||
npx clusteriomaster run
|
||||
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 athentication 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.
|
||||
|
||||
|
||||
### Slaves
|
||||
## Setting up clusterioctl
|
||||
|
||||
Slaves connect to the master server and are managed remotely from the master server.
|
||||
In order for slaves to connect to the master server they need a valid authentication token, you can create a slave config with a valid token with the following command.
|
||||
There's a command line interface available for Clusterio which is installed separately with the same installer as for the master and slave.
|
||||
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.
|
||||
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 clusterioctl slave create-config --name Local --generate-token
|
||||
|
||||
This will write a new `config-slave.json` file in the current directory (you can change the location with the `--output` option) with the name, token and url to connect to the master server with.
|
||||
If you are making the config for a remote slave you will need to have set the `master.external_address` option to the URL the master server can be reached on.
|
||||
|
||||
You can list the config of a slave on the slave itself with the `npx clusterioslave config list` command.
|
||||
See the [readme for @clusterio/slave](/packages/slave/README.md) for more information.
|
||||
|
||||
Once the config is set up run the slave with
|
||||
|
||||
npx clusterioslave run
|
||||
|
||||
|
||||
### Instances
|
||||
|
||||
Instances are created, managed and started from the master server.
|
||||
For now the only interface available is the `clusterioctl` command line tool included in Clusterio.
|
||||
You can run this tool from any slave, or the master server without having to set up a config, if you want to manage the cluster from somewhere else you will need to set the `control.master_url` and `control.master_token` options with the `node clusterioctl control-config set` command:
|
||||
|
||||
The basic operations to start a new instance is the following
|
||||
|
||||
npx clusterioctl instance create "My instance"
|
||||
npx clusterioctl instance assign "My instance" "Local"
|
||||
npx clusterioctl instance start "My instance"
|
||||
|
||||
The first line creates the instance configuration on the master server.
|
||||
The second assigns the instance to a slave which creates the instance directory and files needed to run the instance on the given slave.
|
||||
The third line starts the instance, which creates a new save if there are no save games present.
|
||||
|
||||
There are many more commands available with clusterioctl.
|
||||
See the [Managing a Cluster](/docs/managing-a-cluster.md) document or `npx clusterioctl --help` for a full list of them.
|
||||
npx clusteriomaster bootstrap generate-user-token <username>
|
||||
|
||||
|
||||
## Common problems
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000px" height="840px" viewBox="-0.5 -0.5 1000 840" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="550" width="320" height="250" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 547px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="547" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="780" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 300 750 L 300 730 L 440 730 L 440 140 L 376.37 140" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 371.12 140 L 378.12 136.5 L 376.37 140 L 378.12 143.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 723px; margin-left: 346px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="346" y="726" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="520" y="490" width="440" height="310" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 438px; height: 1px; padding-top: 487px; margin-left: 522px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="522" y="487" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><rect x="40" y="210" width="380" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 378px; height: 1px; padding-top: 207px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connecting</div></div></div></foreignObject><text x="42" y="207" fill="#000000" font-family="Helvetica" font-size="12px">connecting</text></switch></g><path d="M 120 150 L 120 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 228.88 L 116.5 221.88 L 120 223.63 L 123.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; coLine truncated
|
||||
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="919px" height="620px" viewBox="-0.5 -0.5 919 620" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="260" width="320" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 257px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="257" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="700" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 280 390 L 280 370 L 380 370 L 380 130 L 346.37 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 341.12 130 L 348.12 126.5 L 346.37 130 L 348.12 133.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 360px; margin-left: 291px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="291" y="363" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="420" y="190" width="160" height="160" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 158px; height: 1px; padding-top: 187px; margin-left: 422px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="422" y="187" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><path d="M 120 140 L 120 273.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 278.88 L 116.5 271.88 L 120 273.63 L 123.5 271.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 194px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">ready()</div></div></div></foreignObject><text x="151" y="197" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ready()</text></switch></g><rect x="40" y="80" width="320" height="80" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 77px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #00Line truncated
|
||||
|
After Width: | Height: | Size: 45 KiB |
@@ -1,20 +0,0 @@
|
||||
digraph "Client Connection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
start [label="Start"];
|
||||
wait_hello [label="Wait for hello"];
|
||||
new_session [label="New session"];
|
||||
wait_ready [label="Wait for ready"];
|
||||
connected [label="Connected"];
|
||||
abort [label="Abort"];
|
||||
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile connecting" style="dotted"];
|
||||
|
||||
|
||||
lost_connection -> start [label="Wait random delay"];
|
||||
start -> wait_hello [label="Connect to /api/socket"];
|
||||
wait_hello -> new_session [label="Receive hello"];
|
||||
new_session -> wait_ready [label="Send register_*"];
|
||||
wait_ready -> connected [label="Receive ready"];
|
||||
wait_ready -> abort [label="Connection closed with 4003"];
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<mxfile host="Electron" modified="2021-08-30T18:05:06.237Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.9.6 Chrome/89.0.4389.128 Electron/12.0.16 Safari/537.36" etag="uzcLt7Oh4qtDsMY9WSdT" compressed="false" version="14.9.6" type="device">
|
||||
<diagram id="GSbHU_PhJe-l3NQpc0X3" name="Connection Flow">
|
||||
<mxGraphModel dx="710" dy="734" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="760" pageHeight="630" background="#EEF3FA" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="GTJXd52-XhRZD3rAr2nm-0" />
|
||||
<mxCell id="GTJXd52-XhRZD3rAr2nm-1" parent="GTJXd52-XhRZD3rAr2nm-0" />
|
||||
<mxCell id="2r3fOvGVNr9_K5ZBoRis-18" value="Client" style="text;html=1;align=left;verticalAlign=middle;resizable=1;points=[];autosize=0;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="440" y="80" width="145" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-0">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="320.02" />
|
||||
<mxPoint x="260" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-5" value="receive<br>resume" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-1">
|
||||
<mxGeometry x="0.086" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="-6" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-2">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="320.02" />
|
||||
<mxPoint x="180" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-26" value="receive<br>register" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-3">
|
||||
<mxGeometry x="-0.2268" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-13">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="40" y="285.02" />
|
||||
<mxPoint x="40" y="580.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-27" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-14">
|
||||
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="28" y="17" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2r3fOvGVNr9_K5ZBoRis-28" value="Wait for handshake" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="160" y="270.02000000000004" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HutjLCx0ygpoBEJLSh69-0" value="Server" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="40" y="80" width="50" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_PUvpjEMIo1z1dIbIUzw-0" value="<p><span>Connection Flow</span><br></p>" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=0;fontSize=20;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="40" y="30" width="230" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fGsjKHM69yN9R0Hf5M05-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="fGsjKHM69yN9R0Hf5M05-0" target="IKjVovVhkWe_WipwIJL3-7">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="220" y="180" />
|
||||
<mxPoint x="180" y="180" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="fGsjKHM69yN9R0Hf5M05-0" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="119.98" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-9">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="300" y="300.02" />
|
||||
<mxPoint x="300" y="300.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-6" value="bad session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingLeft=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-4">
|
||||
<mxGeometry x="-0.429" y="-1" relative="1" as="geometry">
|
||||
<mxPoint x="29" y="-4" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-19">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-29" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-20">
|
||||
<mxGeometry x="-0.1944" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="18" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-0" value="Validate resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-18">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-28" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-21">
|
||||
<mxGeometry x="-0.2778" relative="1" as="geometry">
|
||||
<mxPoint x="20" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-13">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="60" y="355.02" />
|
||||
<mxPoint x="60" y="560.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-30" value="bad token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-24">
|
||||
<mxGeometry x="-0.8093" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="29" y="23" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-2" value="Validate register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-7" target="2r3fOvGVNr9_K5ZBoRis-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="260.02" />
|
||||
<mxPoint x="180" y="260.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-7" value="Send hello" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="199.96" width="120" height="30.02" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-9" target="2r3fOvGVNr9_K5ZBoRis-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="260.02" />
|
||||
<mxPoint x="260" y="260.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-9" value="Send invalidate" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="199.96" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-13" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="550.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-15" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="480.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-18" target="IKjVovVhkWe_WipwIJL3-15">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="470" />
|
||||
<mxPoint x="180" y="470" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-18" value="Send ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-19" target="IKjVovVhkWe_WipwIJL3-15">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="470" />
|
||||
<mxPoint x="260" y="470" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-19" value="Send continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-44">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="240.02" />
|
||||
<mxPoint x="630" y="240.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-32" value="previous<br>session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-31">
|
||||
<mxGeometry x="0.086" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="-2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-49">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="250.02" />
|
||||
<mxPoint x="550" y="250.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-34" value="no<br>session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-33">
|
||||
<mxGeometry x="-0.2268" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="5" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="440" y="215.02" />
|
||||
<mxPoint x="440" y="570.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-36" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-35">
|
||||
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="35" y="-23" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-37" value="Wait for hello" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="199.96000000000006" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-60" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-39" target="IKjVovVhkWe_WipwIJL3-37">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-39" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="119.98" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-65" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-44" target="IKjVovVhkWe_WipwIJL3-61">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="360.02" />
|
||||
<mxPoint x="660" y="360.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-44" value="Send resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="600" y="270.0199999999999" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-68" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-49" target="IKjVovVhkWe_WipwIJL3-67">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-49" value="Send register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="460" y="270.0199999999999" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-54" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="550.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-55" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="480.0199999999999" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-62">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="390.02" />
|
||||
<mxPoint x="630" y="390.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-73" value="receive<br>invalidate" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-64">
|
||||
<mxGeometry x="0.2133" relative="1" as="geometry">
|
||||
<mxPoint x="-30" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="700" y="570.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-74" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;rotation=90;spacingTop=7;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-70">
|
||||
<mxGeometry x="-0.7644" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-12" y="29" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-71" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-55">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="450.02" />
|
||||
<mxPoint x="630" y="450.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-75" value="receive<br>continue" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];spacingRight=4;labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-71">
|
||||
<mxGeometry x="-0.3167" y="-1" relative="1" as="geometry">
|
||||
<mxPoint x="-19" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-61" value="Wait for continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="600" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-62" target="IKjVovVhkWe_WipwIJL3-49">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="320.02" />
|
||||
<mxPoint x="550" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-62" value="Invalidate session" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="470" y="560.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-77" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rotation=90;labelBackgroundColor=none;spacingBottom=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-69">
|
||||
<mxGeometry x="-0.8148" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="12" y="33" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-72" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-55">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="470.02" />
|
||||
<mxPoint x="550" y="470.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-76" value="receive<br>ready" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-72">
|
||||
<mxGeometry x="-0.2389" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-22" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-67" value="Wait for ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="460" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -1,99 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Client Connection Flow Pages: 1 -->
|
||||
<svg width="371pt" height="496pt"
|
||||
viewBox="0.00 0.00 371.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
|
||||
<title>Client Connection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 367,-492 367,4 -4,4"/>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<polygon fill="none" stroke="black" points="135,-384 81,-384 81,-348 135,-348 135,-384"/>
|
||||
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Start</text>
|
||||
</g>
|
||||
<!-- wait_hello -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_hello</title>
|
||||
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
|
||||
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
|
||||
</g>
|
||||
<!-- start->wait_hello -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>start->wait_hello</title>
|
||||
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
|
||||
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
|
||||
</g>
|
||||
<!-- new_session -->
|
||||
<g id="node3" class="node">
|
||||
<title>new_session</title>
|
||||
<polygon fill="none" stroke="black" points="161,-210 55,-210 55,-174 161,-174 161,-210"/>
|
||||
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">New session</text>
|
||||
</g>
|
||||
<!-- wait_hello->new_session -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_hello->new_session</title>
|
||||
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
|
||||
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
|
||||
</g>
|
||||
<!-- wait_ready -->
|
||||
<g id="node4" class="node">
|
||||
<title>wait_ready</title>
|
||||
<polygon fill="none" stroke="black" points="167.5,-123 48.5,-123 48.5,-87 167.5,-87 167.5,-123"/>
|
||||
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for ready</text>
|
||||
</g>
|
||||
<!-- new_session->wait_ready -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>new_session->wait_ready</title>
|
||||
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
|
||||
<text text-anchor="middle" x="164" y="-144.8" font-family="Times,serif" font-size="14.00">Send register_*</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node5" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
|
||||
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- wait_ready->connected -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>wait_ready->connected</title>
|
||||
<path fill="none" stroke="black" d="M64.78,-86.86C57.71,-82.13 51.29,-76.25 47,-69 43.04,-62.32 41.73,-54.22 41.69,-46.49"/>
|
||||
<polygon fill="black" stroke="black" points="45.19,-46.55 42.31,-36.36 38.21,-46.13 45.19,-46.55"/>
|
||||
<text text-anchor="middle" x="97.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
|
||||
</g>
|
||||
<!-- abort -->
|
||||
<g id="node6" class="node">
|
||||
<title>abort</title>
|
||||
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
|
||||
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
|
||||
</g>
|
||||
<!-- wait_ready->abort -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>wait_ready->abort</title>
|
||||
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
|
||||
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
|
||||
<text text-anchor="middle" x="259.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
|
||||
</g>
|
||||
<!-- lost_connection -->
|
||||
<g id="node7" class="node">
|
||||
<title>lost_connection</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
|
||||
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
|
||||
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
|
||||
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while connecting</text>
|
||||
</g>
|
||||
<!-- lost_connection->start -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>lost_connection->start</title>
|
||||
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
|
||||
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="760px" height="630px" viewBox="-0.5 -0.5 760 630" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="440" y="80" width="145" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 90px; margin-left: 442px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap; ">Client</div></div></div></foreignObject><text x="442" y="94" fill="#000000" font-family="Helvetica" font-size="12px" font-weight="bold">Client</text></switch></g><path d="M 260 300.02 L 260 320 L 260 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 260 338.9 L 256.5 331.9 L 260 333.65 L 263.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 316px; margin-left: 238px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />resume</div></div></div></foreignObject><text x="238" y="319" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 180 300.02 L 180 320 L 180 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 338.9 L 176.5 331.9 L 180 333.65 L 183.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 318px; margin-left: 158px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />register</div></div></div></foreignObject><text x="158" y="322" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 160 285 L 40 285 L 40 580 L 153.63 580" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 158.88 580 L 151.88 583.5 L 153.63 580 L 151.88 576.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 300px; margin-left: 91px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">bad handshake<br />timeout</div></div></div></foreignObject><text x="91" y="304" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">bad handshake...</text></switch></g><rect x="160" y="270.02" width="120" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 285px; margin-left: 161px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "Line truncated
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="440px" viewBox="-0.5 -0.5 800 440" style="background-color: rgb(238, 243, 250);"><defs/><g><path d="M 170 365.02 L 170 385 L 290 385 L 290 371.39" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 290 366.14 L 293.5 373.14 L 290 371.39 L 286.5 373.14 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 110 350 L 40 350 L 40 130 L 103.63 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 108.88 130 L 101.88 133.5 L 103.63 130 L 101.88 126.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="110" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 111px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="160" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">connected</text></switch></g><path d="M 290 335.02 L 290 315 L 180 315 L 180 328.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 333.9 L 176.5 326.9 L 180 328.65 L 183.5 326.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 350 335.02 L 350 135 L 216.37 135" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 135 L 218.12 131.5 L 216.37 135 L 218.12 138.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 320 335.02 L 320 235 L 216.37 235" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 235 L 218.12 231.5 L 216.37 235 L 218.12 238.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="270" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="320" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">resuming</text></switch></g><path d="M 500 149.96 L 500 223.59" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 500 228.84 L 496.5 221.84 L 500 223.59 L 503.5 221.84 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="460" y="119.96" width="80" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 135px; margin-left: 461px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">closed</div></div></div></foreignObject><text x="500" y="139" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">closed</text></switch></g><path d="M 520 259.96 L 520 280 L 630 280 L 630 266.33" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 630 261.08 L 633.5 268.08 L 630 266.33 L 626.5 268.08 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 480 229.96 L 480 190 L 480 156.33" fill="noneLine truncated
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -1,4 +0,0 @@
|
||||
for x in $(dirname $0)/*.dot; do
|
||||
echo $x
|
||||
dot $x -Tsvg -o ${x/.dot/.svg}
|
||||
done
|
||||
@@ -1,23 +0,0 @@
|
||||
digraph "Client Reconnection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
reconnect [label="Reconnect"];
|
||||
wait_hello [label="Wait for hello"];
|
||||
resume_session [label="Resume session"];
|
||||
new_session [label="New session" style="dashed"];
|
||||
wait_continue [label="Wait for continue"];
|
||||
connected [label="Connected"];
|
||||
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile reconnecting" style="dotted"];
|
||||
abort [label="Abort"];
|
||||
|
||||
{ rank="same"; wait_continue; new_session };
|
||||
|
||||
lost_connection -> reconnect [label="Wait random delay"];
|
||||
reconnect -> wait_hello [label="Connect to /api/socket"];
|
||||
wait_hello -> resume_session [label="Receive hello"];
|
||||
resume_session -> wait_continue [label="Send resume"];
|
||||
wait_continue -> abort [label="Connection closed with 4003"];
|
||||
wait_continue -> new_session [label="Receive invalidate"];
|
||||
wait_continue -> connected [label="Receive ready"];
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Client Reconnection Flow Pages: 1 -->
|
||||
<svg width="441pt" height="496pt"
|
||||
viewBox="0.00 0.00 441.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
|
||||
<title>Client Reconnection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 437,-492 437,4 -4,4"/>
|
||||
<!-- reconnect -->
|
||||
<g id="node1" class="node">
|
||||
<title>reconnect</title>
|
||||
<polygon fill="none" stroke="black" points="153.5,-384 62.5,-384 62.5,-348 153.5,-348 153.5,-384"/>
|
||||
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Reconnect</text>
|
||||
</g>
|
||||
<!-- wait_hello -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_hello</title>
|
||||
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
|
||||
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
|
||||
</g>
|
||||
<!-- reconnect->wait_hello -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>reconnect->wait_hello</title>
|
||||
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
|
||||
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
|
||||
</g>
|
||||
<!-- resume_session -->
|
||||
<g id="node3" class="node">
|
||||
<title>resume_session</title>
|
||||
<polygon fill="none" stroke="black" points="173.5,-210 42.5,-210 42.5,-174 173.5,-174 173.5,-210"/>
|
||||
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">Resume session</text>
|
||||
</g>
|
||||
<!-- wait_hello->resume_session -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_hello->resume_session</title>
|
||||
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
|
||||
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
|
||||
</g>
|
||||
<!-- wait_continue -->
|
||||
<g id="node5" class="node">
|
||||
<title>wait_continue</title>
|
||||
<polygon fill="none" stroke="black" points="178,-123 38,-123 38,-87 178,-87 178,-123"/>
|
||||
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for continue</text>
|
||||
</g>
|
||||
<!-- resume_session->wait_continue -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>resume_session->wait_continue</title>
|
||||
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
|
||||
<text text-anchor="middle" x="155" y="-144.8" font-family="Times,serif" font-size="14.00">Send resume</text>
|
||||
</g>
|
||||
<!-- new_session -->
|
||||
<g id="node4" class="node">
|
||||
<title>new_session</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="5,2" points="433,-123 327,-123 327,-87 433,-87 433,-123"/>
|
||||
<text text-anchor="middle" x="380" y="-101.3" font-family="Times,serif" font-size="14.00">New session</text>
|
||||
</g>
|
||||
<!-- wait_continue->new_session -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>wait_continue->new_session</title>
|
||||
<path fill="none" stroke="black" d="M178.05,-105C220.94,-105 275.39,-105 316.94,-105"/>
|
||||
<polygon fill="black" stroke="black" points="316.94,-108.5 326.94,-105 316.94,-101.5 316.94,-108.5"/>
|
||||
<text text-anchor="middle" x="252.5" y="-111.8" font-family="Times,serif" font-size="14.00">Receive invalidate</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node6" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
|
||||
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- wait_continue->connected -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>wait_continue->connected</title>
|
||||
<path fill="none" stroke="black" d="M61.23,-86.99C53.95,-82.28 47.4,-76.37 43,-69 39.01,-62.33 38.18,-54.22 38.78,-46.49"/>
|
||||
<polygon fill="black" stroke="black" points="42.27,-46.77 40.32,-36.36 35.35,-45.72 42.27,-46.77"/>
|
||||
<text text-anchor="middle" x="93.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
|
||||
</g>
|
||||
<!-- abort -->
|
||||
<g id="node8" class="node">
|
||||
<title>abort</title>
|
||||
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
|
||||
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
|
||||
</g>
|
||||
<!-- wait_continue->abort -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>wait_continue->abort</title>
|
||||
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
|
||||
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
|
||||
<text text-anchor="middle" x="260.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
|
||||
</g>
|
||||
<!-- lost_connection -->
|
||||
<g id="node7" class="node">
|
||||
<title>lost_connection</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
|
||||
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
|
||||
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
|
||||
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while reconnecting</text>
|
||||
</g>
|
||||
<!-- lost_connection->reconnect -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>lost_connection->reconnect</title>
|
||||
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
|
||||
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -1,21 +0,0 @@
|
||||
digraph "Server Connection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
new_connection [label="New connection"];
|
||||
wait_handshake [label="Wait for handshake"];
|
||||
validate_register [label="Validate register"];
|
||||
validate_resume [label="Validate resume"];
|
||||
connected [label="Connected"];
|
||||
close [label="Close connection"];
|
||||
|
||||
new_connection -> wait_handshake [label="Send hello"];
|
||||
wait_handshake -> validate_register [label="Receive register_*"];
|
||||
wait_handshake -> validate_resume [label="Receive resume"];
|
||||
wait_handshake -> close [label="Bad handshake\nTimeout"];
|
||||
validate_register -> close [label="Bad token\nBad register"];
|
||||
validate_register -> connected [label="Valid register:\nSend ready"];
|
||||
validate_resume -> close [label="Bad token\nBad resume"];
|
||||
validate_resume -> wait_handshake [label="Bad session:\nsend invalidate"];
|
||||
validate_resume -> connected [label="Valid resume:\nSend continue"];
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Server Connection Flow Pages: 1 -->
|
||||
<svg width="603pt" height="335pt"
|
||||
viewBox="0.00 0.00 603.00 335.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 331)">
|
||||
<title>Server Connection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-331 599,-331 599,4 -4,4"/>
|
||||
<!-- new_connection -->
|
||||
<g id="node1" class="node">
|
||||
<title>new_connection</title>
|
||||
<polygon fill="none" stroke="black" points="355.5,-327 224.5,-327 224.5,-291 355.5,-291 355.5,-327"/>
|
||||
<text text-anchor="middle" x="290" y="-305.3" font-family="Times,serif" font-size="14.00">New connection</text>
|
||||
</g>
|
||||
<!-- wait_handshake -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_handshake</title>
|
||||
<polygon fill="none" stroke="black" points="367.5,-240 212.5,-240 212.5,-204 367.5,-204 367.5,-240"/>
|
||||
<text text-anchor="middle" x="290" y="-218.3" font-family="Times,serif" font-size="14.00">Wait for handshake</text>
|
||||
</g>
|
||||
<!-- new_connection->wait_handshake -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>new_connection->wait_handshake</title>
|
||||
<path fill="none" stroke="black" d="M290,-290.8C290,-279.16 290,-263.55 290,-250.24"/>
|
||||
<polygon fill="black" stroke="black" points="293.5,-250.18 290,-240.18 286.5,-250.18 293.5,-250.18"/>
|
||||
<text text-anchor="middle" x="328" y="-261.8" font-family="Times,serif" font-size="14.00">Send hello</text>
|
||||
</g>
|
||||
<!-- validate_register -->
|
||||
<g id="node3" class="node">
|
||||
<title>validate_register</title>
|
||||
<polygon fill="none" stroke="black" points="136,-138 0,-138 0,-102 136,-102 136,-138"/>
|
||||
<text text-anchor="middle" x="68" y="-116.3" font-family="Times,serif" font-size="14.00">Validate register</text>
|
||||
</g>
|
||||
<!-- wait_handshake->validate_register -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>wait_handshake->validate_register</title>
|
||||
<path fill="none" stroke="black" d="M212.45,-216.73C161.73,-212.32 101.57,-203.5 83,-186 72.82,-176.41 68.91,-161.41 67.62,-148.23"/>
|
||||
<polygon fill="black" stroke="black" points="71.11,-147.99 67.07,-138.2 64.12,-148.37 71.11,-147.99"/>
|
||||
<text text-anchor="middle" x="148.5" y="-167.3" font-family="Times,serif" font-size="14.00">Receive register_*</text>
|
||||
</g>
|
||||
<!-- validate_resume -->
|
||||
<g id="node4" class="node">
|
||||
<title>validate_resume</title>
|
||||
<polygon fill="none" stroke="black" points="356,-138 224,-138 224,-102 356,-102 356,-138"/>
|
||||
<text text-anchor="middle" x="290" y="-116.3" font-family="Times,serif" font-size="14.00">Validate resume</text>
|
||||
</g>
|
||||
<!-- wait_handshake->validate_resume -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_handshake->validate_resume</title>
|
||||
<path fill="none" stroke="black" d="M241.15,-203.81C233.91,-199.14 227.42,-193.3 223,-186 216.09,-174.6 216.09,-167.4 223,-156 225.69,-151.55 229.16,-147.65 233.09,-144.22"/>
|
||||
<polygon fill="black" stroke="black" points="235.23,-146.98 241.15,-138.19 231.04,-141.38 235.23,-146.98"/>
|
||||
<text text-anchor="middle" x="280" y="-167.3" font-family="Times,serif" font-size="14.00">Receive resume</text>
|
||||
</g>
|
||||
<!-- close -->
|
||||
<g id="node6" class="node">
|
||||
<title>close</title>
|
||||
<polygon fill="none" stroke="black" points="450.5,-36 313.5,-36 313.5,0 450.5,0 450.5,-36"/>
|
||||
<text text-anchor="middle" x="382" y="-14.3" font-family="Times,serif" font-size="14.00">Close connection</text>
|
||||
</g>
|
||||
<!-- wait_handshake->close -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>wait_handshake->close</title>
|
||||
<path fill="none" stroke="black" d="M367.54,-213.17C403.99,-207.82 442.06,-199.24 454,-186 493.43,-142.29 497.55,-103.05 465,-54 461.92,-49.36 458.09,-45.33 453.78,-41.84"/>
|
||||
<polygon fill="black" stroke="black" points="455.76,-38.95 445.55,-36.1 451.76,-44.69 455.76,-38.95"/>
|
||||
<text text-anchor="middle" x="540.5" y="-123.8" font-family="Times,serif" font-size="14.00">Bad handshake</text>
|
||||
<text text-anchor="middle" x="540.5" y="-108.8" font-family="Times,serif" font-size="14.00">Timeout</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node5" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="127,-36 35,-36 35,0 127,0 127,-36"/>
|
||||
<text text-anchor="middle" x="81" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- validate_register->connected -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>validate_register->connected</title>
|
||||
<path fill="none" stroke="black" d="M34.78,-101.9C28.43,-97.02 22.64,-91.06 19,-84 12.9,-72.14 12.28,-65.51 19,-54 21.52,-49.69 24.78,-45.87 28.5,-42.49"/>
|
||||
<polygon fill="black" stroke="black" points="30.9,-45.05 36.66,-36.16 26.61,-39.52 30.9,-45.05"/>
|
||||
<text text-anchor="middle" x="70" y="-72.8" font-family="Times,serif" font-size="14.00">Valid register:</text>
|
||||
<text text-anchor="middle" x="70" y="-57.8" font-family="Times,serif" font-size="14.00">Send ready</text>
|
||||
</g>
|
||||
<!-- validate_register->close -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>validate_register->close</title>
|
||||
<path fill="none" stroke="black" d="M136,-111.34C178.72,-105.47 229.03,-96.29 247,-84 260.03,-75.09 253.43,-63.55 266,-54 277.12,-45.55 290.27,-39.13 303.66,-34.26"/>
|
||||
<polygon fill="black" stroke="black" points="305.06,-37.48 313.42,-30.97 302.83,-30.84 305.06,-37.48"/>
|
||||
<text text-anchor="middle" x="311" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
|
||||
<text text-anchor="middle" x="311" y="-57.8" font-family="Times,serif" font-size="14.00">Bad register</text>
|
||||
</g>
|
||||
<!-- validate_resume->wait_handshake -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>validate_resume->wait_handshake</title>
|
||||
<path fill="none" stroke="black" d="M321.89,-138.33C327.98,-143.2 333.53,-149.09 337,-156 342.98,-167.92 342.98,-174.08 337,-186 335.05,-189.89 332.44,-193.45 329.44,-196.69"/>
|
||||
<polygon fill="black" stroke="black" points="326.86,-194.32 321.89,-203.67 331.61,-199.46 326.86,-194.32"/>
|
||||
<text text-anchor="middle" x="395.5" y="-174.8" font-family="Times,serif" font-size="14.00">Bad session:</text>
|
||||
<text text-anchor="middle" x="395.5" y="-159.8" font-family="Times,serif" font-size="14.00">send invalidate</text>
|
||||
</g>
|
||||
<!-- validate_resume->connected -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>validate_resume->connected</title>
|
||||
<path fill="none" stroke="black" d="M223.7,-112.81C197.28,-107.96 167.61,-99.33 144,-84 129.91,-74.85 132.49,-66.26 121,-54 117.46,-50.23 113.56,-46.43 109.61,-42.79"/>
|
||||
<polygon fill="black" stroke="black" points="111.86,-40.11 102.07,-36.06 107.2,-45.33 111.86,-40.11"/>
|
||||
<text text-anchor="middle" x="195.5" y="-72.8" font-family="Times,serif" font-size="14.00">Valid resume:</text>
|
||||
<text text-anchor="middle" x="195.5" y="-57.8" font-family="Times,serif" font-size="14.00">Send continue</text>
|
||||
</g>
|
||||
<!-- validate_resume->close -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>validate_resume->close</title>
|
||||
<path fill="none" stroke="black" d="M336.9,-101.98C345.46,-97.2 353.66,-91.27 360,-84 369.17,-73.47 374.56,-58.96 377.7,-46.32"/>
|
||||
<polygon fill="black" stroke="black" points="381.18,-46.78 379.85,-36.27 374.33,-45.32 381.18,-46.78"/>
|
||||
<text text-anchor="middle" x="418" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
|
||||
<text text-anchor="middle" x="418" y="-57.8" font-family="Times,serif" font-size="14.00">Bad resume</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -41,7 +41,7 @@ If you already cloned the main GitHub repository you can change your clone to po
|
||||
|
||||
To keep your local repository up to to date with the main repository add the main repository as a remote.
|
||||
|
||||
git remote add upstream https://github.com/clusterio/factorioClusterio
|
||||
git remote add upstream https://github.com/clusterio/clusterio
|
||||
|
||||
After this running `git fetch upstream` will get the updated branches from the main repository.
|
||||
Do not run `git pull upstream` it will mess thing up!
|
||||
@@ -54,25 +54,47 @@ In order to get the development environment up and running you will need to run
|
||||
|
||||
npm install
|
||||
npx lerna bootstrap --hoist
|
||||
npx lerna run build
|
||||
|
||||
This installs dependencies needed by tests and links the packages up so they work from the git work tree.
|
||||
To start a cluster from the repo, substitute the commands in the normal readme as follows:
|
||||
To start a cluster from the repo you will first need to initialize it, this can be done using the installer:
|
||||
|
||||
node packages/create --dev
|
||||
|
||||
To add plugins to use in the development environment use
|
||||
|
||||
node packages/ctl plugin add ./path/to/plugin
|
||||
|
||||
It's important that the path starts with `./`.
|
||||
The `plugins/` directory contains the core plugins for the project and can be added using the above mentioned command.
|
||||
|
||||
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 clusterioctl -> node packages/ctl
|
||||
|
||||
For web development on the master there are also the following flags:
|
||||
Note that to use `clusterioctl` you will have to create a control config first:
|
||||
|
||||
`--dev` - Start the master with webpack hot reloading of core components
|
||||
node packages/master 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.
|
||||
|
||||
For web development on the master there are also the following flags.
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
|
||||
`--dev-plugin [name]` - Recompile the web module for the given plugin on the fly proving live reloading of it.
|
||||
|
||||
These options can be combined and multiple plugins can be actived for dev mode.
|
||||
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `npx lerna run build` before you start the master without these flags again.
|
||||
|
||||
`--dev-plugin [name]` - Start the master with hot reloading of a plugin
|
||||
|
||||
### Starting a Feature Branch
|
||||
|
||||
Development is done exclusively in feature branches.
|
||||
This means the first thing to do before starting any development at all is to make a new branch starting from the master or 1.2.x branch of the main repository.
|
||||
To do this check out that branch from the main repository (substitute master for 1.2.x if you're working on that branch).
|
||||
This means the first thing to do before starting any development at all is to make a new branch starting from the master branch of the main repository.
|
||||
To do this check out that branch from the main repository.
|
||||
|
||||
git fetch upstream
|
||||
git checkout upstream/master
|
||||
@@ -113,7 +135,7 @@ If there are more changes you want to have merged or mistakes you need to fix yo
|
||||
|
||||
Until your feature branch is merged you can edit it as you see fit by using the rebase tool.
|
||||
If your feature branch has been merged however it's final and you will have to start a new feature branch and submit that in a new pull request.
|
||||
The base usage of rebase assuming you are checked out on your feature branch is the following command (substitute master with 1.2.x if your feature branch is based on that)
|
||||
The base usage of rebase assuming you are checked out on your feature branch is the following command
|
||||
|
||||
git rebase -i upstream/master
|
||||
|
||||
@@ -132,7 +154,7 @@ If you have a pull request that has not yet been merged submitted for this featu
|
||||
### Resolving Merge Conflicts
|
||||
|
||||
The best way to resolve merge conflicts in feature braches is to rebase it on top of the updated upstream.
|
||||
Run the following commands (substitute master with 1.2.x if your feature branch is based on that)
|
||||
Run the following commands
|
||||
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
@@ -202,9 +224,9 @@ JavaScript template literals may also be used where appropriate.
|
||||
|
||||
### Indenting
|
||||
|
||||
JavaScript code should be indented with tabs.
|
||||
JavaScript and Lua code should be indented with tabs.
|
||||
|
||||
Lua code should be indented with 4 spaces.
|
||||
Other files types should prefer tabs for indents where reasonable with the exception of Markdown.
|
||||
|
||||
|
||||
### Line Length
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
# Protocol
|
||||
|
||||
Clusterio communicates using a simple homebrew message passing protocol based on JSON over WebSocket with limited support for reliable transport of messages.
|
||||
The protocol is implemented in [packages/lib/link](/packages/lib/link) and uses the `ws` library to handle the WebSocket connections on the Node.js side and the native WebSocket API on the browser side.
|
||||
|
||||
Two general kinds of messages are supported: events and requests.
|
||||
Events are messages sent by one party and received by the other and offer no notification for the sender on whether it was actually received.
|
||||
Requests are pairs of request-response messages where one side sends the requests and the other side replies with a response.
|
||||
Apart from the handshake used to establish connections the protocol is directionless, and either party can send or receive both requests and events.
|
||||
|
||||
Events and requests are defined as instances of the `Request` and `Event` classes, and specify a unique type string, who can send and receive the message, optional forwarding of the message and the format of the data payload as a JSON schema.
|
||||
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.
|
||||
|
||||
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.
|
||||
The handlers are defined on the link class itself for core messages, and on the plugin class in the case of plugin defined messages.
|
||||
|
||||
|
||||
## 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 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.
|
||||
Messages sent over the link should be stored in a send buffer and only cleared from the send buffer when a heartbeat with a `last_seq` equal or or higher is received or the session is invalidated.
|
||||
|
||||
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`.
|
||||
|
||||

|
||||
|
||||
|
||||
## Connector flow
|
||||
|
||||
To manage the complexity of connecting, reconnecting, resuming or invalidating dropped sessions the WebSocket connection for a `Link` class is managed by a connector.
|
||||
There's three kinds of connectors defined: `WebSocketClientConnector`, `WebSocketServerConnector` and `VirtualConnector`.
|
||||
The virtual connector is for in-memory connections and doesn't have any connection state logic.
|
||||
|
||||
For the WebSocket connectors there's four states they can exist in:
|
||||
- `closed` - The connector has just been created or has been explicitly closed.
|
||||
- `connecting` (client only) - The connector is in the process of trying to connect to the server.
|
||||
- `connected` - The connector is successfully connected.
|
||||
- `resuming` - The connection dropped and the connector is in the process of attempting to resume it.
|
||||
|
||||
Along with the following events emitted when transition between states:
|
||||
- `connect` From `connecting` (client) or `closed` (server) to `connected`
|
||||
- `drop` - From `connected` to `resuming`
|
||||
- `resume` - From `resuming` to `connected`
|
||||
- `invalidate` - From `resuming` to `connecting` or `closed`
|
||||
- `close` - From any state to `closed`
|
||||
|
||||

|
||||
|
||||
|
||||
### WsClientConnector
|
||||
|
||||
The client connector will keep trying to establish and mantain a connection after `.connect()` is called on it, stopping only in the case of authentication failure or `.close()`/`.setClosing()` being called.
|
||||
Other than that, the logic is pretty straight forward.
|
||||
|
||||

|
||||
|
||||
|
||||
### WsServerConnector
|
||||
|
||||
The server connector doesn't do the work of connecting or validating the handshake.
|
||||
Instead it's handed over a WebSocket connection from `WsServer` once there's only the step of sending the final message left to do.
|
||||
Because of this there's no `connecting` state, and once a session times out from the `resuming` state it goes directly to closed.
|
||||
|
||||

|
||||
|
||||
|
||||
## Wire format
|
||||
|
||||
All communication in the protocol is done via WebSocket text frames containing a JSON payload with the following properties:
|
||||
|
||||
- `seq: integer | null` - The sequence number of the message, can only be null for handshake and heartbeats.
|
||||
- `type: string` - The type of message
|
||||
- `data: object` - Message data
|
||||
|
||||
Before a connection is established it's in the handshake phase and the `seq` property is `null`.
|
||||
The `type` property is used to distinguish which of handshake, heartbeat, events or requests this message is and determines the expected format of the `data` property.
|
||||
|
||||
|
||||
### Handshake Messages
|
||||
|
||||
The following messages are the only valid messages that can be sent during the handshake phase.
|
||||
|
||||
|
||||
#### `hello`
|
||||
|
||||
Sent by the master server when the WebSocket connection has been opened.
|
||||
|
||||
- `version: string` - The version of the master, 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.
|
||||
|
||||
#### `register_slave`
|
||||
|
||||
Slave 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.
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are available on the slave.
|
||||
|
||||
#### `register_control`
|
||||
|
||||
Control handshake for enstablishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `agent: string` - Human readable string of the software connecting.
|
||||
- `version: string` - The protocol version of the control, e.g. "2.0.0".
|
||||
|
||||
#### `resume`
|
||||
|
||||
Handshake for resuming an existing connection session where the connection had dropped unexpectedly.
|
||||
|
||||
- `session_token: string` - Session token to resume session with.
|
||||
- `last_seq: integer | null` - Last message seq received by the client.
|
||||
|
||||
#### `invalidate`
|
||||
|
||||
Sent by the master server 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.
|
||||
|
||||
- `last_seq: integer | null` - Last message seq received by the server.
|
||||
- `session_timeout: integer` - Session timeout in seconds.
|
||||
- `heartbeat_interval: number` - Rate client should send heartbeats at.
|
||||
|
||||
#### `ready`
|
||||
|
||||
Sent by the master server 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.
|
||||
- `heartbeat_interval: number` - Rate client should send heartbeats at.
|
||||
|
||||
|
||||
### Heartbeat message
|
||||
|
||||
Once connected as indicated by the `ready` and `continue` messages both sides of the connection starts sending heartbeats at the inverval given in the `ready` and `continue` messages.
|
||||
The heartbeat should contain the `seq` of the last received message, and receiving a heartbeat with a given sequence number means that the other party of the link has received all messages up to that seq number.
|
||||
|
||||
|
||||
#### `heartbeat`
|
||||
|
||||
Sent by both the master server and clients at the interval given in the continue/ready messages.
|
||||
|
||||
- `last_seq: integer` - Last message seq received over the link.
|
||||
|
||||
### Events
|
||||
|
||||
Events are messages send in one direction over the link, that the receiver is expected to act upon, but not make any replies (for that there's [Requests](#requests).
|
||||
By convention message types ending in `*_event` are events.
|
||||
The data payload of an event consists solely of event specific properties.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core events and their contents.
|
||||
Plugins may defined their own events as well.
|
||||
|
||||
#### `*_event`
|
||||
|
||||
- any properties defined by the event type.
|
||||
|
||||
|
||||
### Requests
|
||||
|
||||
Requests are messages which expects a response in return.
|
||||
They function a lot like HTTP requests except that both parties of a connection can inniate a request that is expected to be responded to.
|
||||
By convention message types ending in `*_request` are requests, and is expected to be replied to with a corresponding `*_response` message.
|
||||
If an error occured while processing the request an error response containing an error property in the data payload is sent instead.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core requests and their contents.
|
||||
Plugins may define their own requests as well.
|
||||
|
||||
#### `*_request`
|
||||
|
||||
- any properties defined by the request type.
|
||||
|
||||
#### `*_response` success
|
||||
|
||||
- `seq: integer` - The seq number of the request message.
|
||||
- any additional properties defined by the request type.
|
||||
|
||||
#### `*_response` error
|
||||
|
||||
- `seq: integer` - The seq number of the request message.
|
||||
- `error: string` - Human readable text message describing the error.
|
||||
|
||||
|
||||
### Graceful Disconnect
|
||||
|
||||
To ensure no data is lost when closing the link it's important to send a prepare disconnect request and wait for it to complete before closing the WebSocket.
|
||||
Either party of the link can send this request and the party sending the request should be the one that initates the close of the WebSocket.
|
||||
|
||||
#### `prepare_disconnect_request`
|
||||
|
||||
Request graceful disconnect of the connection.
|
||||
The receiving party should stop tasks that send events and requests over the link and wait for pending requests it has over the link to complete, but it should not stop responding to requests sent to it.
|
||||
Only after this process is done should the response be sent.
|
||||
|
||||
#### `prepare_disconnect_response`
|
||||
|
||||
Signals the preparation for disconnect is complete and that the connection can be closed.
|
||||
The party that initiatied the prepare disconnect request may still send other requests/events over the link to finish its part of the disconnection proceedure.
|
||||
|
||||
If the error response variant is received it should be logged if possible and treated as a success.
|
||||
|
||||
|
||||
### Example handshake
|
||||
|
||||
Server hello after opening the WebSocket.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "hello",
|
||||
"data": {
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Slave registering a new connection session.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "register_slave",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"agent": "Clusterio Slave",
|
||||
"version": "2.0.0",
|
||||
"name": "Foo",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Master server reply signaling the session is established and ready.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "ready",
|
||||
"data": {
|
||||
"session_token": "xyz.azy.bzy",
|
||||
"session_timeout": 60,
|
||||
"heartbeat_interval": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Slave and master can now send events and requests over the connection.
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"tags": {
|
||||
"allowUnknownTags": false
|
||||
},
|
||||
"opts": {
|
||||
"destination": "docs/api",
|
||||
"recurse": true
|
||||
},
|
||||
"source": {
|
||||
"include": [
|
||||
"packages/master/src/",
|
||||
"packages/master/master.js",
|
||||
"packages/slave/slave.js",
|
||||
"packages/ctl/ctl.js",
|
||||
"packages/lib/",
|
||||
"plugins/"
|
||||
],
|
||||
"excludePattern": "node_modules"
|
||||
}
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"tags": {
|
||||
"allowUnknownTags": false
|
||||
},
|
||||
"opts": {
|
||||
"destination": "docs/api",
|
||||
"recurse": true
|
||||
},
|
||||
"source": {
|
||||
"include": [
|
||||
"packages/master/src/",
|
||||
"packages/master/master.js",
|
||||
"packages/slave/slave.js",
|
||||
"packages/ctl/ctl.js",
|
||||
"packages/lib/",
|
||||
"plugins/"
|
||||
],
|
||||
"excludePattern": "node_modules"
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
# WebSocket protocol
|
||||
|
||||
<sub>This document describes the protocol used between the master and slaves, and unless you're developing Clusterio it's probably not going to be very useful for you.</sub>
|
||||
The protocol is built on WebSocket and uses JSON encoded text messages to communicate to and from the master server.
|
||||
These messages contain a JSON encoded object with the following three properties:
|
||||
|
||||
- seq - integer|null - The sequence number of the message
|
||||
- type - string - The type of message
|
||||
- data - object - Message data
|
||||
|
||||
For the handshake and heartbeats the seq property is null.
|
||||
|
||||
|
||||
## Handshake
|
||||
|
||||
Upon connecting to the WebSocket endpoint the server will respond with a `hello` message containing the master server version as the data.
|
||||
The client is then expected to send either `register_*` or a `resume` message to set up the connection with the master.
|
||||
The exact flow is described below.
|
||||
|
||||
|
||||
### Client connection flow
|
||||
|
||||
For a client making a new connecting to the master server the first course of action after obtaining a token is to connect with WebSocket to the /api/socket endpoint and wait for the server hello.
|
||||
After the hello, the client sends a register\_\* message corresponding to the type of the client.
|
||||
If successful the server will reply with a ready message and the connection has been established.
|
||||
See the flowchart below.
|
||||
|
||||

|
||||
|
||||
If for any reason the connection is closed or lost during the connection attempt the process is started over after a random delay, with the exception of the connection being closed with a 4003 code which means authentication failed and the client should stop trying to connect.
|
||||
The wait for hello and wait for ready states should also have a timeout, after which the connection is closed if the hello/reply wasn't received
|
||||
in time.
|
||||
|
||||
|
||||
### Client reconnection flow
|
||||
|
||||
If the connection is lost after the client has received the ready message from the server it should attempt a reconnect instead.
|
||||
This is very similar to a new connection, but instead of a register\_\* messsage the client sends a resume message and the server replies with a continue message if resuming the session was successful.
|
||||
This continue message has sequence number of the last message the master received and the client is expected to imediately send all messages from its send buffer that occured after the sequence number given.
|
||||
|
||||
The server will respond with an invalidate message if resuming the session is not possible.
|
||||
In this case the client should continue from the new session state of the connection flow (see the previous section).
|
||||
|
||||

|
||||
|
||||
Like the connection flow the process starts over after a delay if the connection is closed or lost.
|
||||
The same exception for connection closed with 4003 applies as with the connection flow.
|
||||
Additionally after an invalidate message has been received and the connection flow fails, it should start over from the connection flow.
|
||||
|
||||
|
||||
### Server connection flow
|
||||
|
||||
From the master server side new connections and reconnection attempts start out the same, a new connection has been made and the server sends the hello messoge to the client.
|
||||
If a vaild register\_\* message is received a new connection is set up for the client and the ready message is sent in reply.
|
||||
If a valid resume message is received and the session that's attempted to be resumed is active it'll signal that the session is resumed with a continue message and the client upon receiving the continue message should send the messages from its send buffer from after the last\_seq given in the continue message.
|
||||
|
||||
If the session token used to resume is not valid it'll send an invalidate message and expect the client to continue with a registr\_\* message.
|
||||
If an invalid or unexpected message is received during the handshake the connection is closed.
|
||||
See the flowchart below.
|
||||
|
||||

|
||||
|
||||
For authentication failure the connection is closed with code 4003, which the connection client should interpret as connecting will never succeed and stop trying to reconnect.
|
||||
|
||||
|
||||
### Connection tracking and heartbeats
|
||||
|
||||
Once connected as indicated by the ready and continue messages from the master server the client must start sending heartbeats at the interval given in the ready/continue message, and storing messages sent over the link in a sending side buffer.
|
||||
The heartbeats sent must have the seq of the last received message on the link and acts as an ack for received messages.
|
||||
The master server also sends heartbeats in the same manner and the client should remove messages up to and including the sequence number received in the heartbeat from its send buffer.
|
||||
|
||||
|
||||
## Handshake messages
|
||||
|
||||
### `hello`
|
||||
|
||||
Sent by the master server when the WebSocket connection has been opened.
|
||||
|
||||
- version - string - The version of the master, 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.
|
||||
|
||||
### `register_slave`
|
||||
|
||||
Slave 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.
|
||||
- plugins - Object<string, string> -
|
||||
Object mapping plugin names to plugin version for plugins that are available on the slave.
|
||||
|
||||
### `register_control`
|
||||
|
||||
Control handshake for enstablishing a new connection session.
|
||||
|
||||
- token - string - Authentication token.
|
||||
- agent - string - Human readable string of the software connecting.
|
||||
- version - string - The protocol version of the control, e.g. "2.0.0".
|
||||
|
||||
### `resume`
|
||||
|
||||
Handshake for resuming an existing connection session where the connection had dropped unexpectedly.
|
||||
|
||||
- session_token - string - Session token to resume session with.
|
||||
- last_seq - integer|null - Last message seq received by the client.
|
||||
|
||||
### `invalidate`
|
||||
|
||||
Sent by the master server 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.
|
||||
|
||||
- last_seq - integer|null - Last message seq received by the server.
|
||||
- heartbeat_interval - number - Rate client should send heartbeats at.
|
||||
|
||||
### `ready`
|
||||
|
||||
Sent by the master server to signal the connection has been established with a new session.
|
||||
|
||||
- session_token - string - Token to resume the session with.
|
||||
- heartbeat_interval - number - Rate client should send heartbeats at.
|
||||
|
||||
### `heartbeat`
|
||||
|
||||
Sent by both the master server and clients at the interval given in the continue/ready messages.
|
||||
|
||||
- last_seq - integer - Last message seq received over the link.
|
||||
|
||||
|
||||
## Example handshake
|
||||
|
||||
Server hello after opening the WebSocket.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "hello",
|
||||
"data": {
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Slave registering a new connection session.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "register_slave",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"agent": "Clusterio Slave",
|
||||
"version": "2.0.0",
|
||||
"name": "Foo",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Master server reply signaling the session is established and ready.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "ready",
|
||||
"data": {
|
||||
"session_token": "xyz.azy.bzy",
|
||||
"heartbeat_interval": 45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
Events are messages send in one direction over the link, that the receiver is expected to act upon, but not make any replies (for that there's [Requests](#requests).
|
||||
By convention message types ending in `*_event` are events.
|
||||
The data payload of an event consists solely of event specific properties.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized events and their contents.
|
||||
|
||||
|
||||
## Requests
|
||||
|
||||
Requests are messages which expects a response in return.
|
||||
They function a lot like HTTP requests except that both parties of a connection can inniate a request that is expected to be responded to.
|
||||
By convention message types ending in `*_request` are requests, and is expected to be replied to with a corresponding `*_response` message.
|
||||
If an error occured while processing the request an error response containing an error property in the data payload is sent instead.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized requests and their contents.
|
||||
|
||||
### `*_response`
|
||||
|
||||
- seq - integer - The seq number of the request message.
|
||||
- additional properties may be present depending on the type of request.
|
||||
|
||||
### `*_response` error
|
||||
|
||||
- seq - integer - The seq number of the request message.
|
||||
- error - string - Human readable text message describing the error.
|
||||
|
||||
|
||||
## Graceful Disconnect
|
||||
|
||||
To ensure no data is lost when closing the link it's important to send a prepare disconnect request and wait for it to complete before closing the WebSocket.
|
||||
Either party of the link can send this request and the party sending the request should be the one that initates the close of the WebSocket.
|
||||
|
||||
### `prepare_disconnect_request`
|
||||
|
||||
Request graceful disconnect of the connection.
|
||||
The receiving party should stop tasks that send events and requests over the link and wait for pending requests it has over the link to complete, but it should not stop responding to requests sent to it.
|
||||
Only after this process is done should the response be sent.
|
||||
|
||||
### `prepare_disconnect_response`
|
||||
|
||||
Signals the preparation for disconnect is complete and that the connection can be closed.
|
||||
The party that initiatied the prepare disconnect request may still send other requests/events over the link to finish its part of the disconnection proceedure.
|
||||
|
||||
If the error response variant is received it should be logged if possible and treated as a success.
|
||||
@@ -49,7 +49,7 @@ Without it the plugin will not recognized by Clusterio.
|
||||
Here's an example of it:
|
||||
|
||||
```js
|
||||
const libLink = require("@clusterio/lib/link"); // For messages
|
||||
const { libLink } = require("@clusterio/lib"); // For messages
|
||||
|
||||
module.exports = {
|
||||
name: "foo_frobber",
|
||||
@@ -502,11 +502,11 @@ This is signaled to `MasterPlugin` classes via the `onPrepareSlaveDisconnect` ho
|
||||
|
||||
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` or `closing` property of the connector for the master/slave connection.
|
||||
For example the sending of an event from an `InstancePlugin` class can be stopped while the connection is closing by using the following code:
|
||||
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.
|
||||
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.connector.closing) {
|
||||
if (this.slave.connected) {
|
||||
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
|
||||
}
|
||||
```
|
||||
@@ -517,8 +517,8 @@ The re-establishement of the connection is notified to plugins via the `connect
|
||||
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.
|
||||
However while in the dropped state any requests and events sent gets queued up in memory until the connection is re-established.
|
||||
This means that if your plugin sends a lot of events or requests, they can end up being queued up in a buffer for a long time and sent out all at once.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"plugins/*",
|
||||
"test"
|
||||
],
|
||||
"version": "2.0.0-alpha.7"
|
||||
"version": "2.0.0-alpha.9"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Clusterio installer
|
||||
|
||||
Installer for Clusterio.
|
||||
|
||||
See [the Clusterio README.md](https://github.com/clusterio/clusterio#readme) the for installation guide.
|
||||
@@ -0,0 +1,609 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
const child_process = require("child_process");
|
||||
const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const inquirer = require("inquirer");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const phin = require("phin");
|
||||
const stream = require("stream");
|
||||
const util = require("util");
|
||||
const yargs = require("yargs");
|
||||
|
||||
const { levels, logger, setLogLevel } = require("./logging");
|
||||
let dev = false;
|
||||
const scriptExt = process.platform === "win32" ? ".cmd" : "";
|
||||
|
||||
|
||||
const availablePlugins = [
|
||||
{ name: "Global Chat", value: "@clusterio/plugin-global_chat" },
|
||||
{ name: "Inventory Sync", value: "@clusterio/plugin-inventory_sync" },
|
||||
{ name: "Player Auth", value: "@clusterio/plugin-player_auth" },
|
||||
{ name: "Research Sync", value: "@clusterio/plugin-research_sync" },
|
||||
{ name: "Statistics Exporter", value: "@clusterio/plugin-statistics_exporter" },
|
||||
{ name: "Subspace Storage", value: "@clusterio/plugin-subspace_storage" },
|
||||
|
||||
// Comunity plugins
|
||||
{ name: "Discord Bridge", value: "@hornwitser/discord_bridge" },
|
||||
{ name: "Server Select", value: "@hornwitser/server_select" },
|
||||
];
|
||||
|
||||
const factorioLocations = {
|
||||
win32: [
|
||||
"C:\\Program Files\\Factorio",
|
||||
"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Factorio",
|
||||
],
|
||||
darwin: [
|
||||
"/Applications/factorio.app/Contents",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
class LineSplitter extends stream.Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._partial = null;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (this._partial) {
|
||||
chunk = Buffer.concat([this._partial, chunk]);
|
||||
this._partial = null;
|
||||
}
|
||||
|
||||
while (chunk.length) {
|
||||
let end = chunk.indexOf("\n");
|
||||
if (end === -1) {
|
||||
this._partial = chunk;
|
||||
break;
|
||||
}
|
||||
|
||||
let next = end + 1;
|
||||
// Eat carriage return as well if present
|
||||
if (end >= 1 && chunk[end-1] === "\r".charCodeAt(0)) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
let line = chunk.slice(0, end);
|
||||
chunk = chunk.slice(next);
|
||||
this.push(line);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
if (this._partial) {
|
||||
this.push(this._partial);
|
||||
this._partial = null;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
class InstallError extends Error { }
|
||||
|
||||
|
||||
async function execFile(cmd, args) {
|
||||
logger.verbose(`executing ${cmd} ${args.join(" ")}`);
|
||||
const asyncExec = util.promisify(child_process.execFile);
|
||||
return new Promise((resolve, reject) => {
|
||||
let child = child_process.execFile(cmd, args, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
let stdout = new LineSplitter({ readableObjectMode: true });
|
||||
stdout.on("data", line => { logger.verbose(line.toString()); });
|
||||
child.stdout.pipe(stdout);
|
||||
let stderr = new LineSplitter({ readableObjectMode: true });
|
||||
stderr.on("data", line => { logger.verbose(`err: ${line.toString()}`); });
|
||||
child.stderr.pipe(stderr);
|
||||
});
|
||||
}
|
||||
|
||||
async function execMaster(args) {
|
||||
if (dev) {
|
||||
return await execFile("node", [path.join("packages", "master"), ...args]);
|
||||
}
|
||||
return await execFile(path.join("node_modules", ".bin", `clusteriomaster${scriptExt}`), args);
|
||||
}
|
||||
|
||||
async function execSlave(args) {
|
||||
if (dev) {
|
||||
return await execFile("node", [path.join("packages", "slave"), ...args]);
|
||||
}
|
||||
return await execFile(path.join("node_modules", ".bin", `clusterioslave${scriptExt}`), args);
|
||||
}
|
||||
|
||||
function validateSlaveToken(token) {
|
||||
let parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new InstallError("Invalid token");
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(Buffer.from(parts[1], "base64"));
|
||||
} catch (err) {
|
||||
throw new InstallError("Invalid token");
|
||||
}
|
||||
|
||||
if (parsed.aud !== "slave" || !Number.isInteger(parsed.slave)) {
|
||||
throw new InstallError("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
async function validateInstallDir() {
|
||||
let entries = new Set(await fs.readdir("."));
|
||||
if (entries.size) {
|
||||
if (!entries.has("package.json")) {
|
||||
throw new InstallError("Refusing to install to non-empty directory");
|
||||
}
|
||||
|
||||
let packageData;
|
||||
try {
|
||||
packageData = JSON.parse(await fs.readFile("package.json"));
|
||||
} catch (err) {
|
||||
throw new InstallError(`Failed to read package.json: ${err.message}`);
|
||||
}
|
||||
if (packageData.name !== "clusterio-install" || !packageData.private) {
|
||||
throw new InstallError("Refusing to run on non-clusterio installation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function installClusterio(mode, plugins) {
|
||||
try {
|
||||
await fs.writeFile("package.json", JSON.stringify({
|
||||
name: "clusterio-install",
|
||||
private: true,
|
||||
}, null, 2), { flag: "wx" });
|
||||
} catch (err) {
|
||||
if (err.code !== "EEXIST") {
|
||||
throw new InstallError(`Failed to write package.json: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let components = [];
|
||||
if (["standalone", "master"].includes(mode)) {
|
||||
components.push("@clusterio/master");
|
||||
}
|
||||
if (["standalone", "slave"].includes(mode)) {
|
||||
components.push("@clusterio/slave");
|
||||
}
|
||||
if (mode === "ctl") {
|
||||
components.push("@clusterio/ctl");
|
||||
}
|
||||
|
||||
logger.info(`Please wait, installing ${mode}`);
|
||||
try {
|
||||
await execFile(`npm${scriptExt}`, ["install", ...components, ...plugins]);
|
||||
} catch (err) {
|
||||
throw new InstallError(`Failed to install: ${err.message}`);
|
||||
}
|
||||
|
||||
if (plugins.length) {
|
||||
logger.info("Setting up plugins");
|
||||
let pluginList;
|
||||
try {
|
||||
pluginList = new Map(JSON.parse(await fs.readFile("plugin-list.json")));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
pluginList = new Map();
|
||||
} else {
|
||||
throw new InstallError(`Error loading plugin-list.json: ${err.message}`);
|
||||
}
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
if (!pluginList.has(plugin)) {
|
||||
// eslint-disable-next-line node/global-require
|
||||
let pluginInfo = require(require.resolve(path.posix.join(plugin, "info"), { paths: [process.cwd()] }));
|
||||
pluginList.set(pluginInfo.name, plugin);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.writeFile("plugin-list.json", JSON.stringify([...pluginList], null, 4));
|
||||
} catch (err) {
|
||||
throw new InstallError(`Error writing plugin-list.json: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeScripts(mode) {
|
||||
if (["standalone", "master"].includes(mode)) {
|
||||
if (process.platform === "win32") {
|
||||
await fs.writeFile(
|
||||
"run-master.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusteriomaster.cmd run\n"
|
||||
);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
"run-master.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusteriomaster run\n",
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await fs.outputFile(
|
||||
"systemd/clusteriomaster.service",
|
||||
`[Unit]
|
||||
Description=Clusterio Master
|
||||
|
||||
[Service]
|
||||
User=${os.userInfo().username}
|
||||
Group=nogroup
|
||||
WorkingDirectory=${process.cwd()}
|
||||
KillMode=mixed
|
||||
KillSignal=SIGINT
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusteriomaster run --log-level=warn
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (["standalone", "slave"].includes(mode)) {
|
||||
if (process.platform === "win32") {
|
||||
await fs.writeFile(
|
||||
"run-slave.cmd",
|
||||
"@echo off\n.\\node_modules\\.bin\\clusterioslave.cmd run\n"
|
||||
);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
"run-slave.sh",
|
||||
"#!/bin/sh\nexec ./node_modules/.bin/clusterioslave run\n",
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await fs.outputFile(
|
||||
"systemd/clusterioslave.service",
|
||||
`[Unit]
|
||||
Description=Clusterio Slave
|
||||
|
||||
[Service]
|
||||
User=${os.userInfo().username}
|
||||
Group=nogroup
|
||||
WorkingDirectory=${process.cwd()}
|
||||
KillMode=mixed
|
||||
KillSignal=SIGINT
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusterioslave run --log-level=warn
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function inquirerMissingArgs(args) {
|
||||
let answers = {};
|
||||
if (args.mode) { answers.mode = args.mode; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "mode",
|
||||
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: "Ctl only", value: "ctl" },
|
||||
{ name: "Plugins only", value: "plugins" },
|
||||
],
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (["standalone", "master"].includes(answers.mode)) {
|
||||
if (args.admin) { answers.admin = args.admin; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "admin",
|
||||
message: "Admin account name",
|
||||
validate: input => {
|
||||
if (!input) {
|
||||
return "May not be empty";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (answers.mode === "slave") {
|
||||
if (args.slaveName) { answers.slaveName = args.slaveName; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "slaveName",
|
||||
message: "Name of slave",
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (["slave", "ctl"].includes(answers.mode)) {
|
||||
if (args.masterUrl) { answers.masterUrl = args.masterUrl; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "masterUrl",
|
||||
message: "Master server URL",
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (args.masterToken) { answers.masterToken = args.masterToken; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "masterToken",
|
||||
message: "Master authentication Token",
|
||||
},
|
||||
], answers);
|
||||
}
|
||||
|
||||
if (answers.mode === "slave") {
|
||||
validateSlaveToken(answers.masterToken);
|
||||
}
|
||||
|
||||
if (["standalone", "slave"].includes(answers.mode)) {
|
||||
let myIp;
|
||||
if (args.publicAddress) {
|
||||
answers.publicAddress = args.publicAddress;
|
||||
} else {
|
||||
try {
|
||||
let result = await phin("https://api.ipify.org/");
|
||||
myIp = result.body.toString();
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "publicAddress",
|
||||
message: "Public DNS/IP address of this server",
|
||||
default: myIp,
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (args.factorioDir) { answers.factorioDir = args.factorioDir; }
|
||||
let locations = factorioLocations[process.platform] || [];
|
||||
let foundLocations = [];
|
||||
for (let location of locations) {
|
||||
if (await fs.pathExists(path.join(location, "data", "changelog.txt"))) {
|
||||
foundLocations.push(location);
|
||||
}
|
||||
}
|
||||
if (process.platform === "linux") {
|
||||
if (args.hasOwnProperty("downloadHeadless")) { answers.downloadHeadless = args.downloadHeadless; }
|
||||
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "downloadHeadless",
|
||||
message: "(Linux only) Automatically download latest factorio release?",
|
||||
default: true,
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (answers.downloadHeadless) {
|
||||
answers.factorioDir = "factorio";
|
||||
}
|
||||
}
|
||||
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "factorioDir",
|
||||
message: "Path to Factorio installation",
|
||||
choices: [
|
||||
...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })),
|
||||
{ name: "Use local factorio directory, you must copy an installation to it", value: "factorio" },
|
||||
{ name: "Provide path manually", value: null },
|
||||
],
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (answers.factorioDir === null) {
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "factorioDir",
|
||||
message: "Path to Factorio installation",
|
||||
askAnswered: true,
|
||||
},
|
||||
], answers);
|
||||
} else if (answers.factorioDir === "factorio") {
|
||||
await fs.ensureDir("factorio");
|
||||
}
|
||||
}
|
||||
|
||||
if (dev) { answers.plugins = []; }
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "plugins",
|
||||
message: "Plugins to install",
|
||||
choices: availablePlugins,
|
||||
pageSize: 20,
|
||||
},
|
||||
], answers);
|
||||
|
||||
return answers;
|
||||
}
|
||||
|
||||
async function downloadLinuxServer() {
|
||||
let res = await phin("https://factorio.com/get-download/stable/headless/linux64");
|
||||
|
||||
const url = new URL(res.headers.location);
|
||||
// get the filename of the latest factorio archive from redirected url
|
||||
const filename = path.posix.basename(url.pathname);
|
||||
const version = filename.match(/(?<=factorio_headless_x64_).*(?=\.tar\.xz)/)[0];
|
||||
|
||||
const tmpDir = "temp/create-temp/";
|
||||
const archivePath = tmpDir + filename;
|
||||
const tmpArchivePath = `${archivePath}.tmp`;
|
||||
const factorioDir = `factorio/${version}/`;
|
||||
const tmpFactorioDir = tmpDir + version;
|
||||
|
||||
if (await fs.pathExists(factorioDir)) {
|
||||
logger.warn(`setting downloadDir to ${factorioDir}, but not downloading because already existing`);
|
||||
} else {
|
||||
await fs.ensureDir(tmpDir);
|
||||
|
||||
// follow the redirect
|
||||
res = await phin({
|
||||
url: url.href,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
logger.info("Downloading latest Factorio server release. This may take a while.");
|
||||
const writeStream = fs.createWriteStream(tmpArchivePath);
|
||||
res.pipe(writeStream);
|
||||
|
||||
await events.once(res, "end");
|
||||
|
||||
await fs.rename(tmpArchivePath, archivePath);
|
||||
try {
|
||||
await fs.ensureDir(tmpFactorioDir);
|
||||
await execFile("tar", [
|
||||
"xf", archivePath, "-C", tmpFactorioDir, "--strip-components", "1",
|
||||
]);
|
||||
} catch (e) {
|
||||
logger.error("error executing command- do you have 'xz-utils' installed?");
|
||||
throw e;
|
||||
}
|
||||
|
||||
await fs.unlink(archivePath);
|
||||
await fs.rename(tmpFactorioDir, factorioDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let args = yargs
|
||||
.option("log-level", {
|
||||
nargs: 1, describe: "Log level to print to stdout", default: "info",
|
||||
choices: ["none"].concat(Object.keys(levels)), type: "string",
|
||||
})
|
||||
.option("dev", {
|
||||
nargs: 0, describe: "Initialize development repository", hidden: true, default: false, type: "boolean",
|
||||
})
|
||||
.option("mode", {
|
||||
nargs: 1, describe: "Operating mode to install",
|
||||
choices: ["standalone", "master", "slave", "ctl", "plugins"],
|
||||
})
|
||||
.option("admin", {
|
||||
nargs: 1, describe: "Admin account name [standalone/master]", type: "string",
|
||||
})
|
||||
.option("slave-name", {
|
||||
nargs: 1, describe: "Slave name [slave]", type: "string",
|
||||
})
|
||||
.option("master-url", {
|
||||
nargs: 1, describe: "Master URL [slave/ctl]", type: "string",
|
||||
})
|
||||
.option("master-token", {
|
||||
nargs: 1, describe: "Master authentication token [slave/ctl]", type: "string",
|
||||
})
|
||||
.option("public-address", {
|
||||
nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/slave]", type: "string",
|
||||
})
|
||||
.option("factorio-dir", {
|
||||
nargs: 1, describe: "Path to Factorio installation [standalone/slave]", type: "string",
|
||||
})
|
||||
.option("plugins", {
|
||||
array: true, describe: "Plugins to install", type: "string",
|
||||
});
|
||||
|
||||
if (process.platform === "linux") {
|
||||
args = args
|
||||
.option("download-headless", {
|
||||
nargs: 0,
|
||||
describe: "(Linux only) Automatically download and unpack the latest factorio release. " +
|
||||
"Can be set to false using --no-download-headless.",
|
||||
type: "boolean",
|
||||
})
|
||||
.conflicts("factorio-dir", "download-headless");
|
||||
}
|
||||
|
||||
args = args.argv;
|
||||
|
||||
setLogLevel(args.logLevel === "none" ? -1 : levels[args.logLevel]);
|
||||
dev = args.dev;
|
||||
|
||||
if (!dev) {
|
||||
await validateInstallDir();
|
||||
}
|
||||
|
||||
let answers = await inquirerMissingArgs(args);
|
||||
if (answers.downloadHeadless) {
|
||||
await downloadLinuxServer();
|
||||
}
|
||||
|
||||
logger.verbose(JSON.stringify(answers));
|
||||
|
||||
if (!dev) {
|
||||
await installClusterio(answers.mode, answers.plugins);
|
||||
}
|
||||
|
||||
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]);
|
||||
adminToken = result.stdout.split("\n").slice(-2)[0];
|
||||
}
|
||||
|
||||
if (answers.mode === "standalone") {
|
||||
logger.info("Setting up slave");
|
||||
await execSlave(["config", "set", "slave.name", "local"]);
|
||||
|
||||
let result = await execSlave(["config", "show", "slave.id"]);
|
||||
let slaveId = 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];
|
||||
|
||||
await execSlave(["config", "set", "slave.master_token", slaveToken]);
|
||||
await execSlave(["config", "set", "slave.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.factorio_directory", answers.factorioDir]);
|
||||
}
|
||||
|
||||
if (!dev && ["standalone", "master", "slave"].includes(answers.mode)) {
|
||||
logger.info("Writing run scripts");
|
||||
await writeScripts(answers.mode);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Successfully installed ${answers.mode}`);
|
||||
if (adminToken) {
|
||||
console.log(`Admin authentication token: ${adminToken}`);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
main().catch(err => {
|
||||
if (err instanceof InstallError) {
|
||||
logger.error(err.message);
|
||||
} else {
|
||||
logger.fatal(`
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured installing clusterio, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable no-console */
|
||||
"use strict";
|
||||
const chalk = require("chalk");
|
||||
|
||||
const levels = Object.freeze({
|
||||
fatal: 0,
|
||||
error: 1,
|
||||
warn: 2,
|
||||
audit: 3,
|
||||
info: 4,
|
||||
verbose: 6,
|
||||
});
|
||||
|
||||
const colors = {
|
||||
fatal: "inverse red",
|
||||
error: "bold redBright",
|
||||
warn: "bold yellowBright",
|
||||
audit: "bold greenBright",
|
||||
info: "bold blueBright",
|
||||
verbose: "bold grey",
|
||||
};
|
||||
|
||||
let maxLogLevel = levels.info;
|
||||
const logger = {};
|
||||
for (let [name, level] of Object.entries(levels)) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
logger[name] = message => {
|
||||
if (level > maxLogLevel) {
|
||||
return;
|
||||
}
|
||||
let style = chalk;
|
||||
for (let part of colors[name].split(" ")) {
|
||||
style = style[part];
|
||||
}
|
||||
if (["fatal", "error"].includes(name)) {
|
||||
console.error(`${style(name)} ${message}`);
|
||||
} else if (["warn"].includes(name)) {
|
||||
console.warn(`${style(name)} ${message}`);
|
||||
} else {
|
||||
console.log(`${style(name)} ${message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
colors,
|
||||
levels,
|
||||
setLogLevel: (level) => { maxLogLevel = level; },
|
||||
logger,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@clusterio/create",
|
||||
"description": "Installer for clusterio",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"main": "create.js",
|
||||
"bin": {
|
||||
"clusteriocreate": "./create.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio",
|
||||
"clusterio"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"inquirer": "^8.1.0",
|
||||
"phin": "^3.5.1",
|
||||
"yargs": "^14.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -88,4 +88,4 @@ Lists up all configuration entries with their currently configured values.
|
||||
|
||||
[Managing a cluster](/docs/managing-a-cluster.md) for the available commands used for managing a cluster.
|
||||
|
||||
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
|
||||
[The Clusterio repository](https://github.com/clusterio/clusterio) for instructions on how to set up a cluster.
|
||||
@@ -819,6 +819,8 @@ logCommands.add(new libCommand.Command({
|
||||
"slave": { describe: "Query log of given slave", 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 },
|
||||
"start": { describe: "Limit from the start instead of the end", nargs: 0, type: "boolean", default: false },
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
@@ -835,8 +837,14 @@ logCommands.add(new libCommand.Command({
|
||||
slave_ids,
|
||||
instance_ids,
|
||||
max_level: args.maxLevel,
|
||||
limit: args.limit,
|
||||
order: args.start ? "asc" : "desc",
|
||||
});
|
||||
|
||||
if (!args.start) {
|
||||
result.log.reverse();
|
||||
}
|
||||
|
||||
let stdoutLogger = winston.createLogger({
|
||||
level: "verbose",
|
||||
levels,
|
||||
@@ -866,13 +874,13 @@ debugCommands.add(new libCommand.Command({
|
||||
* @private
|
||||
*/
|
||||
class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
constructor(url, reconnectDelay, tlsCa, token) {
|
||||
super(url, reconnectDelay, tlsCa);
|
||||
constructor(url, maxReconnectDelay, tlsCa, token) {
|
||||
super(url, maxReconnectDelay, tlsCa);
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
register() {
|
||||
logger.verbose("SOCKET | registering control");
|
||||
logger.verbose("Connector | registering control");
|
||||
this.sendHandshake("register_control", {
|
||||
token: this._token,
|
||||
agent: "clusterioctl",
|
||||
@@ -946,8 +954,6 @@ class Control extends libLink.Link {
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
this.connector.setTimeout(30);
|
||||
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
} catch (err) {
|
||||
@@ -956,7 +962,7 @@ class Control extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
await this.connector.close(1001, "Control Quit");
|
||||
await this.connector.close(1000, "Control Quit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,7 +1126,7 @@ async function startControl() {
|
||||
|
||||
let controlConnector = new ControlConnector(
|
||||
controlConfig.get("control.master_url"),
|
||||
controlConfig.get("control.reconnect_delay"),
|
||||
controlConfig.get("control.max_reconnect_delay"),
|
||||
tlsCa,
|
||||
controlConfig.get("control.master_token")
|
||||
);
|
||||
@@ -1185,10 +1191,10 @@ I version of clusterio. Expect things to break. I
|
||||
startControl().catch(err => {
|
||||
if (!(err instanceof libErrors.StartupError)) {
|
||||
logger.fatal(`
|
||||
+----------------------------------------------------------------+
|
||||
| Unexpected error occured while starting control, please report |
|
||||
| it to https://github.com/clusterio/factorioClusterio/issues |
|
||||
+----------------------------------------------------------------+
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while starting control, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Clusterio command line management tool",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"main": "ctl.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -18,7 +18,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.7",
|
||||
"@clusterio/lib": "^2.0.0-alpha.9",
|
||||
"as-table": "^1.0.31",
|
||||
"fs-extra": "^8.1.0",
|
||||
"phin": "^3.5.1",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for implementing the exponential backoff algorithm
|
||||
*
|
||||
* Provides a convenient interface for implementing an exponential backoff
|
||||
* when reconnecting or resending in a distributed network.
|
||||
*
|
||||
* Once instantiated the delay method will return the next interval to wait
|
||||
* for in milliseconds before making another attempt. For each attempt the
|
||||
* delay chosen is a random value between 0 and the current maximum delay
|
||||
* wwhich starts at a value of 2 * `base` and doubles for ever attempt up to
|
||||
* the passed value of `max`. The value for the delay will reset back to
|
||||
* `base` if more time than the value for `reset` has passed between two
|
||||
* invocations of delay.
|
||||
*
|
||||
* @alias module:lib/ExponentialBackoff
|
||||
*/
|
||||
class ExponentialBackoff {
|
||||
/**
|
||||
* Construct ExponentialBackoff helper
|
||||
*
|
||||
* The options passed may also be modified on the instance at runtime.
|
||||
*
|
||||
* @param {object=} options -
|
||||
* Options for this exponential backoff helper
|
||||
* @param {number=} [options.base=1] -
|
||||
* Base delay in seconds. The first delay will on average be this
|
||||
* long, but can be anywhere between 0 and 2 times this value.
|
||||
* @param {number=} [options.max=60] -
|
||||
* Maximum delay in seconds. A delay will not take more than this
|
||||
* value.
|
||||
* @param {number=} [options.reset=2*max] -
|
||||
* Time in seconds between two invocations of delay which will cause
|
||||
* it to start over with `base` instead of doubling.
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
/**
|
||||
* Base delay in seconds. The first delay will on average be this
|
||||
* long, but can be anywhere between 0 and 2 times this value.
|
||||
* @type {number}
|
||||
*/
|
||||
this.base = options.base || 1;
|
||||
/**
|
||||
* Maximum delay in seconds. A delay will not take more than this
|
||||
* value.
|
||||
* @type {number}
|
||||
*/
|
||||
this.max = options.max || 60;
|
||||
/**
|
||||
* Time in seconds between two invocations of delay which will cause
|
||||
* it to start over with `base` instead of doubling.
|
||||
* @type {number}
|
||||
*/
|
||||
this.reset = options.reset || 2 * this.max;
|
||||
|
||||
this._exp = 0;
|
||||
this._lastInvocationTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the next delay to wait
|
||||
*
|
||||
* Returns the next delay to wait according to the exponential backoff
|
||||
* algorithm. This is a value between 0 and min(2 * options.base *
|
||||
* 2<sup>retries</sup>, optinos.max).
|
||||
*
|
||||
* @returns {number} time in milliseconds to wait.
|
||||
*/
|
||||
delay() {
|
||||
let invocationTime = Date.now();
|
||||
let interval = (invocationTime - this._lastInvocationTime) / 1000;
|
||||
this._lastInvocationTime = invocationTime;
|
||||
|
||||
if (interval > this.reset) {
|
||||
this._exp = 0;
|
||||
}
|
||||
|
||||
this._exp = Math.min(this._exp + 1, Math.log2(this.max));
|
||||
return Math.random() * this.base * 2 ** this._exp * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExponentialBackoff;
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This package contains functionality shared between master, slave, ctl and/or plugins of Clusterio.
|
||||
|
||||
See [the Clusterio repository](https://github.com/clusterio/factorioClusterio) for more information on Clusterio.
|
||||
See [the Clusterio repository](https://github.com/clusterio/clusterio) for more information on Clusterio.
|
||||
@@ -0,0 +1,12 @@
|
||||
export { default as libConfig } from "./config";
|
||||
export { default as libErrors } from "./errors";
|
||||
export { default as libFactorio } from "./factorio/exchange_string";
|
||||
export { default as libHelpers } from "./helpers";
|
||||
export { default as libLink } from "./link";
|
||||
export { default as libLogging } from "./logging";
|
||||
export { default as libPlugin } from "./plugin";
|
||||
export { default as libSchema } from "./schema";
|
||||
export { default as libUsers } from "./users";
|
||||
|
||||
export { default as ExponentialBackoff } from "./ExponentialBackoff";
|
||||
export { default as RateLimiter } from "./RateLimiter";
|
||||
@@ -85,14 +85,14 @@ MasterGroup.define({
|
||||
title: "Heartbeat Interval",
|
||||
description: "Interval heartbeats are sent out on WebSocket connections",
|
||||
type: "number",
|
||||
initial_value: 45,
|
||||
initial_value: 15,
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "connector_shutdown_timeout",
|
||||
title: "Connector Shutdown Timeout",
|
||||
description: "Timeout in seconds for connectors to properly disconnect on shutdown",
|
||||
name: "session_timeout",
|
||||
title: "Session Timeout",
|
||||
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
|
||||
type: "number",
|
||||
initial_value: 30,
|
||||
initial_value: 60,
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "metrics_timeout",
|
||||
@@ -184,11 +184,11 @@ SlaveGroup.define({
|
||||
initial_value: "localhost",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "reconnect_delay",
|
||||
title: "Reconnect Delay",
|
||||
name: "max_reconnect_delay",
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initial_value: 5,
|
||||
initial_value: 60,
|
||||
});
|
||||
SlaveGroup.finalize();
|
||||
|
||||
@@ -358,11 +358,11 @@ ControlGroup.define({
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "reconnect_delay",
|
||||
title: "Reconnect Delay",
|
||||
name: "max_reconnect_delay",
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initial_value: 2,
|
||||
initial_value: 60,
|
||||
});
|
||||
ControlGroup.finalize();
|
||||
|
||||
|
||||
@@ -90,7 +90,16 @@ async function loadFile(server, modVersions, modPath) {
|
||||
}
|
||||
}
|
||||
|
||||
let zip = await loadZip(server, modVersions, mod);
|
||||
let zip;
|
||||
try {
|
||||
zip = await loadZip(server, modVersions, mod);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
let file = zip.file(filePath);
|
||||
if (!file) {
|
||||
return null;
|
||||
@@ -310,7 +319,15 @@ async function exportLocale(server, modVersions, modOrder, languageCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let zip = await loadZip(server, modVersions, mod);
|
||||
let zip;
|
||||
try {
|
||||
zip = await loadZip(server, modVersions, mod);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
for (let file of zip.file(new RegExp(`locale\\/${languageCode}\\/.*\\.cfg`))) {
|
||||
let content = await file.async("nodebuffer");
|
||||
mergeLocale(ini.parse(content.toString("utf8")));
|
||||
@@ -357,6 +374,28 @@ async function exportData(server) {
|
||||
throw new Error("No items got exported");
|
||||
}
|
||||
|
||||
// Some mod authors put leading zeros into the versions of their zip files.
|
||||
let splitter = /^(.*)_(\d+)\.(\d+)\.(\d+)\.zip?$/;
|
||||
for (let entry of await fs.readdir(server.writePath("mods"))) {
|
||||
let match = splitter.exec(entry);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let modVersion = `${match[2]}.${match[3]}.${match[4]}`;
|
||||
let normalizedVersion =
|
||||
`${Number.parseInt(match[2], 10)}.${Number.parseInt(match[3], 10)}.${Number.parseInt(match[4], 10)}`
|
||||
;
|
||||
|
||||
if (modVersion === normalizedVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (modVersions.get(match[1]) === normalizedVersion) {
|
||||
modVersions.set(match[1], modVersion);
|
||||
}
|
||||
}
|
||||
|
||||
let { iconSheet, itemData } = await exportItems(server, modVersions, items);
|
||||
let locale = await exportLocale(server, modVersions, modOrder, "en");
|
||||
|
||||
|
||||
@@ -605,7 +605,11 @@ class FactorioServer extends events.EventEmitter {
|
||||
let type = line.slice(channelEnd + 1, channelEnd + 2).toString("utf-8");
|
||||
let content;
|
||||
if (type === "j") {
|
||||
content = JSON.parse(line.slice(channelEnd + 2).toString("utf-8"));
|
||||
try {
|
||||
content = JSON.parse(line.slice(channelEnd + 2).toString("utf-8"));
|
||||
} catch (err) {
|
||||
throw new Error(`Malformed JSON to ${channel}: ${line.slice(channelEnd + 2).toString("utf-8")}`);
|
||||
}
|
||||
|
||||
} else if (type === "f") {
|
||||
let fileName = line.slice(channelEnd + 2).toString("utf-8");
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
const libBuildMod = require("./build_mod");
|
||||
const libCommand = require("./command");
|
||||
const libConfig = require("./config");
|
||||
const libDatabase = require("./database");
|
||||
const libErrors = require("./errors");
|
||||
const libFactorio = require("./factorio");
|
||||
const libFileOps = require("./file_ops");
|
||||
const libHash = require("./hash");
|
||||
const libHelpers = require("./helpers");
|
||||
const libLink = require("./link");
|
||||
const libLogging = require("./logging");
|
||||
const libLoggingUtils = require("./logging_utils");
|
||||
const libLuaTools = require("./lua_tools");
|
||||
const libPlugin = require("./plugin");
|
||||
const libPluginLoader = require("./plugin_loader");
|
||||
const libPrometheus = require("./prometheus");
|
||||
const libSchema = require("./schema");
|
||||
const libSharedCommands = require("./shared_commands");
|
||||
const libUsers = require("./users");
|
||||
|
||||
const ExponentialBackoff = require("./ExponentialBackoff");
|
||||
const RateLimiter = require("./RateLimiter");
|
||||
|
||||
|
||||
module.exports = {
|
||||
libBuildMod,
|
||||
libCommand,
|
||||
libConfig,
|
||||
libDatabase,
|
||||
libErrors,
|
||||
libFactorio,
|
||||
libFileOps,
|
||||
libHash,
|
||||
libHelpers,
|
||||
libLink,
|
||||
libLogging,
|
||||
libLoggingUtils,
|
||||
libLuaTools,
|
||||
libPlugin,
|
||||
libPluginLoader,
|
||||
libPrometheus,
|
||||
libSchema,
|
||||
libSharedCommands,
|
||||
libUsers,
|
||||
|
||||
ExponentialBackoff,
|
||||
RateLimiter,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const WebSocket = require("isomorphic-ws");
|
||||
const libSchema = require("../schema");
|
||||
const libErrors = require("../errors");
|
||||
const { logger } = require("../logging");
|
||||
const ExponentialBackoff = require("../ExponentialBackoff");
|
||||
|
||||
|
||||
/**
|
||||
@@ -15,9 +16,14 @@ const { logger } = require("../logging");
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class WebSocketBaseConnector extends events.EventEmitter {
|
||||
constructor() {
|
||||
constructor(sessionTimeout) {
|
||||
super();
|
||||
|
||||
this._sessionTimeout = sessionTimeout;
|
||||
|
||||
// One of closed, connecting (client only), connected and resuming.
|
||||
this._state = "closed";
|
||||
this._closing = false;
|
||||
this._socket = null;
|
||||
this._seq = 1;
|
||||
this._lastHeartbeat = null;
|
||||
@@ -27,6 +33,26 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
this._sendBuffer = [];
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._state = "closed";
|
||||
this._closing = false;
|
||||
this._socket = null;
|
||||
this._seq = 1;
|
||||
this._lastHeartbeat = null;
|
||||
this._heartbeatId = null;
|
||||
this._heartbeatInterval = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sendBuffer.length = 0;
|
||||
}
|
||||
|
||||
_check(...expectedStates) {
|
||||
if (!expectedStates.includes(this._state)) {
|
||||
throw new Error(
|
||||
`Expected state ${expectedStates} but state is ${this._state}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_dropSendBufferSeq(seq) {
|
||||
if (seq === null) {
|
||||
return;
|
||||
@@ -44,12 +70,10 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
_doHeartbeat() {
|
||||
this._check(["connected", "closing"]);
|
||||
this._check("connected");
|
||||
if (Date.now() - this._lastHeartbeat > 2000 * this._heartbeatInterval) {
|
||||
logger.verbose("SOCKET | closing after heartbeat timed out");
|
||||
logger.verbose("Connector | closing after heartbeat timed out");
|
||||
this._socket.close(1008, "Heartbeat timeout");
|
||||
clearInterval(this._heartbeatId);
|
||||
this._heartbeatId = null;
|
||||
|
||||
} else {
|
||||
this._socket.send(JSON.stringify({
|
||||
@@ -62,7 +86,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
|
||||
_processHeartbeat(message) {
|
||||
if (!libSchema.heartbeat(message)) {
|
||||
logger.warn("SOCKET | closing after received invalid heartbeat");
|
||||
logger.warn("Connector | closing after received invalid heartbeat");
|
||||
this._socket.close(1002, "Invalid heartbeat");
|
||||
return;
|
||||
}
|
||||
@@ -91,6 +115,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
if (this._heartbeatId) {
|
||||
clearInterval(this._heartbeatId);
|
||||
this._heartbeatId = null;
|
||||
this._lastHeartbeat = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,14 +127,14 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
* @returns {number} the sequence number of the message sent
|
||||
*/
|
||||
send(type, data) {
|
||||
if (!["handshake", "connected", "closing"].includes(this._state)) {
|
||||
if (!["connected", "resuming"].includes(this._state)) {
|
||||
throw new libErrors.SessionLost("No session");
|
||||
}
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
let message = { seq, type, data };
|
||||
this._sendBuffer.push(message);
|
||||
if (["connected", "closing"].includes(this._state)) {
|
||||
if (this._state === "connected") {
|
||||
this._socket.send(JSON.stringify(message));
|
||||
}
|
||||
return seq;
|
||||
@@ -118,11 +143,11 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
/**
|
||||
* Set connection state to closing
|
||||
*
|
||||
* Signal the connection is about tho close and that once the close
|
||||
* Signal the connection is about to close and that once the close
|
||||
* frame is received it should be considered finished.
|
||||
*/
|
||||
setClosing() {
|
||||
this._state = "closing";
|
||||
throw new Error("Abstract function");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +161,15 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
* True if the connection is closing down or closed
|
||||
*/
|
||||
get closing() {
|
||||
return ["closing", "closed"].includes(this._state);
|
||||
return this._closing || this._state === "closed";
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connector currently has a valid session that is either
|
||||
* connected or in the process of resuming.
|
||||
*/
|
||||
get hasSession() {
|
||||
return ["connected", "resuming"].includes(this._state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,41 +180,32 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
constructor(url, reconnectDelay, tlsCa = null) {
|
||||
super();
|
||||
constructor(url, maxReconnectDelay, tlsCa = null) {
|
||||
super(null);
|
||||
|
||||
this._url = url;
|
||||
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
|
||||
this._tlsCa = tlsCa;
|
||||
|
||||
// The following states are used in the client connector
|
||||
// new: Not connected
|
||||
// handshake: Attempting to (re)connect to server.
|
||||
// closed: Not connected
|
||||
// connecting: Attempting to connect to server.
|
||||
// resuming: Attempting to resume an existing connection to the server.
|
||||
// connected: Connection is online
|
||||
// closing: Connection is in the process of being closed.
|
||||
this._state = "new";
|
||||
this._reconnectId = null;
|
||||
this._sessionToken = null;
|
||||
this._connected = false;
|
||||
this._reconnectDelay = reconnectDelay;
|
||||
this._timeout = 15 * 60;
|
||||
this._startedReconnect = null;
|
||||
this._startedResuming = null;
|
||||
}
|
||||
|
||||
_check(expectedStates) {
|
||||
if (!expectedStates.includes(this._state)) {
|
||||
throw new Error(
|
||||
`Expected state ${expectedStates} but state is ${this._state}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(timeout) {
|
||||
this._timeout = timeout;
|
||||
|
||||
if (this._reconnectId && this._timeout < this._reconnectDelay / 2) {
|
||||
clearTimeout(this._reconnectId);
|
||||
this.reconnect();
|
||||
}
|
||||
_reset() {
|
||||
clearTimeout(this._reconnectId);
|
||||
this._reconnectId = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._startedResuming = null;
|
||||
super._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +218,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
* @param {Object} data - Data to send with message.
|
||||
*/
|
||||
sendHandshake(type, data) {
|
||||
this._check(["handshake"]);
|
||||
this._check("connecting", "resuming");
|
||||
this._socket.send(JSON.stringify({ seq: null, type, data }));
|
||||
}
|
||||
|
||||
@@ -207,34 +231,53 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
* @param {string} reason - WebSocket close reason.
|
||||
*/
|
||||
async close(code, reason) {
|
||||
if (this._state === "new") {
|
||||
if (this._state === "closed") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._reconnectId) {
|
||||
clearTimeout(this._reconnectId);
|
||||
this._reconnectId = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._state = "new";
|
||||
this._connected = false;
|
||||
this._closing = true;
|
||||
if (this._state === "connected" || this._socket) {
|
||||
this._socket.close(code, reason);
|
||||
await events.once(this, "close");
|
||||
|
||||
} else {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the connection is expected to close
|
||||
*
|
||||
* Sets the internal flag to signal not to attempt to resume the
|
||||
* connection if the server closes it. This will close the connection
|
||||
* if it's currently attempting to connect or resume.
|
||||
*/
|
||||
setClosing() {
|
||||
if (this._state === "closed") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopHeartbeat();
|
||||
this._state = "closing";
|
||||
this._socket.close(code, reason);
|
||||
await events.once(this, "close");
|
||||
this._closing = true;
|
||||
if (this._state === "connected") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._socket) {
|
||||
this._socket.close(1000, "Connector closing");
|
||||
|
||||
} else {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the master server
|
||||
*/
|
||||
async connect() {
|
||||
this._check(["new"]);
|
||||
this._state = "handshake";
|
||||
this._check("closed");
|
||||
this._state = "connecting";
|
||||
|
||||
this._doConnect();
|
||||
|
||||
@@ -242,12 +285,12 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
await events.once(this, "connect");
|
||||
}
|
||||
|
||||
async _doConnect() {
|
||||
_doConnect() {
|
||||
let url = new URL(this._url);
|
||||
url.pathname += "api/socket";
|
||||
|
||||
// Open WebSocket to master
|
||||
logger.verbose(`SOCKET | connecting to ${url}`);
|
||||
logger.verbose(`Connector | connecting to ${url}`);
|
||||
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
if (process.env.APP_ENV === "browser") {
|
||||
@@ -266,69 +309,89 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
* Attempt re-establishing the connecting again
|
||||
*/
|
||||
reconnect() {
|
||||
if (this._socket) {
|
||||
throw new Error("Cannot reconnect while socket is open");
|
||||
}
|
||||
|
||||
if (this._reconnectId !== null) {
|
||||
return;
|
||||
logger.error("Unexpected double call to reconnect");
|
||||
}
|
||||
|
||||
if (this._startedReconnect === null) {
|
||||
this._startedReconnect = Date.now();
|
||||
let delay = this._backoff.delay();
|
||||
if (
|
||||
this._state === "resuming"
|
||||
&& this._startedResuming + this._sessionTimeout * 1000 < Date.now() + delay
|
||||
) {
|
||||
logger.error("Connector | Session timed out trying to resume");
|
||||
this._reset();
|
||||
this._state = "connecting";
|
||||
this.emit("invalidate");
|
||||
}
|
||||
|
||||
if (this._startedReconnect + this._timeout * 1000 < Date.now()) {
|
||||
logger.error("SOCKET | Timed out trying to reconnect");
|
||||
this._heartbeatInterval = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._state = "new";
|
||||
this._connected = false;
|
||||
this.emit("close");
|
||||
return;
|
||||
}
|
||||
|
||||
let delay = Math.random() * this._reconnectDelay;
|
||||
logger.verbose(
|
||||
`SOCKET | waiting ${delay.toLocaleString("en", { maximumFractionDigits: 2 })} seconds for reconnect`
|
||||
`Connector | waiting ${(Math.round(delay / 10) / 100)} seconds for reconnect`
|
||||
);
|
||||
this._reconnectId = setTimeout(() => {
|
||||
this._reconnectId = null;
|
||||
this._doConnect();
|
||||
}, delay * 1000);
|
||||
}, delay);
|
||||
this._reconnectTime = Date.now() + delay;
|
||||
}
|
||||
|
||||
_attachSocketHandlers() {
|
||||
this._socket.onclose = event => {
|
||||
logger.verbose(`SOCKET | Close (code: ${event.code}, reason: ${event.reason})`);
|
||||
const previousState = this._state;
|
||||
|
||||
// Authentication failed
|
||||
if (event.code === 4003) {
|
||||
this.emit("error", new libErrors.AuthenticationFailed(event.reason));
|
||||
this._state = "closing";
|
||||
this._closing = true;
|
||||
}
|
||||
|
||||
if (this._state === "closing") {
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._state = "new";
|
||||
this._connected = false;
|
||||
this.emit("close");
|
||||
this._socket = null;
|
||||
if (this._state === "connected") {
|
||||
this.stopHeartbeat();
|
||||
if (this._closing) {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
|
||||
} else {
|
||||
this._state = "resuming";
|
||||
this._startedResuming = Date.now();
|
||||
this.reconnect();
|
||||
this.emit("drop");
|
||||
}
|
||||
|
||||
} else {
|
||||
this._state = "handshake";
|
||||
this.reconnect();
|
||||
if (this._connected) {
|
||||
this._connected = false;
|
||||
this.emit("drop");
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (this._closing) {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
|
||||
} else {
|
||||
this.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
this.stopHeartbeat();
|
||||
// Log must be sent after state change is complete
|
||||
let message = `Connector | Close (code: ${event.code}, reason: ${event.reason})`;
|
||||
if (previousState === "connected" && event.code !== 1000) {
|
||||
logger.info(message);
|
||||
} else {
|
||||
logger.verbose(message);
|
||||
}
|
||||
};
|
||||
|
||||
this._socket.onerror = event => {
|
||||
// It's assumed that close is always called by ws
|
||||
let code = !event.error ? "" : `, code: ${event.error.code}`;
|
||||
logger.error(`SOCKET | Error: ${event.message || "unknown error"}${code}`);
|
||||
let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`;
|
||||
if (this._state === "connected") {
|
||||
logger.error(message);
|
||||
} else {
|
||||
// Don't log as error if not actually connected, prevents
|
||||
// log spam during reconnecting attempts.
|
||||
logger.verbose(message);
|
||||
}
|
||||
if (event.error) {
|
||||
// Abort connection if certificate validation failed
|
||||
if ([
|
||||
@@ -337,22 +400,22 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
"ERR_TLS_CERT_ALTNAME_INVALID",
|
||||
].includes(event.error.code)) {
|
||||
this.emit("error", new libErrors.AuthenticationFailed(event.error.message));
|
||||
this._state = "closing";
|
||||
this._closing = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._socket.onopen = () => {
|
||||
logger.verbose("SOCKET | Open");
|
||||
logger.verbose("Connector | Open");
|
||||
};
|
||||
|
||||
// Handle messages
|
||||
this._socket.onmessage = event => {
|
||||
let message = JSON.parse(event.data);
|
||||
if (this._state === "handshake") {
|
||||
if (["connecting", "resuming"].includes(this._state)) {
|
||||
this._processHandshake(message);
|
||||
|
||||
} else if (["connected", "closing"].includes(this._state)) {
|
||||
} else if (this._state === "connected") {
|
||||
if (message.seq !== null) {
|
||||
this._lastReceivedSeq = message.seq;
|
||||
}
|
||||
@@ -372,17 +435,17 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
|
||||
_processHandshake(message) {
|
||||
if (!libSchema.serverHandshake(message)) {
|
||||
logger.verbose("SOCKET | closing after received invalid handshake");
|
||||
logger.warn("Connector | closing after received invalid handshake");
|
||||
this._socket.close(1002, "Invalid handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
let { seq, type, data } = message;
|
||||
if (type === "hello") {
|
||||
logger.verbose(`SOCKET | received hello from master version ${data.version}`);
|
||||
logger.verbose(`Connector | received hello from master version ${data.version}`);
|
||||
this.emit("hello", data);
|
||||
if (this._sessionToken) {
|
||||
logger.verbose("SOCKET | Attempting resume");
|
||||
logger.verbose("Connector | Attempting resume");
|
||||
this.sendHandshake("resume", {
|
||||
session_token: this._sessionToken,
|
||||
last_seq: this._lastReceivedSeq,
|
||||
@@ -392,36 +455,39 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
} else if (type === "ready") {
|
||||
logger.verbose("SOCKET | received ready from master");
|
||||
logger.verbose("Connector | received ready from master");
|
||||
this._state = "connected";
|
||||
this._sessionToken = data.session_token;
|
||||
this._sessionTimeout = data.session_timeout;
|
||||
this._heartbeatInterval = data.heartbeat_interval;
|
||||
this.startHeartbeat();
|
||||
for (let bufferedMessage of this._sendBuffer) {
|
||||
this._socket.send(JSON.stringify(bufferedMessage));
|
||||
}
|
||||
this._startedReconnect = null;
|
||||
this._connected = true;
|
||||
this.emit("connect", data);
|
||||
|
||||
} else if (type === "continue") {
|
||||
logger.verbose("SOCKET | resuming existing session");
|
||||
logger.info("Connector | resuming existing session");
|
||||
this._state = "connected";
|
||||
this._heartbeatInterval = data.heartbeat_interval;
|
||||
this._sessionTimeout = data.session_timeout;
|
||||
this.startHeartbeat();
|
||||
this._dropSendBufferSeq(data.last_seq);
|
||||
for (let bufferedMessage of this._sendBuffer) {
|
||||
this._socket.send(JSON.stringify(bufferedMessage));
|
||||
}
|
||||
this._startedReconnect = null;
|
||||
this._connected = true;
|
||||
this._startedResuming = null;
|
||||
this.emit("resume");
|
||||
|
||||
} else if (type === "invalidate") {
|
||||
this._heartbeatInterval = null;
|
||||
logger.warn("Connector | session invalidated by master");
|
||||
this._state = "connecting";
|
||||
this._seq = 1;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._startedResuming = null;
|
||||
this.emit("invalidate");
|
||||
this.register();
|
||||
}
|
||||
|
||||
@@ -859,6 +859,8 @@ messages.queryLog = new Request({
|
||||
items: { type: "integer" },
|
||||
},
|
||||
"max_level": { type: ["string", "null"] },
|
||||
"limit": { type: "integer" },
|
||||
"order": { enum: ["asc", "desc"] },
|
||||
},
|
||||
responseProperties: {
|
||||
"log": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@clusterio/lib",
|
||||
"description": "Shared library for Clusterio",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"repository": "https://github.com/clusterio/factorioClusterio",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"repository": "https://github.com/clusterio/clusterio",
|
||||
"license": "MIT",
|
||||
"browser": "browser.js",
|
||||
"keywords": [
|
||||
"factorio"
|
||||
],
|
||||
|
||||
@@ -256,6 +256,7 @@ class BaseMasterPlugin {
|
||||
* or received notice from a slave 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.
|
||||
* - `stopped`: Instance is no longer running or was just assigned to a
|
||||
* slave.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
@@ -270,7 +271,8 @@ class BaseMasterPlugin {
|
||||
* 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
|
||||
* is invoked for all those instances. You can detect this situation by
|
||||
* checking if prev equals `unknown`.
|
||||
* checking if prev equals `unknown`. If the slave disconnects the
|
||||
* instances will again get the `unknown` status.
|
||||
*
|
||||
* When instances are created on the master they will notify of a status
|
||||
* change with prev set to null. While the status of new instances is
|
||||
|
||||
@@ -98,9 +98,10 @@ const serverHandshake = ajv.compile({
|
||||
"type": { const: "ready" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["session_token", "heartbeat_interval"],
|
||||
required: ["session_token", "session_timeout", "heartbeat_interval"],
|
||||
properties: {
|
||||
"session_token": { type: "string" },
|
||||
"session_timeout": { type: "number" },
|
||||
"heartbeat_interval": { type: "number" },
|
||||
"account": {
|
||||
additionalProperties: false,
|
||||
@@ -118,9 +119,10 @@ const serverHandshake = ajv.compile({
|
||||
"type": { const: "continue" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["last_seq", "heartbeat_interval"],
|
||||
required: ["last_seq", "session_timeout", "heartbeat_interval"],
|
||||
properties: {
|
||||
"last_seq": { type: ["integer", "null"] },
|
||||
"session_timeout": { type: "number" },
|
||||
"heartbeat_interval": { type: "number" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -236,7 +236,7 @@ class User {
|
||||
* @returns {string} JWT access token for the user.
|
||||
*/
|
||||
createToken(secret) {
|
||||
return jwt.sign({ aud: "user", user: this.name }, secret);
|
||||
return jwt.sign({ aud: "user", user: this.name }, Buffer.from(secret, "base64"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,4 +111,4 @@ Runs the master server.
|
||||
|
||||
## See Also
|
||||
|
||||
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
|
||||
[The Clusterio repository](https://github.com/clusterio/clusterio) for instructions on how to set up a cluster.
|
||||
@@ -138,7 +138,10 @@ async function handleBootstrapCommand(args, masterConfig) {
|
||||
|
||||
} else if (subCommand === "generate-slave-token") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(jwt.sign({ aud: "slave", slave: args.id }, masterConfig.get("master.auth_secret")));
|
||||
console.log(jwt.sign(
|
||||
{ aud: "slave", slave: args.id },
|
||||
Buffer.from(masterConfig.get("master.auth_secret"), "base64")
|
||||
));
|
||||
|
||||
} else if (subCommand === "create-ctl-config") {
|
||||
let admin = userManager.users.get(args.name);
|
||||
@@ -389,10 +392,10 @@ ${err.original.stack}`
|
||||
);
|
||||
} else {
|
||||
logger.fatal(`
|
||||
+---------------------------------------------------------------+
|
||||
| Unexpected error occured while starting master, please report |
|
||||
| it to https://github.com/clusterio/factorioClusterio/issues |
|
||||
+---------------------------------------------------------------+
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while starting master, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Implementation of Clusterio master server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"main": "master.js",
|
||||
"scripts": {
|
||||
"build": "webpack-cli --env production",
|
||||
@@ -19,7 +19,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.7",
|
||||
"@clusterio/lib": "^2.0.0-alpha.9",
|
||||
"busboy": "^0.3.1",
|
||||
"compression": "^1.7.2",
|
||||
"express": "^4.16.2",
|
||||
@@ -33,7 +33,7 @@
|
||||
"yargs": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.7",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@@ -16,6 +16,7 @@ class BaseConnection extends libLink.Link {
|
||||
/** @member {module:master/src/WsServerConnector} module:master/src/BaseConnection#connector */
|
||||
super("master", target, connector);
|
||||
this._master = master;
|
||||
this._disconnecting = false;
|
||||
libLink.attachAllMessages(this);
|
||||
for (let masterPlugin of this._master.plugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, masterPlugin);
|
||||
@@ -57,6 +58,7 @@ class BaseConnection extends libLink.Link {
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
await libPlugin.invokeHook(this._master.plugins, "onPrepareSlaveDisconnect", this);
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
}
|
||||
@@ -72,6 +74,15 @@ class BaseConnection extends libLink.Link {
|
||||
|
||||
await this.connector.close(code, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the link is connected, not in the dropped state and not in
|
||||
* the process of disconnecting.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get connected() {
|
||||
return !this._disconnecting && this.connector.connected;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseConnection;
|
||||
@@ -126,7 +126,10 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
generateSlaveToken(slaveId) {
|
||||
return jwt.sign({ aud: "slave", slave: slaveId }, this._master.config.get("master.auth_secret"));
|
||||
return jwt.sign(
|
||||
{ aud: "slave", slave: slaveId },
|
||||
Buffer.from(this._master.config.get("master.auth_secret"), "base64")
|
||||
);
|
||||
}
|
||||
|
||||
async generateSlaveTokenRequestHandler(message) {
|
||||
@@ -453,12 +456,15 @@ class ControlConnection extends BaseConnection {
|
||||
// - The log file is read starting from the beginning
|
||||
let setDuration = lastQueryLogTime.startTimer();
|
||||
let log = await query({
|
||||
order: "asc",
|
||||
order: message.data.order,
|
||||
limit: Infinity,
|
||||
from: new Date(0),
|
||||
});
|
||||
|
||||
log = log.filter(this.constructor.logFilter(message.data));
|
||||
if (message.data.limit) {
|
||||
log = log.slice(0, message.data.limit);
|
||||
}
|
||||
setDuration();
|
||||
return { log };
|
||||
}
|
||||
|
||||
@@ -39,6 +39,20 @@ class SlaveConnection extends BaseConnection {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "unknown";
|
||||
this._master.instanceUpdated(instance);
|
||||
libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const WebSocket = require("ws");
|
||||
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
const libPrometheus = require("@clusterio/lib/prometheus");
|
||||
const libSchema = require("@clusterio/lib/schema");
|
||||
@@ -64,12 +65,10 @@ class WsServer {
|
||||
|
||||
let disconnectTasks = [];
|
||||
for (let controlConnection of this.controlConnections) {
|
||||
controlConnection.connector.setTimeout(this.master.config.get("master.connector_shutdown_timeout"));
|
||||
disconnectTasks.push(controlConnection.disconnect(1001, "Server Quit"));
|
||||
}
|
||||
|
||||
for (let slaveConnection of this.slaveConnections.values()) {
|
||||
slaveConnection.connector.setTimeout(this.master.config.get("master.connector_shutdown_timeout"));
|
||||
disconnectTasks.push(slaveConnection.disconnect(1001, "Server Quit"));
|
||||
}
|
||||
|
||||
@@ -148,10 +147,10 @@ class WsServer {
|
||||
message, socket, req
|
||||
).catch(err => {
|
||||
logger.error(`
|
||||
+----------------------------------------------------------------+
|
||||
| Unexpected error occured in WebSocket handshake, please report |
|
||||
| it to https://github.com/clusterio/factorioClusterio/issues |
|
||||
+----------------------------------------------------------------+
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured in WebSocket handshake, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
@@ -184,7 +183,7 @@ ${err.stack}`
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
data.session_token,
|
||||
this.master.config.get("master.auth_secret"),
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: this.sessionAud }
|
||||
);
|
||||
|
||||
@@ -216,7 +215,7 @@ ${err.stack}`
|
||||
if (type === "register_slave") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
this.master.config.get("master.auth_secret"),
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "slave" }
|
||||
);
|
||||
|
||||
@@ -227,7 +226,7 @@ ${err.stack}`
|
||||
} else if (type === "register_control") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
this.master.config.get("master.auth_secret"),
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
@@ -251,13 +250,15 @@ ${err.stack}`
|
||||
let sessionId = this.nextSessionId;
|
||||
this.nextSessionId += 1;
|
||||
let sessionToken = jwt.sign(
|
||||
{ aud: this.sessionAud, sid: sessionId }, this.master.config.get("master.auth_secret")
|
||||
{ aud: this.sessionAud, sid: sessionId },
|
||||
Buffer.from(this.master.config.get("master.auth_secret"), "base64"),
|
||||
);
|
||||
let sessionTimeout = this.master.config.get("master.session_timeout");
|
||||
let heartbeatInterval = this.master.config.get("master.heartbeat_interval");
|
||||
let connector = new WsServerConnector(socket, sessionId, heartbeatInterval);
|
||||
let connector = new WsServerConnector(sessionId, sessionTimeout, heartbeatInterval);
|
||||
this.activeConnectors.set(sessionId, connector);
|
||||
connector.on("close", () => {
|
||||
this.activeConnectors.delete(connector);
|
||||
this.activeConnectors.delete(sessionId);
|
||||
});
|
||||
|
||||
let additionalReadyData = {};
|
||||
@@ -275,6 +276,8 @@ ${err.stack}`
|
||||
if (this.slaveConnections.get(data.id) === connection) {
|
||||
this.slaveConnections.delete(data.id);
|
||||
this.master.slaveUpdated(this.master.slaves.get(data.id));
|
||||
} else {
|
||||
logger.warn("Unlisted SlaveConnection closed");
|
||||
}
|
||||
});
|
||||
this.slaveConnections.set(data.id, connection);
|
||||
@@ -285,9 +288,11 @@ ${err.stack}`
|
||||
logger.verbose(`WsServer | registered control from ${req.socket.remoteAddress}`);
|
||||
let connection = new ControlConnection(data, connector, this.master, user);
|
||||
connector.on("close", () => {
|
||||
let index = this.controlConnections.indexOf(this);
|
||||
let index = this.controlConnections.indexOf(connection);
|
||||
if (index !== -1) {
|
||||
this.controlConnections.splice(index, 1);
|
||||
} else {
|
||||
logger.warn("Unlisted ControlConnection closed");
|
||||
}
|
||||
});
|
||||
this.controlConnections.push(connection);
|
||||
@@ -296,7 +301,7 @@ ${err.stack}`
|
||||
};
|
||||
}
|
||||
|
||||
connector.ready(sessionToken, additionalReadyData);
|
||||
connector.ready(socket, sessionToken, additionalReadyData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,28 +12,24 @@ const { logger } = require("@clusterio/lib/logging");
|
||||
* @alias module:master/src/WsServerConnector
|
||||
*/
|
||||
class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
constructor(socket, sessionId, heartbeatInterval) {
|
||||
super();
|
||||
constructor(sessionId, sessionTimeout, heartbeatInterval) {
|
||||
super(sessionTimeout);
|
||||
|
||||
this._socket = socket;
|
||||
this._socket = null;
|
||||
this._sessionId = sessionId;
|
||||
this._heartbeatInterval = heartbeatInterval;
|
||||
this._timeoutId = null;
|
||||
|
||||
// The following states are used in the server connector
|
||||
// handshake: Waiting for client to (re)connect.
|
||||
// closed: Connection is closed
|
||||
// connected: Connection is online
|
||||
// closing: Connection is in the process of being closed.
|
||||
// closed: Connection has been closed
|
||||
this._state = "handshake";
|
||||
this._timeout = 15 * 60;
|
||||
// resuming: Waiting for client to resume.
|
||||
}
|
||||
|
||||
_check(expectedStates) {
|
||||
if (!expectedStates.includes(this._state)) {
|
||||
throw new Error(
|
||||
`Expected state ${expectedStates} but state is ${this._state}`
|
||||
);
|
||||
}
|
||||
_reset() {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = null;
|
||||
super._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,17 +37,20 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
*
|
||||
* Sends the ready message over the socket to initiate the session.
|
||||
*
|
||||
* @param {Object} socket - WebSocket connection to client.
|
||||
* @param {string} sessionToken -
|
||||
* the session token to send to the client.
|
||||
* @param {Object=} additionalData -
|
||||
* extra properties to send along the ready message.
|
||||
*/
|
||||
ready(sessionToken, additionalData) {
|
||||
ready(socket, sessionToken, additionalData) {
|
||||
this._socket = socket;
|
||||
this._socket.send(JSON.stringify({
|
||||
seq: null,
|
||||
type: "ready",
|
||||
data: {
|
||||
session_token: sessionToken,
|
||||
session_timeout: this._sessionTimeout,
|
||||
heartbeat_interval: this._heartbeatInterval,
|
||||
...additionalData,
|
||||
},
|
||||
@@ -77,7 +76,7 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
// stale connection. If this is the case the connector will have to
|
||||
// wait for the previous socket to close down before it can continue
|
||||
// from a new socket, or there will be overlapping events from both.
|
||||
if (["connected", "closing"].includes(this._state)) {
|
||||
if (this._state === "connected") {
|
||||
this._socket.close(4003, "Session Hijacked");
|
||||
|
||||
// Correctly waiting for the connector to close and then
|
||||
@@ -102,6 +101,7 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
type: "continue",
|
||||
data: {
|
||||
last_seq: this._lastReceivedSeq,
|
||||
session_timeout: this._sessionTimeout,
|
||||
heartbeat_interval: this._heartbeatInterval,
|
||||
},
|
||||
}));
|
||||
@@ -115,63 +115,44 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
this.emit("resume");
|
||||
}
|
||||
|
||||
setTimeout(timeout) {
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId);
|
||||
}
|
||||
|
||||
this._timeoutId = setTimeout(() => { this._timedOut(); }, timeout * 1000);
|
||||
this._timeout = timeout;
|
||||
}
|
||||
|
||||
_timedOut() {
|
||||
logger.verbose("SOCKET | Connection timed out");
|
||||
this._timeoutId = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._state = "closed";
|
||||
logger.verbose("Connector | Connection timed out");
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
this.emit("invalidate");
|
||||
}
|
||||
|
||||
_attachSocketHandlers() {
|
||||
this.startHeartbeat();
|
||||
|
||||
this._socket.on("close", (code, reason) => {
|
||||
logger.verbose(`SOCKET | Close (code: ${code}, reason: ${reason})`);
|
||||
if (this._state === "closing") {
|
||||
this._lastReceivedSeq = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._state = "closed";
|
||||
logger.verbose(`Connector | Close (code: ${code}, reason: ${reason})`);
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this._closing) {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = null;
|
||||
}
|
||||
|
||||
} else {
|
||||
this._state = "handshake";
|
||||
this._state = "resuming";
|
||||
this.emit("drop");
|
||||
this._timeoutId = setTimeout(() => { this._timedOut(); }, this._timeout * 1000);
|
||||
this._timeoutId = setTimeout(() => { this._timedOut(); }, this._sessionTimeout * 1000);
|
||||
}
|
||||
|
||||
this.stopHeartbeat();
|
||||
});
|
||||
|
||||
this._socket.on("error", err => {
|
||||
// It's assumed that close is always called by ws
|
||||
logger.verbose("SOCKET | Error:", err);
|
||||
logger.verbose("Connector | Error:", err);
|
||||
});
|
||||
|
||||
this._socket.on("open", () => {
|
||||
logger.verbose("SOCKET | Open");
|
||||
logger.verbose("Connector | Open");
|
||||
});
|
||||
this._socket.on("ping", data => {
|
||||
logger.verbose(`SOCKET | Ping (data: ${data}`);
|
||||
logger.verbose(`Connector | Ping (data: ${data}`);
|
||||
});
|
||||
this._socket.on("pong", data => {
|
||||
logger.verbose(`SOCKET | Pong (data: ${data}`);
|
||||
logger.verbose(`Connector | Pong (data: ${data}`);
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
@@ -208,11 +189,34 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopHeartbeat();
|
||||
this._state = "closing";
|
||||
if (this._state === "resuming") {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
return;
|
||||
}
|
||||
|
||||
this._closing = true;
|
||||
this._socket.close(code, reason);
|
||||
await events.once(this, "close");
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the connection is expected to close
|
||||
*
|
||||
* Sets the internal flag to signal not to wait for the client to
|
||||
* reconnect and resume the connection if the client closes it. This
|
||||
* will close the connection if it's currently waiting for the client to
|
||||
* resume it.
|
||||
*/
|
||||
setClosing() {
|
||||
if (this._state === "resuming") {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
return;
|
||||
}
|
||||
|
||||
this._closing = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WsServerConnector;
|
||||
@@ -101,7 +101,11 @@ function validateSlaveToken(req, res, next) {
|
||||
}
|
||||
|
||||
try {
|
||||
jwt.verify(token, req.app.locals.master.config.get("master.auth_secret"), { audience: "slave" });
|
||||
jwt.verify(
|
||||
token,
|
||||
Buffer.from(req.app.locals.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "slave" }
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
res.sendStatus(401);
|
||||
@@ -121,7 +125,7 @@ function validateUserToken(req, res, next) {
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
token,
|
||||
req.app.locals.master.config.get("master.auth_secret"),
|
||||
Buffer.from(req.app.locals.master.config.get("master.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
let user = req.app.locals.master.userManager.users.get(tokenPayload.user);
|
||||
@@ -259,6 +263,11 @@ async function getStream(req, res) {
|
||||
}
|
||||
|
||||
|
||||
const zipMimes = [
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
];
|
||||
|
||||
async function uploadSave(req, res) {
|
||||
try {
|
||||
res.locals.user.checkPermission("core.instance.save.upload");
|
||||
@@ -319,7 +328,7 @@ async function uploadSave(req, res) {
|
||||
requestErrors.push("instance_id must come before files uploaded");
|
||||
}
|
||||
|
||||
if (fileMime !== "application/zip") {
|
||||
if (!zipMimes.includes(fileMime)) {
|
||||
requestErrors.push("invalid file Content-Type");
|
||||
}
|
||||
|
||||
@@ -350,7 +359,7 @@ async function uploadSave(req, res) {
|
||||
req.pipe(busboy);
|
||||
});
|
||||
|
||||
} else if (mimeType === "application/zip") {
|
||||
} else if (zipMimes.includes(mimeType)) {
|
||||
let filename = req.query.filename;
|
||||
if (typeof filename !== "string") {
|
||||
requestErrors.push("Missing or invalid filename parameter");
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2a2a2a" />
|
||||
<title>Clusterio</title>
|
||||
<script>
|
||||
window.webRoot = new URL("__WEB_ROOT__", document.location).pathname;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script src="__WEB_ROOT__bundle.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2a2a2a" />
|
||||
<title>Clusterio</title>
|
||||
<script>
|
||||
window.webRoot = new URL("__WEB_ROOT__", document.location).pathname;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script src="__WEB_ROOT__bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,14 +20,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "master",
|
||||
shared: {
|
||||
"@clusterio/lib/config": { singleton: true },
|
||||
"@clusterio/lib/errors": { singleton: true },
|
||||
"@clusterio/lib/helpers": { singleton: true },
|
||||
"@clusterio/lib/link": { singleton: true },
|
||||
"@clusterio/lib/logging": { singleton: true },
|
||||
"@clusterio/lib/plugin": { singleton: true },
|
||||
"@clusterio/lib/schema": { singleton: true },
|
||||
"@clusterio/lib/users": { singleton: true },
|
||||
"@clusterio/lib": { singleton: true },
|
||||
"@clusterio/web_ui": { singleton: true },
|
||||
"antd": { singleton: true },
|
||||
"react": { singleton: true },
|
||||
|
||||
@@ -90,4 +90,4 @@ Runs the slave.
|
||||
|
||||
## See Also
|
||||
|
||||
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
|
||||
[The Clusterio repository](https://github.com/clusterio/clusterio) for instructions on how to set up a cluster.
|
||||
@@ -4,63 +4,63 @@ local api = {}
|
||||
|
||||
local initialized = false
|
||||
function api.init()
|
||||
if initialized then
|
||||
return
|
||||
end
|
||||
initialized = true
|
||||
if initialized then
|
||||
return
|
||||
end
|
||||
initialized = true
|
||||
|
||||
if remote.interfaces.clusterio_api then
|
||||
api.events = remote.call("clusterio_api", "get_events")
|
||||
else
|
||||
local null_event = script.generate_event_name()
|
||||
api.events = {
|
||||
on_instance_updated = null_event,
|
||||
on_server_startup = null_event,
|
||||
}
|
||||
end
|
||||
if remote.interfaces.clusterio_api then
|
||||
api.events = remote.call("clusterio_api", "get_events")
|
||||
else
|
||||
local null_event = script.generate_event_name()
|
||||
api.events = {
|
||||
on_instance_updated = null_event,
|
||||
on_server_startup = null_event,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function call_api(fn, ...)
|
||||
if remote.interfaces.clusterio_api and remote.interfaces.clusterio_api[fn] then
|
||||
return remote.call("clusterio_api", fn, ...)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
if remote.interfaces.clusterio_api and remote.interfaces.clusterio_api[fn] then
|
||||
return remote.call("clusterio_api", fn, ...)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function api.get_instance_name()
|
||||
return call_api("get_instance_name")
|
||||
return call_api("get_instance_name")
|
||||
end
|
||||
|
||||
function api.get_instance_id()
|
||||
return call_api("get_instance_id")
|
||||
return call_api("get_instance_id")
|
||||
end
|
||||
|
||||
|
||||
function api.send_json(channel, data)
|
||||
if not remote.interfaces.clusterio_api then
|
||||
return
|
||||
end
|
||||
if not remote.interfaces.clusterio_api then
|
||||
return
|
||||
end
|
||||
|
||||
-- Escape bad characters. The question mark is used for separating the
|
||||
-- channel name from the payload.
|
||||
channel = channel:gsub("([\x00-\x1f?\\])", function(match)
|
||||
return "\\x" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
-- Escape bad characters. The question mark is used for separating the
|
||||
-- channel name from the payload.
|
||||
channel = channel:gsub("([\x00-\x1f?\\])", function(match)
|
||||
return "\\x" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
|
||||
data = game.table_to_json(data)
|
||||
data = game.table_to_json(data)
|
||||
|
||||
-- If there's more than about 4kB of data users running with the Windows
|
||||
-- console open will start to experience stuttering, otherwise we could
|
||||
-- output about 1 MB of data at a time through stdout.
|
||||
if #data < 4000 then
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
else
|
||||
local file_no = remote.call("clusterio_api", "get_file_no")
|
||||
local file_name = "clst_" .. file_no .. ".json"
|
||||
game.write_file(file_name, data, false, 0)
|
||||
print("\f$ipc:" .. channel .. "?f" .. file_name)
|
||||
end
|
||||
-- If there's more than about 4kB of data users running with the Windows
|
||||
-- console open will start to experience stuttering, otherwise we could
|
||||
-- output about 1 MB of data at a time through stdout.
|
||||
if #data < 4000 then
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
else
|
||||
local file_no = remote.call("clusterio_api", "get_file_no")
|
||||
local file_name = "clst_" .. file_no .. ".json"
|
||||
game.write_file(file_name, data, false, 0)
|
||||
print("\f$ipc:" .. channel .. "?f" .. file_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "clusterio_lib",
|
||||
"variants": [
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"factorio_version": "0.17"
|
||||
},
|
||||
{
|
||||
"version": "0.1.1",
|
||||
"factorio_version": "0.18"
|
||||
},
|
||||
{
|
||||
"version": "0.1.2",
|
||||
"factorio_version": "1.1"
|
||||
}
|
||||
],
|
||||
"title": "Clusterio Library (Alpha)",
|
||||
"author": "Clusterio Team",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio",
|
||||
"description": "Library for interfacing with Clusterio. Warning: work in progress alpha.",
|
||||
"additional_files": {
|
||||
"serialize.lua": ["..", "..", "modules", "clusterio", "serialize.lua"]
|
||||
}
|
||||
"name": "clusterio_lib",
|
||||
"variants": [
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"factorio_version": "0.17"
|
||||
},
|
||||
{
|
||||
"version": "0.1.1",
|
||||
"factorio_version": "0.18"
|
||||
},
|
||||
{
|
||||
"version": "0.1.2",
|
||||
"factorio_version": "1.1"
|
||||
}
|
||||
],
|
||||
"title": "Clusterio Library (Alpha)",
|
||||
"author": "Clusterio Team",
|
||||
"homepage": "https://github.com/clusterio/clusterio",
|
||||
"description": "Library for interfacing with Clusterio. Warning: work in progress alpha.",
|
||||
"additional_files": {
|
||||
"serialize.lua": ["..", "..", "modules", "clusterio", "serialize.lua"]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
local function send_json(channel, data)
|
||||
data = game.table_to_json(data)
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
data = game.table_to_json(data)
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
end
|
||||
|
||||
script.on_init(function()
|
||||
send_json("mod_list", game.active_mods)
|
||||
send_json("mod_list", game.active_mods)
|
||||
end)
|
||||
@@ -1,101 +1,104 @@
|
||||
local item_types = {
|
||||
-- Item categories
|
||||
"item",
|
||||
"ammo",
|
||||
"capsule",
|
||||
"gun",
|
||||
"item-with-entity-data",
|
||||
"item-with-label",
|
||||
"item-with-inventory",
|
||||
"blueprint-book",
|
||||
"item-with-tags",
|
||||
"selection-tool",
|
||||
"blueprint",
|
||||
"deconstruction-item",
|
||||
"copy-paste-tool",
|
||||
"upgrade-item",
|
||||
"module",
|
||||
"rail-planner",
|
||||
"tool",
|
||||
"armor",
|
||||
"mining-tool",
|
||||
"repair-tool",
|
||||
-- Item categories
|
||||
"item",
|
||||
"ammo",
|
||||
"capsule",
|
||||
"gun",
|
||||
"item-with-entity-data",
|
||||
"item-with-label",
|
||||
"item-with-inventory",
|
||||
"blueprint-book",
|
||||
"item-with-tags",
|
||||
"selection-tool",
|
||||
"blueprint",
|
||||
"deconstruction-item",
|
||||
"copy-paste-tool",
|
||||
"upgrade-item",
|
||||
"module",
|
||||
"rail-planner",
|
||||
"tool",
|
||||
"armor",
|
||||
"mining-tool",
|
||||
"repair-tool",
|
||||
|
||||
-- Fluids
|
||||
"fluid",
|
||||
-- Fluids
|
||||
"fluid",
|
||||
}
|
||||
|
||||
local function is_array(t)
|
||||
local count = 0
|
||||
for k in pairs(t) do
|
||||
if type(k) ~= "number" then
|
||||
return false
|
||||
end
|
||||
count = count + 1
|
||||
end
|
||||
local count = 0
|
||||
for k in pairs(t) do
|
||||
if type(k) ~= "number" then
|
||||
return false
|
||||
end
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
for i = 1, count do
|
||||
if not t[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for i = 1, count do
|
||||
if not t[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return count ~= 0
|
||||
return count ~= 0
|
||||
end
|
||||
|
||||
-- no game.table_to_json here :(
|
||||
local function table_to_json(data)
|
||||
if type(data) == "number" then
|
||||
return string.format("%.17g", data)
|
||||
if type(data) == "number" then
|
||||
if data == math.huge then return "1e500" end -- Inf is not representable in JSON, but 1e500 will overflow to it.
|
||||
if data == -math.huge then return "-1e500" end
|
||||
if data ~= data then return "0" end -- NaN is not representable in JSON, treat as 0 instead.
|
||||
return string.format("%.17g", data)
|
||||
|
||||
elseif type(data) == "string" then
|
||||
data = data:gsub('([\x00-\x1f\\"])', function(match)
|
||||
return "\\x" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
return '"' .. data .. '"'
|
||||
elseif type(data) == "string" then
|
||||
data = data:gsub('([\x00-\x1f\\"])', function(match)
|
||||
return "\\u00" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
return '"' .. data .. '"'
|
||||
|
||||
elseif data == nil then
|
||||
return "null"
|
||||
elseif data == nil then
|
||||
return "null"
|
||||
|
||||
elseif data == true then
|
||||
return "true"
|
||||
elseif data == false then
|
||||
return "false"
|
||||
elseif data == true then
|
||||
return "true"
|
||||
elseif data == false then
|
||||
return "false"
|
||||
|
||||
elseif type(data) == "table" then
|
||||
local r
|
||||
if is_array(data) then
|
||||
r = {"["}
|
||||
for i=1, #data do
|
||||
r[#r+1] = table_to_json(data[i])
|
||||
r[#r+1] = ","
|
||||
end
|
||||
elseif type(data) == "table" then
|
||||
local r
|
||||
if is_array(data) then
|
||||
r = {"["}
|
||||
for i=1, #data do
|
||||
r[#r+1] = table_to_json(data[i])
|
||||
r[#r+1] = ","
|
||||
end
|
||||
|
||||
-- overwrite last comma, empty table is an object
|
||||
r[#r] = "]"
|
||||
-- overwrite last comma, empty table is an object
|
||||
r[#r] = "]"
|
||||
|
||||
else
|
||||
r = {"{"}
|
||||
for k, v in pairs(data) do
|
||||
r[#r+1] = table_to_json(tostring(k))
|
||||
r[#r+1] = ":"
|
||||
r[#r+1] = table_to_json(v)
|
||||
r[#r+1] = ","
|
||||
end
|
||||
r[#r + (r[#r] == "," and 0 or 1)] = "}"
|
||||
end
|
||||
else
|
||||
r = {"{"}
|
||||
for k, v in pairs(data) do
|
||||
r[#r+1] = table_to_json(tostring(k))
|
||||
r[#r+1] = ":"
|
||||
r[#r+1] = table_to_json(v)
|
||||
r[#r+1] = ","
|
||||
end
|
||||
r[#r + (r[#r] == "," and 0 or 1)] = "}"
|
||||
end
|
||||
|
||||
return table.concat(r)
|
||||
end
|
||||
return table.concat(r)
|
||||
end
|
||||
end
|
||||
|
||||
local function send_json(channel, data)
|
||||
data = table_to_json(data)
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
data = table_to_json(data)
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
end
|
||||
|
||||
for _, group in ipairs(item_types) do
|
||||
for _, item in pairs(data.raw[group]) do
|
||||
send_json("item_export", item)
|
||||
end
|
||||
for _, item in pairs(data.raw[group]) do
|
||||
send_json("item_export", item)
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "export",
|
||||
"version": "0.0.0",
|
||||
"title": "Icon exporter",
|
||||
"author": "Clusterio Team",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio",
|
||||
"description": "Exports item prototypes on stdout during the data stage."
|
||||
"name": "export",
|
||||
"version": "0.0.0",
|
||||
"title": "Icon exporter",
|
||||
"author": "Clusterio Team",
|
||||
"homepage": "https://github.com/clusterio/clusterio",
|
||||
"description": "Exports item prototypes on stdout during the data stage."
|
||||
}
|
||||
@@ -3,39 +3,39 @@ local api = {}
|
||||
|
||||
|
||||
api.events = {
|
||||
on_instance_updated = script.generate_event_name(),
|
||||
on_server_startup = script.generate_event_name(),
|
||||
on_instance_updated = script.generate_event_name(),
|
||||
on_server_startup = script.generate_event_name(),
|
||||
}
|
||||
|
||||
function api.get_instance_name()
|
||||
return global.clusterio.instance_name
|
||||
return global.clusterio.instance_name
|
||||
end
|
||||
|
||||
function api.get_instance_id()
|
||||
return global.clusterio.instance_id
|
||||
return global.clusterio.instance_id
|
||||
end
|
||||
|
||||
function api.send_json(channel, data)
|
||||
|
||||
-- Escape bad characters. The question mark is used for separating the
|
||||
-- channel name from the payload.
|
||||
channel = channel:gsub("([\x00-\x1f?\\])", function(match)
|
||||
return "\\x" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
-- Escape bad characters. The question mark is used for separating the
|
||||
-- channel name from the payload.
|
||||
channel = channel:gsub("([\x00-\x1f?\\])", function(match)
|
||||
return "\\x" .. string.format("%02x", match:byte())
|
||||
end)
|
||||
|
||||
data = game.table_to_json(data)
|
||||
data = game.table_to_json(data)
|
||||
|
||||
-- If there's more than about 4kB of data users running with the Windows
|
||||
-- console open will start to experience stuttering, otherwise we could
|
||||
-- output about 1 MB of data at a time through stdout.
|
||||
if #data < 4000 then
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
else
|
||||
global.clusterio_file_no = (global.clusterio_file_no or 0) + 1
|
||||
local file_name = "clst_" .. global.clusterio_file_no .. ".json"
|
||||
game.write_file(file_name, data, false, 0)
|
||||
print("\f$ipc:" .. channel .. "?f" .. file_name)
|
||||
end
|
||||
-- If there's more than about 4kB of data users running with the Windows
|
||||
-- console open will start to experience stuttering, otherwise we could
|
||||
-- output about 1 MB of data at a time through stdout.
|
||||
if #data < 4000 then
|
||||
print("\f$ipc:" .. channel .. "?j" .. data)
|
||||
else
|
||||
global.clusterio_file_no = (global.clusterio_file_no or 0) + 1
|
||||
local file_name = "clst_" .. global.clusterio_file_no .. ".json"
|
||||
game.write_file(file_name, data, false, 0)
|
||||
print("\f$ipc:" .. channel .. "?f" .. file_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -5,69 +5,69 @@ local impl = {}
|
||||
|
||||
impl.events = {}
|
||||
impl.events[defines.events.on_tick] = function()
|
||||
if global.clusterio_patch_number ~= clusterio_patch_number then
|
||||
global.clusterio_patch_number = clusterio_patch_number
|
||||
script.raise_event(api.events.on_server_startup, {})
|
||||
end
|
||||
if global.clusterio_patch_number ~= clusterio_patch_number then
|
||||
global.clusterio_patch_number = clusterio_patch_number
|
||||
script.raise_event(api.events.on_server_startup, {})
|
||||
end
|
||||
end
|
||||
|
||||
impl.events[api.events.on_server_startup] = function()
|
||||
if not global.clusterio then
|
||||
global.clusterio = {
|
||||
instance_id = nil,
|
||||
instance_name = nil,
|
||||
}
|
||||
end
|
||||
if not global.clusterio then
|
||||
global.clusterio = {
|
||||
instance_id = nil,
|
||||
instance_name = nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- Replay will just desync after the save is patched
|
||||
game.disable_replay()
|
||||
-- Replay will just desync after the save is patched
|
||||
game.disable_replay()
|
||||
end
|
||||
|
||||
impl.events[defines.events.on_player_joined_game] = function(event)
|
||||
local player = game.players[event.player_index]
|
||||
api.send_json("player_event", { type = "join", name = player.name })
|
||||
local player = game.players[event.player_index]
|
||||
api.send_json("player_event", { type = "join", name = player.name })
|
||||
end
|
||||
|
||||
impl.events[defines.events.on_player_left_game] = function(event)
|
||||
local player = game.players[event.player_index]
|
||||
api.send_json("player_event", { type = "leave", name = player.name })
|
||||
local player = game.players[event.player_index]
|
||||
api.send_json("player_event", { type = "leave", name = player.name })
|
||||
end
|
||||
|
||||
-- Internal API
|
||||
clusterio_private = {}
|
||||
function clusterio_private.update_instance(new_id, new_name)
|
||||
global.clusterio.instance_id = new_id
|
||||
global.clusterio.instance_name = new_name
|
||||
script.raise_event(api.events.on_instance_updated, {
|
||||
instance_id = new_id,
|
||||
instance_name = new_name,
|
||||
})
|
||||
global.clusterio.instance_id = new_id
|
||||
global.clusterio.instance_name = new_name
|
||||
script.raise_event(api.events.on_instance_updated, {
|
||||
instance_id = new_id,
|
||||
instance_name = new_name,
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
-- This is not part of the add_remote_interface callback to ensure it is
|
||||
-- available when the clusterio_lib mod is loaded. The reason this is
|
||||
-- available when the clusterio_lib mod is loaded. The reason this is
|
||||
-- neccessary is that on_init for newly added mods happen before on_load
|
||||
-- for existing mods, and the add_remote_interface callback is done in
|
||||
-- on_load. See https://forums.factorio.com/viewtopic.php?f=25&t=81552 for
|
||||
-- more details.
|
||||
remote.add_interface('clusterio_api', {
|
||||
get_events = function()
|
||||
return api.events
|
||||
end,
|
||||
get_events = function()
|
||||
return api.events
|
||||
end,
|
||||
|
||||
get_instance_id = function()
|
||||
return global.clusterio.instance_id
|
||||
end,
|
||||
get_instance_id = function()
|
||||
return global.clusterio.instance_id
|
||||
end,
|
||||
|
||||
get_instance_name = function()
|
||||
return global.clusterio.instance_name
|
||||
end,
|
||||
get_instance_name = function()
|
||||
return global.clusterio.instance_name
|
||||
end,
|
||||
|
||||
get_file_no = function()
|
||||
global.clusterio_file_no = (global.clusterio_file_no or 0) + 1
|
||||
return global.clusterio_file_no
|
||||
end,
|
||||
get_file_no = function()
|
||||
global.clusterio_file_no = (global.clusterio_file_no or 0) + 1
|
||||
return global.clusterio_file_no
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clusterio",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {},
|
||||
"load": ["impl.lua"]
|
||||
"name": "clusterio",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {},
|
||||
"load": ["impl.lua"]
|
||||
}
|
||||
@@ -3,38 +3,38 @@
|
||||
local serialize = {}
|
||||
|
||||
local function version_to_table(version)
|
||||
local t = {}
|
||||
for p in string.gmatch(version, "%d+") do
|
||||
t[#t + 1] = tonumber(p)
|
||||
end
|
||||
return t
|
||||
local t = {}
|
||||
for p in string.gmatch(version, "%d+") do
|
||||
t[#t + 1] = tonumber(p)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- 0.17 compatibility
|
||||
local supports_bar, get_bar, has_bar, version
|
||||
local supports_bar, get_bar, set_bar, version
|
||||
if (pcall(function() local mods = script.active_mods end)) then
|
||||
supports_bar = "supports_bar"
|
||||
get_bar = "get_bar"
|
||||
set_bar = "set_bar"
|
||||
version = version_to_table(script.active_mods.base)
|
||||
supports_bar = "supports_bar"
|
||||
get_bar = "get_bar"
|
||||
set_bar = "set_bar"
|
||||
version = version_to_table(script.active_mods.base)
|
||||
else
|
||||
supports_bar = "hasbar"
|
||||
get_bar = "getbar"
|
||||
set_bar = "setbar"
|
||||
version = version_to_table("0.17.69")
|
||||
supports_bar = "hasbar"
|
||||
get_bar = "getbar"
|
||||
set_bar = "setbar"
|
||||
version = version_to_table("0.17.69")
|
||||
end
|
||||
|
||||
-- returns true if the game version is greater than or equal to the given version
|
||||
local function version_ge(comp)
|
||||
comp = version_to_table(comp)
|
||||
for i=1, 3 do
|
||||
if comp[i] > version[i] then
|
||||
return false
|
||||
elseif comp[i] < version[i] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return true
|
||||
comp = version_to_table(comp)
|
||||
for i=1, 3 do
|
||||
if comp[i] > version[i] then
|
||||
return false
|
||||
elseif comp[i] < version[i] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local has_create_grid = version_ge("1.1.7")
|
||||
@@ -42,265 +42,265 @@ local has_create_grid = version_ge("1.1.7")
|
||||
|
||||
-- Equipment Grids are serialized into an array of equipment entries
|
||||
-- where ench entry is a table with the following fields:
|
||||
-- n: name
|
||||
-- p: position (array of 2 numbers corresponding to x and y)
|
||||
-- s: shield (optional)
|
||||
-- e: energy (optional)
|
||||
-- n: name
|
||||
-- p: position (array of 2 numbers corresponding to x and y)
|
||||
-- s: shield (optional)
|
||||
-- e: energy (optional)
|
||||
-- If the equipment is a burner the following is also present:
|
||||
-- i: burner inventory
|
||||
-- r: result inventory
|
||||
-- b: curently burning (optional)
|
||||
-- f: remaining_burning_fuel (optional)
|
||||
-- i: burner inventory
|
||||
-- r: result inventory
|
||||
-- b: curently burning (optional)
|
||||
-- f: remaining_burning_fuel (optional)
|
||||
function serialize.serialize_equipment_grid(grid)
|
||||
local serialized = {}
|
||||
local processed = {}
|
||||
for y = 0, grid.height - 1 do
|
||||
for x = 0, grid.width - 1 do
|
||||
local equipment = grid.get({x, y})
|
||||
if equipment ~= nil then
|
||||
local pos = equipment.position
|
||||
local combined_pos = pos.x + pos.y * grid.width + 1
|
||||
if not processed[combined_pos] then
|
||||
processed[combined_pos] = true
|
||||
local entry = {
|
||||
n = equipment.name,
|
||||
p = {pos.x, pos.y},
|
||||
}
|
||||
if equipment.shield > 0 then entry.s = equipment.shield end
|
||||
if equipment.energy > 0 then entry.e = equipment.energy end
|
||||
-- TODO: Test with Industrial Revolution
|
||||
if equipment.burner then
|
||||
local burner = equipment.burner
|
||||
entry.i = serialize.serialize_inventory(burner.inventory)
|
||||
entry.r = serialize.serialize_inventory(burner.burnt_result_inventory)
|
||||
if burner.curently_burning then
|
||||
entry.b = {}
|
||||
serialize.serialize_item_stack(burner.curently_burning, entry.b)
|
||||
entry.f = burner.remaining_burning_fuel
|
||||
end
|
||||
end
|
||||
table.insert(serialized, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return serialized
|
||||
local serialized = {}
|
||||
local processed = {}
|
||||
for y = 0, grid.height - 1 do
|
||||
for x = 0, grid.width - 1 do
|
||||
local equipment = grid.get({x, y})
|
||||
if equipment ~= nil then
|
||||
local pos = equipment.position
|
||||
local combined_pos = pos.x + pos.y * grid.width + 1
|
||||
if not processed[combined_pos] then
|
||||
processed[combined_pos] = true
|
||||
local entry = {
|
||||
n = equipment.name,
|
||||
p = {pos.x, pos.y},
|
||||
}
|
||||
if equipment.shield > 0 then entry.s = equipment.shield end
|
||||
if equipment.energy > 0 then entry.e = equipment.energy end
|
||||
-- TODO: Test with Industrial Revolution
|
||||
if equipment.burner then
|
||||
local burner = equipment.burner
|
||||
entry.i = serialize.serialize_inventory(burner.inventory)
|
||||
entry.r = serialize.serialize_inventory(burner.burnt_result_inventory)
|
||||
if burner.curently_burning then
|
||||
entry.b = {}
|
||||
serialize.serialize_item_stack(burner.curently_burning, entry.b)
|
||||
entry.f = burner.remaining_burning_fuel
|
||||
end
|
||||
end
|
||||
table.insert(serialized, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return serialized
|
||||
end
|
||||
|
||||
function serialize.deserialize_equipment_grid(grid, serialized)
|
||||
grid.clear()
|
||||
for _, entry in ipairs(serialized) do
|
||||
local equipment = grid.put({
|
||||
name = entry.n,
|
||||
position = entry.p,
|
||||
})
|
||||
if equipment then
|
||||
if entry.s then equipment.shield = entry.s end
|
||||
if entry.e then equipment.energy = entry.e end
|
||||
if entry.i then
|
||||
if entry.b then serialize.deserialize_item_stack(burner.currently_burning, entry.b) end
|
||||
if entry.f then burner.remaining_burning_fuel = entry.f end
|
||||
serialize.deserialize_inventory(burner.burnt_result_inventory, entry.r)
|
||||
serialize.deserialize_inventory(burner.inventory, entry.i)
|
||||
end
|
||||
end
|
||||
end
|
||||
grid.clear()
|
||||
for _, entry in ipairs(serialized) do
|
||||
local equipment = grid.put({
|
||||
name = entry.n,
|
||||
position = entry.p,
|
||||
})
|
||||
if equipment then
|
||||
if entry.s then equipment.shield = entry.s end
|
||||
if entry.e then equipment.energy = entry.e end
|
||||
if entry.i then
|
||||
if entry.b then serialize.deserialize_item_stack(burner.currently_burning, entry.b) end
|
||||
if entry.f then burner.remaining_burning_fuel = entry.f end
|
||||
serialize.deserialize_inventory(burner.burnt_result_inventory, entry.r)
|
||||
serialize.deserialize_inventory(burner.inventory, entry.i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Item stacks are serialized into a table with the following fields:
|
||||
-- n: name
|
||||
-- c: count
|
||||
-- h: health (optional)
|
||||
-- d: durability (optional)
|
||||
-- a: ammo count (optional)
|
||||
-- l: label (optional)
|
||||
-- g: equipment grid (optional)
|
||||
-- i: item inventory (optional)
|
||||
-- n: name
|
||||
-- c: count
|
||||
-- h: health (optional)
|
||||
-- d: durability (optional)
|
||||
-- a: ammo count (optional)
|
||||
-- l: label (optional)
|
||||
-- g: equipment grid (optional)
|
||||
-- i: item inventory (optional)
|
||||
-- If the item stack is exportable it has the following property instead
|
||||
-- e: export string
|
||||
-- e: export string
|
||||
-- Label is a table with the following fields:
|
||||
-- t: label text (optional)
|
||||
-- c: color (optional)
|
||||
-- a: allow manual label change
|
||||
-- t: label text (optional)
|
||||
-- c: color (optional)
|
||||
-- a: allow manual label change
|
||||
function serialize.serialize_item_stack(slot, entry)
|
||||
if
|
||||
slot.is_blueprint
|
||||
or slot.is_blueprint_book
|
||||
or slot.is_upgrade_item
|
||||
or slot.is_deconstruction_item
|
||||
or slot.is_item_with_tags
|
||||
then
|
||||
local call_success, call_return = pcall(slot.export_stack)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown exporting '" .. slot.name .. "'")
|
||||
else
|
||||
entry.e = call_return
|
||||
end
|
||||
if
|
||||
slot.is_blueprint
|
||||
or slot.is_blueprint_book
|
||||
or slot.is_upgrade_item
|
||||
or slot.is_deconstruction_item
|
||||
or slot.is_item_with_tags
|
||||
then
|
||||
local call_success, call_return = pcall(slot.export_stack)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown exporting '" .. slot.name .. "'")
|
||||
else
|
||||
entry.e = call_return
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
entry.n = slot.name
|
||||
entry.c = slot.count
|
||||
if slot.health < 1 then entry.h = slot.health end
|
||||
if slot.durability then entry.d = slot.durability end
|
||||
if slot.type == "ammo" then entry.a = slot.ammo end
|
||||
if slot.is_item_with_label then
|
||||
local label = {}
|
||||
if slot.label then label.t = slot.label end
|
||||
if slot.label_color then label.c = slot.label_color end
|
||||
label.a = slot.allow_manual_label_change
|
||||
entry.l = label
|
||||
end
|
||||
entry.n = slot.name
|
||||
entry.c = slot.count
|
||||
if slot.health < 1 then entry.h = slot.health end
|
||||
if slot.durability then entry.d = slot.durability end
|
||||
if slot.type == "ammo" then entry.a = slot.ammo end
|
||||
if slot.is_item_with_label then
|
||||
local label = {}
|
||||
if slot.label then label.t = slot.label end
|
||||
if slot.label_color then label.c = slot.label_color end
|
||||
label.a = slot.allow_manual_label_change
|
||||
entry.l = label
|
||||
end
|
||||
|
||||
if slot.grid then
|
||||
entry.g = serialize.serialize_equipment_grid(slot.grid)
|
||||
end
|
||||
if slot.grid then
|
||||
entry.g = serialize.serialize_equipment_grid(slot.grid)
|
||||
end
|
||||
|
||||
if slot.is_item_with_inventory then
|
||||
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
|
||||
entry.i = serialize.serialize_inventory(sub_inventory)
|
||||
end
|
||||
if slot.is_item_with_inventory then
|
||||
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
|
||||
entry.i = serialize.serialize_inventory(sub_inventory)
|
||||
end
|
||||
end
|
||||
|
||||
function serialize.deserialize_item_stack(slot, entry)
|
||||
if entry.e then
|
||||
local success = slot.import_stack(entry.e)
|
||||
if success == 1 then
|
||||
print("Error: import of '" .. entry.e .. "' succeeded with errors")
|
||||
elseif success == -1 then
|
||||
print("Error: import of '" .. entry.e .. "' failed")
|
||||
end
|
||||
if entry.e then
|
||||
local success = slot.import_stack(entry.e)
|
||||
if success == 1 then
|
||||
print("Error: import of '" .. entry.e .. "' succeeded with errors")
|
||||
elseif success == -1 then
|
||||
print("Error: import of '" .. entry.e .. "' failed")
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local item_stack = {
|
||||
name = entry.n,
|
||||
count = entry.c,
|
||||
}
|
||||
if entry.h then item_stack.health = entry.h end
|
||||
if entry.d then item_stack.durability = entry.d end
|
||||
if entry.a then item_stack.ammo = entry.a end
|
||||
local item_stack = {
|
||||
name = entry.n,
|
||||
count = entry.c,
|
||||
}
|
||||
if entry.h then item_stack.health = entry.h end
|
||||
if entry.d then item_stack.durability = entry.d end
|
||||
if entry.a then item_stack.ammo = entry.a end
|
||||
|
||||
local call_success, call_return = pcall(slot.set_stack, item_stack)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown setting stack ".. serpent.line(entry))
|
||||
local call_success, call_return = pcall(slot.set_stack, item_stack)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown setting stack ".. serpent.line(entry))
|
||||
|
||||
elseif not call_return then
|
||||
print("Error: Failed to set stack " .. serpent.line(entry))
|
||||
elseif not call_return then
|
||||
print("Error: Failed to set stack " .. serpent.line(entry))
|
||||
|
||||
else
|
||||
if entry.l then
|
||||
-- TODO test this with AAI's unit-remote-control
|
||||
local label = entry.l
|
||||
if label.t then slot.label = label.t end
|
||||
if label.c then slot.label_color = label.c end
|
||||
slot.allow_manual_label_change = label.a
|
||||
end
|
||||
if entry.g then
|
||||
if slot.grid then
|
||||
serialize.deserialize_equipment_grid(slot.grid, entry.g)
|
||||
elseif slot.type == "item-with-entity-data" and has_create_grid then
|
||||
slot.create_grid()
|
||||
serialize.deserialize_equipment_grid(slot.grid, entry.g)
|
||||
else
|
||||
print("Error: Attempt to deserialize equipment grid on an unsupported entity")
|
||||
end
|
||||
end
|
||||
if entry.i then
|
||||
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
|
||||
serialize.deserialize_inventory(sub_inventory, entry.i)
|
||||
end
|
||||
end
|
||||
else
|
||||
if entry.l then
|
||||
-- TODO test this with AAI's unit-remote-control
|
||||
local label = entry.l
|
||||
if label.t then slot.label = label.t end
|
||||
if label.c then slot.label_color = label.c end
|
||||
slot.allow_manual_label_change = label.a
|
||||
end
|
||||
if entry.g then
|
||||
if slot.grid then
|
||||
serialize.deserialize_equipment_grid(slot.grid, entry.g)
|
||||
elseif slot.type == "item-with-entity-data" and has_create_grid then
|
||||
slot.create_grid()
|
||||
serialize.deserialize_equipment_grid(slot.grid, entry.g)
|
||||
else
|
||||
print("Error: Attempt to deserialize equipment grid on an unsupported entity")
|
||||
end
|
||||
end
|
||||
if entry.i then
|
||||
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
|
||||
serialize.deserialize_inventory(sub_inventory, entry.i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Inventories are serialized into a table with the following fields:
|
||||
-- i: array of item stack or exportable item entries
|
||||
-- b: bar position (optional)
|
||||
-- i: array of item stack or exportable item entries
|
||||
-- b: bar position (optional)
|
||||
-- Each item entry has the following fields
|
||||
-- s: index (optional, equals to previous plus one if not present)
|
||||
-- r: repeat count (optional)
|
||||
-- f: slot filter (optional)
|
||||
-- s: index (optional, equals to previous plus one if not present)
|
||||
-- r: repeat count (optional)
|
||||
-- f: slot filter (optional)
|
||||
-- Pluss all the fields for item stacks (see deserialize_item_stack)
|
||||
-- It's also possible that the slot is empty but has a slot filter.
|
||||
function serialize.serialize_inventory(inventory)
|
||||
local serialized = {}
|
||||
if inventory[supports_bar]() and inventory[get_bar]() <= #inventory then
|
||||
serialized.b = inventory[get_bar]()
|
||||
end
|
||||
local serialized = {}
|
||||
if inventory[supports_bar]() and inventory[get_bar]() <= #inventory then
|
||||
serialized.b = inventory[get_bar]()
|
||||
end
|
||||
|
||||
serialized.i = {}
|
||||
local previous_index = 0
|
||||
local previous_serialized = nil
|
||||
for i = 1, #inventory do
|
||||
local item = {}
|
||||
local slot = inventory[i]
|
||||
if inventory.supports_filters() then
|
||||
item.f = inventory.get_filter(i)
|
||||
end
|
||||
serialized.i = {}
|
||||
local previous_index = 0
|
||||
local previous_serialized = nil
|
||||
for i = 1, #inventory do
|
||||
local item = {}
|
||||
local slot = inventory[i]
|
||||
if inventory.supports_filters() then
|
||||
item.f = inventory.get_filter(i)
|
||||
end
|
||||
|
||||
if slot.valid_for_read then
|
||||
serialize.serialize_item_stack(slot, item)
|
||||
end
|
||||
if slot.valid_for_read then
|
||||
serialize.serialize_item_stack(slot, item)
|
||||
end
|
||||
|
||||
if item.n or item.f or item.e then
|
||||
local item_serialized = game.table_to_json(item)
|
||||
if item_serialized == previous_serialized then
|
||||
local previous_item = serialized.i[#serialized.i]
|
||||
previous_item.r = (previous_item.r or 0) + 1
|
||||
previous_index = i
|
||||
if item.n or item.f or item.e then
|
||||
local item_serialized = game.table_to_json(item)
|
||||
if item_serialized == previous_serialized then
|
||||
local previous_item = serialized.i[#serialized.i]
|
||||
previous_item.r = (previous_item.r or 0) + 1
|
||||
previous_index = i
|
||||
|
||||
else
|
||||
if i ~= previous_index + 1 then
|
||||
item.s = i
|
||||
end
|
||||
else
|
||||
if i ~= previous_index + 1 then
|
||||
item.s = i
|
||||
end
|
||||
|
||||
previous_index = i
|
||||
previous_serialized = item_serialized
|
||||
table.insert(serialized.i, item)
|
||||
end
|
||||
previous_index = i
|
||||
previous_serialized = item_serialized
|
||||
table.insert(serialized.i, item)
|
||||
end
|
||||
|
||||
else
|
||||
-- Either an empty slot or serilization failed
|
||||
previous_index = 0
|
||||
previous_serialized = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Either an empty slot or serilization failed
|
||||
previous_index = 0
|
||||
previous_serialized = nil
|
||||
end
|
||||
end
|
||||
|
||||
return serialized
|
||||
return serialized
|
||||
end
|
||||
|
||||
function serialize.deserialize_inventory(inventory, serialized)
|
||||
if serialized.b and inventory[supports_bar]() then
|
||||
inventory[set_bar](serialized.b)
|
||||
end
|
||||
if serialized.b and inventory[supports_bar]() then
|
||||
inventory[set_bar](serialized.b)
|
||||
end
|
||||
|
||||
local last_slot_index = 0
|
||||
for _, entry in ipairs(serialized.i) do
|
||||
local base_index = entry.s or last_slot_index + 1
|
||||
local last_slot_index = 0
|
||||
for _, entry in ipairs(serialized.i) do
|
||||
local base_index = entry.s or last_slot_index + 1
|
||||
|
||||
local repeat_count = entry.r or 0
|
||||
for offset = 0, repeat_count do
|
||||
-- XXX what if the inventory is smaller on this instance?
|
||||
local index = base_index + offset
|
||||
local slot = inventory[index]
|
||||
if entry.f then
|
||||
local call_success, call_return = pcall(inventory.set_filter, index, entry.f)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown setting filter " .. entry.f)
|
||||
local repeat_count = entry.r or 0
|
||||
for offset = 0, repeat_count do
|
||||
-- XXX what if the inventory is smaller on this instance?
|
||||
local index = base_index + offset
|
||||
local slot = inventory[index]
|
||||
if entry.f then
|
||||
local call_success, call_return = pcall(inventory.set_filter, index, entry.f)
|
||||
if not call_success then
|
||||
print("Error: '" .. call_return .. "' thrown setting filter " .. entry.f)
|
||||
|
||||
elseif not call_return then
|
||||
print("Error: Failed to set filter " .. entry.f)
|
||||
end
|
||||
end
|
||||
elseif not call_return then
|
||||
print("Error: Failed to set filter " .. entry.f)
|
||||
end
|
||||
end
|
||||
|
||||
if entry.n or entry.e then
|
||||
serialize.deserialize_item_stack(slot, entry)
|
||||
end
|
||||
end
|
||||
last_slot_index = base_index + repeat_count
|
||||
end
|
||||
if entry.n or entry.e then
|
||||
serialize.deserialize_item_stack(slot, entry)
|
||||
end
|
||||
end
|
||||
last_slot_index = base_index + repeat_count
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Implementation of Clusterio slave server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"main": "slave.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -18,7 +18,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.7",
|
||||
"@clusterio/lib": "^2.0.0-alpha.9",
|
||||
"fs-extra": "^8.1.0",
|
||||
"phin": "^3.5.1",
|
||||
"pidusage": "^2.0.18",
|
||||
|
||||
@@ -168,8 +168,8 @@ class Instance extends libLink.Link {
|
||||
maxConcurrentCommands: this.config.get("factorio.max_concurrent_commands"),
|
||||
};
|
||||
|
||||
// Valid statuses are stopped, starting, running, stopping, creating_save and exporting_data.
|
||||
this._status = "stopped";
|
||||
this._running = false;
|
||||
this._loadedSave = null;
|
||||
this.server = new libFactorio.FactorioServer(
|
||||
factorioDir, this._dir, serverOptions
|
||||
@@ -276,7 +276,6 @@ class Instance extends libLink.Link {
|
||||
}
|
||||
|
||||
notifyExit() {
|
||||
this._running = false;
|
||||
this._loadedSave = null;
|
||||
this.notifyStatus("stopped");
|
||||
|
||||
@@ -296,10 +295,6 @@ class Instance extends libLink.Link {
|
||||
for (let pluginInstance of this.plugins.values()) {
|
||||
pluginInstance.onExit();
|
||||
}
|
||||
|
||||
this.sendSaveListUpdate().catch(err => {
|
||||
this.logger.error(`Unexpected error updating save list:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
|
||||
async _loadPlugin(pluginInfo, slave) {
|
||||
@@ -319,6 +314,7 @@ class Instance extends libLink.Link {
|
||||
await this.server.init();
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -336,6 +332,7 @@ class Instance extends libLink.Link {
|
||||
await this._loadPlugin(pluginInfo, this._slave);
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -577,7 +574,6 @@ class Instance extends libLink.Link {
|
||||
await this.sendSaveListUpdate();
|
||||
await libPlugin.invokeHook(this.plugins, "onStart");
|
||||
|
||||
this._running = true;
|
||||
this.notifyStatus("running");
|
||||
}
|
||||
|
||||
@@ -602,7 +598,6 @@ class Instance extends libLink.Link {
|
||||
|
||||
await libPlugin.invokeHook(this.plugins, "onStart");
|
||||
|
||||
this._running = true;
|
||||
this.notifyStatus("running");
|
||||
}
|
||||
|
||||
@@ -683,13 +678,16 @@ class Instance extends libLink.Link {
|
||||
* Stop the instance
|
||||
*/
|
||||
async stop() {
|
||||
this._running = false;
|
||||
if (this._status === "stopped") {
|
||||
return;
|
||||
}
|
||||
this.notifyStatus("stopping");
|
||||
|
||||
// XXX this needs more thought to it
|
||||
if (this.server._state === "running") {
|
||||
await libPlugin.invokeHook(this.plugins, "onStop");
|
||||
await this.server.stop();
|
||||
await this.sendSaveListUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,7 +701,7 @@ class Instance extends libLink.Link {
|
||||
|
||||
async getMetricsRequestHandler() {
|
||||
let results = [];
|
||||
if (this._running) {
|
||||
if (!["stopped", "stopping"].includes(this._status)) {
|
||||
let pluginResults = await libPlugin.invokeHook(this.plugins, "onMetrics");
|
||||
for (let metricIterator of pluginResults) {
|
||||
for await (let metric of metricIterator) {
|
||||
@@ -729,6 +727,7 @@ class Instance extends libLink.Link {
|
||||
saveName = await this.prepareSave(saveName);
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -750,6 +749,7 @@ class Instance extends libLink.Link {
|
||||
await this.prepare();
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -780,12 +780,14 @@ class Instance extends libLink.Link {
|
||||
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.server.on("exit", () => this.notifyExit());
|
||||
let { name, seed, map_gen_settings, map_settings } = message.data;
|
||||
await this.server.create(name, seed, map_gen_settings, map_settings);
|
||||
await this.sendSaveListUpdate();
|
||||
this.logger.info("Successfully created save");
|
||||
}
|
||||
|
||||
@@ -943,6 +945,10 @@ class InstanceConnection extends libLink.Link {
|
||||
}
|
||||
|
||||
async forwardEventToMaster(message, event) {
|
||||
if (!this.slave.connector.hasSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.send(this.slave, message.data);
|
||||
}
|
||||
|
||||
@@ -973,7 +979,7 @@ class SlaveConnector extends libLink.WebSocketClientConnector {
|
||||
constructor(slaveConfig, tlsCa, pluginInfos) {
|
||||
super(
|
||||
slaveConfig.get("slave.master_url"),
|
||||
slaveConfig.get("slave.reconnect_delay"),
|
||||
slaveConfig.get("slave.max_reconnect_delay"),
|
||||
tlsCa
|
||||
);
|
||||
this.slaveConfig = slaveConfig;
|
||||
@@ -981,7 +987,7 @@ class SlaveConnector extends libLink.WebSocketClientConnector {
|
||||
}
|
||||
|
||||
register() {
|
||||
logger.info("SOCKET | registering slave");
|
||||
logger.info("Connector | registering slave");
|
||||
let plugins = {};
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
plugins[pluginInfo.name] = pluginInfo.version;
|
||||
@@ -1583,10 +1589,10 @@ class Slave extends libLink.Link {
|
||||
}
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
this._disconnecting = true;
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
await libLink.messages.prepareMasterDisconnect.send(instanceConnection);
|
||||
}
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
}
|
||||
@@ -1599,7 +1605,6 @@ class Slave extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
this._shuttingDown = true;
|
||||
this.connector.setTimeout(30);
|
||||
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
@@ -1613,23 +1618,32 @@ class Slave extends libLink.Link {
|
||||
for (let instanceId of this.instanceConnections.keys()) {
|
||||
await this.stopInstance(instanceId);
|
||||
}
|
||||
await this.connector.close(1001, "Slave Shutdown");
|
||||
await this.connector.close(1000, "Slave Shutdown");
|
||||
|
||||
// Clear silly interval in pidfile library.
|
||||
pidusage.clear();
|
||||
} catch (err) {
|
||||
setBlocking(true);
|
||||
logger.error(`
|
||||
+--------------------------------------------------------------------+
|
||||
| Unexpected error occured while shutting down slave, please report |
|
||||
| it to https://github.com/clusterio/factorioClusterio/issues |
|
||||
+--------------------------------------------------------------------+
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while shutting down slave, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
// eslint-disable-next-line node/no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connection to the master is connected, not in the dropped
|
||||
* state,and not in the process of disconnecting.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get connected() {
|
||||
return !this._disconnecting && this.connector.connected;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1863,6 +1877,7 @@ async function startSlave() {
|
||||
});
|
||||
|
||||
await slaveConnector.connect();
|
||||
logger.info("Started slave");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -1905,10 +1920,10 @@ ${err.original.stack}`
|
||||
|
||||
} else {
|
||||
logger.fatal(`
|
||||
+--------------------------------------------------------------+
|
||||
| Unexpected error occured while starting slave, please report |
|
||||
| it to https://github.com/clusterio/factorioClusterio/issues |
|
||||
+--------------------------------------------------------------+
|
||||
+------------------------------------------------------------+
|
||||
| Unexpected error occured while starting slave, please |
|
||||
| report it to https://github.com/clusterio/clusterio/issues |
|
||||
+------------------------------------------------------------+
|
||||
${err.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Clusterio web interface implementation",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"browser": "src/index.jsx",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -18,7 +18,7 @@
|
||||
"@ant-design/icons": "^4.2.2",
|
||||
"@babel/core": "^7.13.8",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@clusterio/lib": "^2.0.0-alpha.7",
|
||||
"@clusterio/lib": "^2.0.0-alpha.9",
|
||||
"antd": "^4.16.0",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
||||
@@ -3,9 +3,8 @@ import "./index.css";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import libConfig from "@clusterio/lib/config";
|
||||
import libPlugin from "@clusterio/lib/plugin";
|
||||
import { ConsoleTransport, WebConsoleFormat, logger } from "@clusterio/lib/logging";
|
||||
import { libConfig, libLogging, libPlugin } from "@clusterio/lib";
|
||||
const { ConsoleTransport, WebConsoleFormat, logger } = libLogging;
|
||||
|
||||
import App from "./components/App";
|
||||
import { Control, ControlConnector } from "./util/websocket";
|
||||
@@ -86,8 +85,7 @@ export default async function bootstrap() {
|
||||
let wsUrl = new URL(window.webRoot, document.location);
|
||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
|
||||
|
||||
let controlConnector = new ControlConnector(wsUrl, 10);
|
||||
controlConnector.setTimeout(15);
|
||||
let controlConnector = new ControlConnector(wsUrl, 120);
|
||||
let control = new Control(controlConnector, plugins);
|
||||
|
||||
ReactDOM.render(<App control={control}/>, document.getElementById("root"));
|
||||
|
||||
@@ -2,7 +2,7 @@ import events from "events";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
import { libLogging } from "@clusterio/lib";
|
||||
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
import SiteLayout from "./SiteLayout";
|
||||
@@ -12,6 +12,7 @@ import LoginForm from "./LoginForm";
|
||||
import { Card, Spin, Typography } from "antd";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
function ErrorCard(props) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Form, Modal, Select, Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
|
||||
@@ -190,7 +190,7 @@ export default function BaseConfigTree(props) {
|
||||
request = props.setProp(fieldName, prop, value);
|
||||
|
||||
} else {
|
||||
request = props.setField(field, value);
|
||||
request = props.setField(field, value === null ? "" : String(value));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Col, Form, Input, Modal, Row, Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { readMapExchangeString } from "@clusterio/lib/factorio/exchange_string";
|
||||
import { libFactorio, libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -23,7 +22,7 @@ export default function CreateSaveModal(props) {
|
||||
if (values.exchangeString && values.exchangeString.trim()) {
|
||||
let result;
|
||||
try {
|
||||
result = readMapExchangeString(values.exchangeString);
|
||||
result = libFactorio.readMapExchangeString(values.exchangeString);
|
||||
} catch (err) {
|
||||
form.setFields([{ name: "exchangeString", errors: [err.message] }]);
|
||||
return;
|
||||
@@ -72,7 +71,7 @@ export default function CreateSaveModal(props) {
|
||||
let exchangeString = form.getFieldValue("exchangeString");
|
||||
let result;
|
||||
try {
|
||||
result = readMapExchangeString(exchangeString);
|
||||
result = libFactorio.readMapExchangeString(exchangeString);
|
||||
} catch (err) {
|
||||
form.setFields([{ name: "exchangeString", errors: [err.message] }]);
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import libConfig from "@clusterio/lib/config";
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
|
||||
import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -22,7 +21,7 @@ export default function InstanceConfigTree(props) {
|
||||
await libLink.messages.setInstanceConfigField.send(control, {
|
||||
instance_id: props.id,
|
||||
field,
|
||||
value: String(value),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useContext, useRef, useState } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
@@ -65,8 +65,10 @@ export default function InstanceConsole(props) {
|
||||
slave_ids: [],
|
||||
instance_ids: [props.id],
|
||||
max_level: null,
|
||||
limit: 400,
|
||||
order: "desc",
|
||||
}).then(result => {
|
||||
setPastLines(result.log.slice(-400).map((info, index) => formatLog(info, index)));
|
||||
setPastLines(result.log.map((info, index) => formatLog(info, index)).reverse());
|
||||
}).catch(err => {
|
||||
setPastLines([<span key={0}>{`Error loading log: ${err.message}`}<br/></span>]);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Input, Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory, useParams } from "react-router-dom";
|
||||
import { Button, Descriptions, Popconfirm, Space, Spin, Typography } from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useContext, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import libConfig from "@clusterio/lib/config";
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Col, Form, Input, Modal, Row, Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { readMapExchangeString } from "@clusterio/lib/factorio/exchange_string";
|
||||
import { libFactorio, libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -23,7 +22,7 @@ export default function LoadScenarioModal(props) {
|
||||
if (values.exchangeString && values.exchangeString.trim()) {
|
||||
let result;
|
||||
try {
|
||||
result = readMapExchangeString(values.exchangeString);
|
||||
result = libFactorio.readMapExchangeString(values.exchangeString);
|
||||
} catch (err) {
|
||||
form.setFields([{ name: "exchangeString", errors: [err.message] }]);
|
||||
return;
|
||||
@@ -71,7 +70,7 @@ export default function LoadScenarioModal(props) {
|
||||
let exchangeString = form.getFieldValue("exchangeString");
|
||||
let result;
|
||||
try {
|
||||
result = readMapExchangeString(exchangeString);
|
||||
result = libFactorio.readMapExchangeString(exchangeString);
|
||||
} catch (err) {
|
||||
form.setFields([{ name: "exchangeString", errors: [err.message] }]);
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import libConfig from "@clusterio/lib/config";
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
|
||||
import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useHistory, useParams } from "react-router-dom";
|
||||
import { Button, Checkbox, Col, Form, Input, Popconfirm, Row, Space, Spin } from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import libUsers from "@clusterio/lib/users";
|
||||
import { libLink, libUsers } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import DataTable from "./data-table";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Popconfirm, Space, Table, Tooltip, Upload } from "antd";
|
||||
import { Button, List, Popconfirm, Progress, Space, Table, Tooltip, Upload } from "antd";
|
||||
import CaretLeftOutlined from "@ant-design/icons/CaretLeftOutlined";
|
||||
import LeftOutlined from "@ant-design/icons/LeftOutlined";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import CreateSaveModal from "./CreateSaveModal";
|
||||
@@ -27,7 +27,9 @@ export default function SavesList(props) {
|
||||
let control = useContext(ControlContext);
|
||||
let saves = useSaves(props.instance.id);
|
||||
let [starting, setStarting] = useState(false);
|
||||
let [uploadingFiles, setUploadingFiles] = useState([]);
|
||||
|
||||
let slaveOffline = ["unassigned", "unknown"].includes(props.instance.status);
|
||||
const saveTable = <Table
|
||||
size="small"
|
||||
columns={[
|
||||
@@ -78,6 +80,7 @@ export default function SavesList(props) {
|
||||
}}
|
||||
>Load save</Button>
|
||||
<Button
|
||||
disabled={slaveOffline}
|
||||
onClick={() => {
|
||||
libLink.messages.downloadSave.send(
|
||||
control, { instance_id: props.instance.id, save: save.name }
|
||||
@@ -101,13 +104,46 @@ export default function SavesList(props) {
|
||||
).catch(notifyErrorHandler("Error deleting save"));
|
||||
}}
|
||||
>
|
||||
<Button danger>Delete</Button>
|
||||
<Button danger disabled={slaveOffline}>Delete</Button>
|
||||
</Popconfirm>
|
||||
</Space>,
|
||||
}}
|
||||
/>;
|
||||
|
||||
function onChange(changeEvent) {
|
||||
if (["done", "error"].includes(changeEvent.file.status)) {
|
||||
if (changeEvent.file.status === "error") {
|
||||
notifyErrorHandler("Error uploading file")(changeEvent.file.error);
|
||||
}
|
||||
|
||||
let newUploadingFiles = [...uploadingFiles];
|
||||
let index = newUploadingFiles.findIndex(f => f.uid === changeEvent.file.uid);
|
||||
if (index !== -1) {
|
||||
newUploadingFiles.splice(index, 1);
|
||||
setUploadingFiles(newUploadingFiles);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let file = {
|
||||
uid: changeEvent.file.uid,
|
||||
name: changeEvent.file.name,
|
||||
percent: changeEvent.file.percent,
|
||||
status: changeEvent.file.status,
|
||||
};
|
||||
|
||||
let newUploadingFiles = [...uploadingFiles];
|
||||
let index = newUploadingFiles.findIndex(f => f.uid === changeEvent.file.uid);
|
||||
if (index !== -1) {
|
||||
newUploadingFiles[index] = file;
|
||||
} else {
|
||||
newUploadingFiles.push(file);
|
||||
}
|
||||
setUploadingFiles(newUploadingFiles);
|
||||
}
|
||||
|
||||
let uploadProps = {
|
||||
disabled: slaveOffline,
|
||||
name: "file",
|
||||
accept: ".zip",
|
||||
headers: {
|
||||
@@ -118,18 +154,28 @@ export default function SavesList(props) {
|
||||
},
|
||||
showUploadList: false,
|
||||
action: `${webRoot}api/upload-save`,
|
||||
// TODO: show progress to user.
|
||||
onChange,
|
||||
};
|
||||
|
||||
return <div>
|
||||
<SectionHeader title="Saves" extra=<Space>
|
||||
<Upload {...uploadProps} >
|
||||
<Button>Upload save</Button>
|
||||
<Button disabled={slaveOffline}>Upload save</Button>
|
||||
</Upload>
|
||||
<CreateSaveModal instance={props.instance} />
|
||||
</Space> />
|
||||
<Upload.Dragger className="save-list-dragger" openFileDialogOnClick={false} {...uploadProps}>
|
||||
{saveTable}
|
||||
</Upload.Dragger>
|
||||
{uploadingFiles.length ? <List>
|
||||
{uploadingFiles.map(file => <List.Item key={file.uid}>
|
||||
{file.name}
|
||||
<Progress
|
||||
percent={file.percent}
|
||||
format={percent => `${Math.floor(percent)}%`}
|
||||
status={file.status === "error" ? "exception" : "normal"}
|
||||
/>
|
||||
</List.Item>)}
|
||||
</List> : null}
|
||||
</div>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory, useParams } from "react-router-dom";
|
||||
import { Button, Checkbox, Col, Form, Input, Popconfirm, Popover, Row, Select, Space, Spin, Switch } from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import PageLayout from "./PageLayout";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useContext, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Spin, Tag } from "antd";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
import DataTable from "./data-table";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
import { libLink, libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
export function useInstance(id) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
import { libLink, libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
export function useSaves(instanceId) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import { libLink } from "@clusterio/lib";
|
||||
|
||||
|
||||
export function useSlave(id) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { notification } from "antd";
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
import { libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
export default function notify(message, type = "info", description = undefined) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import libLink from "@clusterio/lib/link";
|
||||
import libPlugin from "@clusterio/lib/plugin";
|
||||
import libErrors from "@clusterio/lib/errors";
|
||||
import { logger } from "@clusterio/lib/logging";
|
||||
import { libErrors, libLink, libLogging, libPlugin } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
import packageJson from "../../package.json";
|
||||
|
||||
|
||||
@@ -10,8 +8,8 @@ import packageJson from "../../package.json";
|
||||
* @private
|
||||
*/
|
||||
export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
constructor(url, reconnectDelay) {
|
||||
super(url, reconnectDelay);
|
||||
constructor(url, maxReconnectDelay) {
|
||||
super(url, maxReconnectDelay);
|
||||
this.token = null;
|
||||
}
|
||||
|
||||
@@ -20,7 +18,7 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
throw new Error("Token not set");
|
||||
}
|
||||
|
||||
logger.verbose("SOCKET | registering control");
|
||||
logger.verbose("Connector | registering control");
|
||||
this.sendHandshake("register_control", {
|
||||
token: this.token,
|
||||
agent: "Clusterio Web UI",
|
||||
@@ -135,7 +133,7 @@ export class Control extends libLink.Link {
|
||||
throw new Error(`Given handler is not registered for slave ${id}`);
|
||||
}
|
||||
|
||||
handlers.slice(index, 1);
|
||||
handlers.splice(index, 1);
|
||||
if (!handlers.length) {
|
||||
this.slaveUpdateHandlers.delete(id);
|
||||
await this.updateSlaveSubscriptions();
|
||||
@@ -244,12 +242,12 @@ export class Control extends libLink.Link {
|
||||
let index = handlers.lastIndexOf(handler);
|
||||
if (index === -1) {
|
||||
throw new Error(`Given handler is not registered for instance ${id}`);
|
||||
}
|
||||
|
||||
handlers.splice(index, 1);
|
||||
if (!handlers.length) {
|
||||
this.saveListUpdateHandlers.delete(id);
|
||||
await this.updateSaveListSubscriptions();
|
||||
}
|
||||
handlers.splice(index, 1);
|
||||
if (!handlers.length) {
|
||||
this.saveListUpdateHandlers.delete(id);
|
||||
await this.updateSaveListSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use strict";
|
||||
let libLink = require("@clusterio/lib/link");
|
||||
let { libLink } = require("@clusterio/lib");
|
||||
|
||||
module.exports = {
|
||||
name: "global_chat",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-global_chat",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/factorioClusterio",
|
||||
"repository": "clusterio/clusterio",
|
||||
"scripts": {
|
||||
"build": "webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/clusterio/factorioClusterio/issues"
|
||||
"url": "https://github.com/clusterio/clusterio/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -20,8 +20,8 @@
|
||||
"@clusterio/lib": "^2.0.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.7",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.7",
|
||||
"@clusterio/lib": "^2.0.0-alpha.9",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.9",
|
||||
"webpack": "^5.36.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-merge": "^5.2.0"
|
||||
|
||||
@@ -23,10 +23,8 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib/config": { import: false },
|
||||
"@clusterio/lib/link": { import: false },
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"ajv": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
@@ -0,0 +1,45 @@
|
||||
# Clusterio inventory sync plugin
|
||||
|
||||
Carry over player inventory between servers
|
||||
|
||||
As a player, it mostly just works. Most important to know is that factorio data transfer is slow, which means if you have "big" items (blueprints mostly) in your inventory it will take a long time to transfer when joining servers. For this reason it is recommended to keep blueprints in the blueprint library.
|
||||
|
||||
A note on crashing servers:
|
||||
|
||||
When a server crashes while you are online there won't be time to immediately upload the inventory. Instead, the inventory is uploaded as soon as the server restarts. This means if you have been playing on a different server from the one that crashed and gathered/consumed items but are offline at the time of the first server coming online again your inventory will reset to the autosave.
|
||||
|
||||
## Installation
|
||||
|
||||
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
|
||||
|
||||
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicated slave or ctl installation respectively.
|
||||
|
||||
## Method of operation
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
When the player leaves the inventory is uploaded if it's a synced inventory and the exclusive access the scenario script holds is released.
|
||||
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.
|
||||
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.
|
||||
@@ -0,0 +1,122 @@
|
||||
"use strict";
|
||||
const { libConfig, libLink, libUsers } = require("@clusterio/lib");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = "inventory_sync";
|
||||
MasterConfigGroup.define({
|
||||
name: "autosave_interval",
|
||||
title: "Autosave Interval",
|
||||
description: "Interval the player data is autosaved at in seconds.",
|
||||
type: "number",
|
||||
initial_value: 600, // 10 minutes
|
||||
});
|
||||
MasterConfigGroup.define({
|
||||
name: "player_lock_timeout",
|
||||
title: "Player Lock Timeout",
|
||||
description:
|
||||
"Time in seconds before the lock on a player inventory expires after an instance stops or is disconnected",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
MasterConfigGroup.finalize();
|
||||
|
||||
class InstanceConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.groupName = "inventory_sync";
|
||||
InstanceConfigGroup.define({
|
||||
name: "rcon_chunk_size",
|
||||
title: "Rcon inventory chunk size",
|
||||
description: "Divide inventories into multiple chunks before sending with rcon to prevent blocking the pipe",
|
||||
type: "number",
|
||||
initial_value: 1000,
|
||||
});
|
||||
InstanceConfigGroup.finalize();
|
||||
|
||||
libUsers.definePermission({
|
||||
name: "inventory_sync.inventory.view",
|
||||
title: "View player inventories",
|
||||
description: "View player inventories",
|
||||
grantByDefault: true,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
name: "inventory_sync",
|
||||
title: "Inventory sync",
|
||||
description: "Synchronizes players inventories between instances",
|
||||
|
||||
instanceEntrypoint: "instance",
|
||||
InstanceConfigGroup,
|
||||
|
||||
masterEntrypoint: "master",
|
||||
MasterConfigGroup,
|
||||
|
||||
webEntrypoint: "./web",
|
||||
routes: ["/inventory"],
|
||||
|
||||
messages: {
|
||||
acquire: new libLink.Request({
|
||||
type: "inventory_sync:acquire",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
responseRequired: ["status"],
|
||||
responseProperties: {
|
||||
"status": { enum: ["acquired", "error", "busy"] },
|
||||
"generation": { type: "integer" },
|
||||
"has_data": { type: "boolean" },
|
||||
"message": { type: "string" },
|
||||
},
|
||||
}),
|
||||
release: new libLink.Request({
|
||||
type: "inventory_sync:release",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
}),
|
||||
upload: new libLink.Request({
|
||||
type: "inventory_sync:upload",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
"player_data": { type: "object" },
|
||||
},
|
||||
}),
|
||||
download: new libLink.Request({
|
||||
type: "inventory_sync:download",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"player_data": { type: ["object", "null"] },
|
||||
},
|
||||
}),
|
||||
databaseStats: new libLink.Request({
|
||||
type: "inventory_sync:databaseStats",
|
||||
links: ["control-master"],
|
||||
permission: "inventory_sync.inventory.view",
|
||||
responseProperties: {
|
||||
"database_size": { type: "integer" },
|
||||
"database_entries": { type: "integer" },
|
||||
"largest_entry": {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
size: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
"use strict";
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
|
||||
/**
|
||||
* Splits string into array of strings with max of a certain length
|
||||
* @param {Number} chunkSize - Max length of each chunk
|
||||
* @param {String} string - String to split into chunks
|
||||
* @returns {String[]} Chunks
|
||||
*/
|
||||
function chunkify(chunkSize, string) {
|
||||
return string.match(new RegExp(`.{1,${chunkSize}}`, "g"));
|
||||
}
|
||||
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
async init() {
|
||||
this.playersToRelease = new Set();
|
||||
this.disconnecting = false;
|
||||
|
||||
// Handle IPC from scenario script
|
||||
this.instance.server.on("ipc-inventory_sync_acquire", content => this.handleAcquire(content)
|
||||
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_acquire:\n${err.stack}`)));
|
||||
this.instance.server.on("ipc-inventory_sync_release", content => this.handleRelease(content)
|
||||
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_release:\n${err.stack}`)));
|
||||
this.instance.server.on("ipc-inventory_sync_upload", content => this.handleUpload(content)
|
||||
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`)));
|
||||
this.instance.server.on("ipc-inventory_sync_download", content => this.handleDownload(content)
|
||||
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`)));
|
||||
}
|
||||
|
||||
onPrepareMasterDisconnect() {
|
||||
this.disconnecting = true;
|
||||
}
|
||||
|
||||
onMasterConnectionEvent(event) {
|
||||
if (event === "connect") {
|
||||
this.disconnecting = false;
|
||||
(async () => {
|
||||
for (let player_name of this.playersToRelease) {
|
||||
if (!this.slave.connector.connected || this.disconnecting) {
|
||||
return;
|
||||
}
|
||||
this.playersToRelease.delete(player);
|
||||
await this.info.messages.release.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name,
|
||||
});
|
||||
}
|
||||
})().catch(
|
||||
err => this.logger.error(`Unpexpected error releasing queued up players:\n${err.stack}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAcquire(request) {
|
||||
let response = {
|
||||
player_name: request.player_name,
|
||||
status: "error",
|
||||
message: "Master is temporarily unavailable",
|
||||
};
|
||||
|
||||
if (this.slave.connector.connected && !this.disconnecting) {
|
||||
try {
|
||||
response = {
|
||||
player_name: request.player_name,
|
||||
...await this.info.messages.acquire.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: request.player_name,
|
||||
}),
|
||||
};
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
this.logger.error(`Unexpected error sending aquire request:\n${err.stack}`);
|
||||
response.message = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let json = libLuaTools.escapeString(JSON.stringify(response));
|
||||
await this.sendRcon(`/sc inventory_sync.acquire_response("${json}")`, true);
|
||||
}
|
||||
|
||||
async handleRelease(request) {
|
||||
if (!this.slave.connector.connected) {
|
||||
this.playersToRelease.set(request.player_name);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.info.messages.release.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: request.player_name,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof libErrors.SessionLost) {
|
||||
this.playersToRelease.set(request.player_name);
|
||||
} else {
|
||||
this.logger.error(`Unexpected error releasing player ${request.player_name}:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleUpload(player_data) {
|
||||
if (!this.slave.connector.connected || this.disconnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Uploading ${player_data.name} (${JSON.stringify(player_data).length / 1000}kB)`);
|
||||
try {
|
||||
await this.info.messages.upload.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: player_data.name,
|
||||
player_data: player_data,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
this.logger.error(`Unexpected error uploading inventory for ${player_data.name}:\n${err.stack}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendRcon(
|
||||
`/sc inventory_sync.confirm_upload("${player_data.name}", ${player_data.generation})`, true
|
||||
);
|
||||
}
|
||||
|
||||
async handleDownload(request) {
|
||||
const player_name = request.player_name;
|
||||
this.logger.verbose(`Downloading ${player_name}`);
|
||||
|
||||
let response = await this.info.messages.download.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name,
|
||||
});
|
||||
|
||||
if (!response.player_data) {
|
||||
await this.sendRcon(`/sc inventory_sync.download_inventory('${player_name}',nil,0,0)`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size");
|
||||
const chunks = chunkify(chunkSize, JSON.stringify(response.player_data));
|
||||
this.logger.verbose(`Sending inventory for ${player_name} in ${chunks.length} chunks`);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
// this.logger.verbose(`Sending chunk ${i+1} of ${chunks.length}`)
|
||||
const chunk = libLuaTools.escapeString(chunks[i]);
|
||||
await this.sendRcon(
|
||||
`/sc inventory_sync.download_inventory('${player_name}','${chunk}',${i + 1},${chunks.length})`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InstancePlugin,
|
||||
};
|
||||