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 |
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,
|
||||
},
|
||||
|
||||
@@ -34,6 +34,8 @@ If you are starting a new cluster it's highly recommended to use the 2.0 alpha.
|
||||
* [MacOS setup](#macos-setup)
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Configure Master Server](#configure-master-server)
|
||||
* [Managing Factorio mods](#managing-factorio-mods)
|
||||
* [Setting up shared storage](#setting-up-shared-storage)
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Setting up remote slaves](#setting-up-remote-slaves)
|
||||
* [Setting up clusterioctl](#setting-up-clusterioctl)
|
||||
@@ -72,6 +74,7 @@ These are the plugins supported and maintained by the Clusterio developers:
|
||||
- [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/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:
|
||||
|
||||
@@ -112,8 +115,8 @@ Otherwise see below for OS specific instructions.
|
||||
|
||||
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.gz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
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".
|
||||
|
||||
@@ -210,6 +213,24 @@ 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.
|
||||
|
||||
|
||||
## Managing Factorio mods
|
||||
|
||||
Clusterio currently only provides a manual method for running mods on servers by copying them either to each instance's individual `mods` folder, or by copying them into a slave installation's `sharedMods` folder which will link/copy them to the `mods` folder of each instance on that slave if the file doesn't already exist in the `mods` folder for that instance.
|
||||
Mods are copied from `sharedMods` on Windows and symlinked on Linux, this means that when mods are removed from `sharedMods` the mods will also be removed from the instance's `mods` folder on Linux if it got there by being linked by Clusterio, but not on Windows.
|
||||
|
||||
For managing the mod settings the `mod-settings.dat` and `mod-list.json` files will also be linked/copied over if present in `sharedMods`, but since Factorio creates these files automatically it might be necessary to delete them from each instance's `mods` folder for changes to them in `sharedMods` to take effect.
|
||||
|
||||
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 completing the installation start up the master server and at least one slave separately.
|
||||
@@ -218,6 +239,21 @@ Once the master process is running you can log into the web interface which is h
|
||||
|
||||
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
|
||||
|
||||
|
||||
## Setting up remote slaves
|
||||
|
||||
|
||||
@@ -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 |
@@ -224,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.8"
|
||||
"version": "2.0.0-alpha.9"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"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");
|
||||
@@ -18,6 +19,7 @@ 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" },
|
||||
@@ -275,6 +277,7 @@ WantedBy=multi-user.target
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function inquirerMissingArgs(args) {
|
||||
let answers = {};
|
||||
if (args.mode) { answers.mode = args.mode; }
|
||||
@@ -373,6 +376,22 @@ async function inquirerMissingArgs(args) {
|
||||
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([
|
||||
{
|
||||
@@ -415,9 +434,55 @@ async function inquirerMissingArgs(args) {
|
||||
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() {
|
||||
const args = yargs
|
||||
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",
|
||||
@@ -449,9 +514,20 @@ async function main() {
|
||||
})
|
||||
.option("plugins", {
|
||||
array: true, describe: "Plugins to install", type: "string",
|
||||
})
|
||||
.argv
|
||||
;
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -461,6 +537,10 @@ async function main() {
|
||||
}
|
||||
|
||||
let answers = await inquirerMissingArgs(args);
|
||||
if (answers.downloadHeadless) {
|
||||
await downloadLinuxServer();
|
||||
}
|
||||
|
||||
logger.verbose(JSON.stringify(answers));
|
||||
|
||||
if (!dev) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Installer for clusterio",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"main": "create.js",
|
||||
"bin": {
|
||||
"clusteriocreate": "./create.js"
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Clusterio command line management tool",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"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.8",
|
||||
"@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;
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.8",
|
||||
"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"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Implementation of Clusterio master server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"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.8",
|
||||
"@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.8",
|
||||
"@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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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/clusterio",
|
||||
"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,104 +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
|
||||
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)
|
||||
return string.format("%.17g", data)
|
||||
|
||||
elseif type(data) == "string" then
|
||||
data = data:gsub('([\x00-\x1f\\"])', function(match)
|
||||
return "\\u00" .. 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/clusterio",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"@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,7 +1618,7 @@ 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();
|
||||
@@ -1630,6 +1635,15 @@ ${err.stack}`
|
||||
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 = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Clusterio web interface implementation",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"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.8",
|
||||
"@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";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, List, Popconfirm, Progress, Space, Table, Tooltip, Upload } fro
|
||||
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";
|
||||
@@ -29,6 +29,7 @@ export default function SavesList(props) {
|
||||
let [starting, setStarting] = useState(false);
|
||||
let [uploadingFiles, setUploadingFiles] = useState([]);
|
||||
|
||||
let slaveOffline = ["unassigned", "unknown"].includes(props.instance.status);
|
||||
const saveTable = <Table
|
||||
size="small"
|
||||
columns={[
|
||||
@@ -79,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 }
|
||||
@@ -102,7 +104,7 @@ export default function SavesList(props) {
|
||||
).catch(notifyErrorHandler("Error deleting save"));
|
||||
}}
|
||||
>
|
||||
<Button danger>Delete</Button>
|
||||
<Button danger disabled={slaveOffline}>Delete</Button>
|
||||
</Popconfirm>
|
||||
</Space>,
|
||||
}}
|
||||
@@ -141,6 +143,7 @@ export default function SavesList(props) {
|
||||
}
|
||||
|
||||
let uploadProps = {
|
||||
disabled: slaveOffline,
|
||||
name: "file",
|
||||
accept: ".zip",
|
||||
headers: {
|
||||
@@ -157,7 +160,7 @@ export default function SavesList(props) {
|
||||
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> />
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-global_chat",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"version": "2.0.0-alpha.9",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
@@ -20,8 +20,8 @@
|
||||
"@clusterio/lib": "^2.0.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.8",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.8",
|
||||
"@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,
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
|
||||
async function loadDatabase(config, logger) {
|
||||
let itemsPath = path.resolve(config.get("master.database_directory"), "inventories.json");
|
||||
logger.verbose(`Loading ${itemsPath}`);
|
||||
try {
|
||||
let content = await fs.readFile(itemsPath);
|
||||
return new Map(JSON.parse(content));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.verbose("Creating new player data database");
|
||||
return new Map();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDatabase(masterConfig, playerDatastore, logger) {
|
||||
if (playerDatastore) {
|
||||
let file = path.resolve(masterConfig.get("master.database_directory"), "inventories.json");
|
||||
logger.verbose(`writing ${file}`);
|
||||
let content = JSON.stringify(Array.from(playerDatastore));
|
||||
await fs.outputFile(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
class MasterPlugin extends libPlugin.BaseMasterPlugin {
|
||||
async init() {
|
||||
this.acquiredPlayers = new Map();
|
||||
this.playerDatastore = await loadDatabase(this.master.config, this.logger);
|
||||
this.autosaveId = setInterval(() => {
|
||||
saveDatabase(this.master.config, this.playerDatastore, this.logger).catch(err => {
|
||||
this.logger.error(`Unexpected error autosaving player data:\n${err.stack}`);
|
||||
});
|
||||
}, this.master.config.get("inventory_sync.autosave_interval") * 1000);
|
||||
}
|
||||
|
||||
async onInstanceStatusChanged(instance) {
|
||||
let instanceId = instance.config.get("instance.id");
|
||||
if (["unassigned", "deleted"].includes(instance.status)) {
|
||||
for (let [playerName, acquisitionRecord] of this.acquiredPlayers) {
|
||||
if (acquisitionRecord.instanceId === instanceId) {
|
||||
this.acquiredPlayers.delete(playerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (["unknown", "stopped"].includes(instance.status)) {
|
||||
let timeout = this.master.config.get("inventory_sync.player_lock_timeout") * 1000;
|
||||
for (let acquisitonRecord of this.acquiredPlayers.values()) {
|
||||
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expires) {
|
||||
acquisitonRecord.expires = Date.now() + timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.status === "running") {
|
||||
for (let acquisitonRecord of this.acquiredPlayers.values()) {
|
||||
if (acquisitonRecord.instanceId === instanceId && acquisitonRecord.expires) {
|
||||
delete acquisitonRecord.expires;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acquire(instanceId, playerName) {
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (
|
||||
!acquisitionRecord
|
||||
|| acquisitionRecord.instanceId === instanceId
|
||||
|| !this.master.instances.has(acquisitionRecord.instanceId)
|
||||
|| acquisitionRecord.expires && acquisitionRecord.expires < Date.now()
|
||||
) {
|
||||
this.acquiredPlayers.set(playerName, { instanceId });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async acquireRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
if (!this.acquire(instance_id, player_name)) {
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
let instance = this.master.instances.get(acquisitionRecord.instanceId);
|
||||
return {
|
||||
status: "busy",
|
||||
message: instance.config.get("instance.name"),
|
||||
};
|
||||
}
|
||||
|
||||
let playerData = this.playerDatastore.get(player_name);
|
||||
return {
|
||||
status: "acquired",
|
||||
has_data: Boolean(playerData),
|
||||
generation: playerData ? playerData.generation : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async releaseRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
if (!acquisitionRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (acquisitionRecord.instanceId === instance_id) {
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadRequestHandler(message) {
|
||||
let { instance_id, player_name, player_data } = message.data;
|
||||
let instanceName = this.master.instances.get(instance_id).config.get("instance.name");
|
||||
let store = true;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
if (!acquisitionRecord) {
|
||||
this.logger.warn(`${instanceName} uploaded ${player_name} without an acquisition`);
|
||||
// Allow upload in this case as it might come from a crashed instance that restarted and is now
|
||||
// uploading the player data for all the players that were online during the last autosave.
|
||||
|
||||
} else if (acquisitionRecord.instanceId !== instance_id) {
|
||||
this.logger.warn(`${instanceName} uploaded ${player_name} while another instance has acquired it`);
|
||||
store = false;
|
||||
|
||||
} else {
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
}
|
||||
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
let oldPlayerData = this.playerDatastore.get(player_name);
|
||||
if (store && oldPlayerData && oldPlayerData.generation >= player_data.generation) {
|
||||
this.logger.warn(
|
||||
`${instanceName} uploaded generation ${player_data.generation} while the stored` +
|
||||
`generation is ${oldPlayerData.generation} for ${player_name}`
|
||||
);
|
||||
store = false;
|
||||
}
|
||||
|
||||
if (store) {
|
||||
this.logger.verbose(`Received player data for ${player_name} from ${instanceName}`);
|
||||
this.playerDatastore.set(player_name, player_data);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
let instanceName = this.master.instances.get(instance_id).config.get("instance.name");
|
||||
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
if (!acquisitionRecord) {
|
||||
this.logger.warn(`${instanceName} downloaded ${player_name} without an acquisition`);
|
||||
} else if (acquisitionRecord.instanceId !== instance_id) {
|
||||
this.logger.warn(`${instanceName} downloaded ${player_name} while another instance has acquired it`);
|
||||
}
|
||||
|
||||
this.logger.verbose(`Sending player data for ${player_name} to ${instanceName}`);
|
||||
return {
|
||||
player_data: this.playerDatastore.get(player_name) || null,
|
||||
};
|
||||
}
|
||||
|
||||
async onShutdown() {
|
||||
clearInterval(this.autosaveId);
|
||||
await saveDatabase(this.master.config, this.playerDatastore, this.logger);
|
||||
}
|
||||
|
||||
async databaseStatsRequestHandler(message) {
|
||||
let playerDatastore = Array.from(this.playerDatastore.keys())
|
||||
.map(name => ({
|
||||
name,
|
||||
length: JSON.stringify(this.playerDatastore.get(name)).length,
|
||||
}))
|
||||
.sort((a, b) => a.length - b.length);
|
||||
return {
|
||||
database_size: playerDatastore.map(x => x.length).reduce((a, b) => b - a, 0),
|
||||
database_entries: playerDatastore.length,
|
||||
largest_entry: {
|
||||
name: playerDatastore[0] && playerDatastore[0].name || "-",
|
||||
size: playerDatastore[0] && playerDatastore[0].length || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MasterPlugin,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
local inventories = {
|
||||
-- Armor must be before main or the sync will fail when the inventory
|
||||
-- bonus slots from the armor are in use.
|
||||
armor = defines.inventory.character_armor,
|
||||
main = defines.inventory.character_main,
|
||||
guns = defines.inventory.character_guns,
|
||||
ammo = defines.inventory.character_ammo,
|
||||
trash = defines.inventory.character_trash,
|
||||
}
|
||||
|
||||
return inventories
|
||||
@@ -0,0 +1,17 @@
|
||||
return {
|
||||
"character_crafting_speed_modifier",
|
||||
"character_mining_speed_modifier",
|
||||
"character_additional_mining_categories",
|
||||
"character_running_speed_modifier",
|
||||
"character_build_distance_bonus",
|
||||
"character_item_drop_distance_bonus",
|
||||
"character_reach_distance_bonus",
|
||||
"character_resource_reach_distance_bonus",
|
||||
"character_item_pickup_distance_bonus",
|
||||
"character_loot_pickup_distance_bonus",
|
||||
"character_inventory_slots_bonus",
|
||||
"character_trash_slot_count_bonus",
|
||||
"character_maximum_following_robot_count_bonus",
|
||||
"character_health_bonus",
|
||||
"character_personal_logistic_requests_enabled",
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
local progress_dialog = require("modules/inventory_sync/gui/progress_dialog")
|
||||
local ensure_character = require("modules/inventory_sync/ensure_character")
|
||||
local serialize = require("modules/inventory_sync/serialize")
|
||||
local load_crafting_queue = require("modules/inventory_sync/load_crafting_queue")
|
||||
|
||||
local function download_inventory(player_name, data, number, total)
|
||||
local player = game.get_player(player_name)
|
||||
if player == nil then
|
||||
rcon.print("Player " .. player_name .. " does not exist")
|
||||
return
|
||||
end
|
||||
|
||||
local record = global.inventory_sync.active_downloads[player_name]
|
||||
if record == nil then
|
||||
rcon.print("No active download is in progress for " .. player_name)
|
||||
return
|
||||
end
|
||||
|
||||
local player_record = global.inventory_sync.players[player_name]
|
||||
if record.restart then
|
||||
rcon.print("Restarting outdated download")
|
||||
inventory_sync.initiate_inventory_download(player, player_record, record.generation)
|
||||
return
|
||||
end
|
||||
|
||||
if total == 0 then
|
||||
-- Download was requested but the player had no data stored.
|
||||
log("ERROR: Inventory sync failed, got empty player data for " .. player.name .. " from the master")
|
||||
player.print("ERROR: Inventory sync failed, got empty player data from the master")
|
||||
|
||||
progress_dialog.remove(player)
|
||||
|
||||
-- Give the player a character and pretend that's the synced player data
|
||||
ensure_character(player)
|
||||
|
||||
-- Restore player position in case they moved while being spectator
|
||||
if record.surface and record.position then
|
||||
player.teleport(record.position, record.surface)
|
||||
end
|
||||
|
||||
global.inventory_sync.active_downloads[player_name] = nil
|
||||
player_record.dirty = player.connected
|
||||
player_record.sync = true
|
||||
player_record.generation = record.generation
|
||||
return
|
||||
end
|
||||
|
||||
record.data = record.data .. data
|
||||
|
||||
-- Show progress in console
|
||||
-- player.print("Downloaded "..(number + 1).."/"..total.." parts")
|
||||
if number ~= total then
|
||||
-- Show progress in GUI
|
||||
progress_dialog.display(player, number, total)
|
||||
|
||||
-- Update activity to indicate this session is still going
|
||||
record.last_active = game.ticks_played
|
||||
return
|
||||
end
|
||||
|
||||
progress_dialog.remove(player)
|
||||
|
||||
-- Editor mode is not supported.
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
player.toggle_map_editor()
|
||||
end
|
||||
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
log("ERROR: Inventory sync failed, unable to switch " .. player.name .. " out of editor mode")
|
||||
player.print("ERROR: Inventory sync failed, unable to switch out of editor mode")
|
||||
return
|
||||
end
|
||||
|
||||
-- Stash temporary inventory if it exists
|
||||
local stashed_corpse
|
||||
if player_record.dirty and player.character then
|
||||
local character = player.character
|
||||
local surface = character.surface
|
||||
local position = character.position
|
||||
local corpse_name = character.prototype.character_corpse.name
|
||||
player.character = nil
|
||||
character.die()
|
||||
stashed_corpse = surface.find_entity(corpse_name, position)
|
||||
end
|
||||
|
||||
-- Deserialize downloaded player data
|
||||
local serialized_player = game.json_to_table(record.data)
|
||||
serialize.deserialize_player(player, serialized_player)
|
||||
|
||||
-- Restore player position in case they moved while being spectator
|
||||
if record.surface and record.position then
|
||||
player.teleport(record.position, record.surface)
|
||||
end
|
||||
|
||||
-- Load crafting queue
|
||||
if player.character then
|
||||
load_crafting_queue(serialized_player.crafting_queue, player)
|
||||
end
|
||||
|
||||
-- Transfer items from stashed inventory
|
||||
if stashed_corpse then
|
||||
local main = player.get_main_inventory()
|
||||
local stash = stashed_corpse.get_inventory(defines.inventory.character_corpse)
|
||||
if main then
|
||||
for i = 1, #stash do
|
||||
-- Try transfering a stack
|
||||
local source = stash[i]
|
||||
if source and source.valid_for_read then
|
||||
local target = main.find_empty_stack()
|
||||
if not target then
|
||||
break
|
||||
end
|
||||
target.transfer_stack(source)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if stash.is_empty() then
|
||||
stashed_corpse.destroy()
|
||||
else
|
||||
player.print(
|
||||
"Your temprorary inventory did not fit into your synced one and the remaining items have " ..
|
||||
"been placed in a corpse below you."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Player may have left before the download completed
|
||||
player_record.dirty = player.connected
|
||||
player_record.sync = true
|
||||
player_record.generation = serialized_player.generation
|
||||
|
||||
-- Remove download session and data
|
||||
global.inventory_sync.active_downloads[player_name] = nil
|
||||
|
||||
local ticks = game.ticks_played - record.started
|
||||
player.print("Imported player data in " .. ticks .. " ticks")
|
||||
log("Imported player data for " .. player_name .. " in " .. ticks .. " ticks")
|
||||
end
|
||||
|
||||
return download_inventory
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Ensures that the given player has a character if one doesn't exist
|
||||
local function ensure_character(player)
|
||||
if not player.character and not player.ticks_to_respawn then
|
||||
if player.controller_type ~= defines.controllers.ghost then
|
||||
player.set_controller({ type = defines.controllers.ghost })
|
||||
end
|
||||
player.ticks_to_respawn = 0
|
||||
player.ticks_to_respawn = nil
|
||||
end
|
||||
end
|
||||
|
||||
return ensure_character
|
||||
@@ -0,0 +1,147 @@
|
||||
local ensure_character = require("modules/inventory_sync/ensure_character")
|
||||
|
||||
local dialog_failed_download = {}
|
||||
|
||||
function dialog_failed_download.create(player, acquire_response)
|
||||
if player.gui.screen.dialog_failed_download then
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
end
|
||||
|
||||
local frame = player.gui.screen.add {
|
||||
name = "dialog_failed_download",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
}
|
||||
|
||||
local titlebar = frame.add {
|
||||
type = "flow",
|
||||
}
|
||||
titlebar.drag_target = frame
|
||||
|
||||
local title_caption = "Inventory sync download failed"
|
||||
if acquire_response.status == "busy" then
|
||||
title_caption = "Inventory is in use on " .. acquire_response.message
|
||||
end
|
||||
|
||||
local title = titlebar.add {
|
||||
type = "label",
|
||||
style = "frame_title",
|
||||
caption = title_caption,
|
||||
}
|
||||
title.drag_target = frame
|
||||
|
||||
local filler = titlebar.add {
|
||||
type = "empty-widget",
|
||||
style = "draggable_space_header",
|
||||
}
|
||||
filler.style.horizontally_stretchable = "on"
|
||||
filler.style.right_margin = 4
|
||||
filler.style.height = 24
|
||||
filler.drag_target = frame
|
||||
|
||||
local content = frame.add {
|
||||
name = "inventory_sync_failed_download_content",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
style = "inside_shallow_frame_with_padding",
|
||||
}
|
||||
content.style.width = 400
|
||||
|
||||
local reason
|
||||
if acquire_response.status == "timeout" then
|
||||
reason = "Timed out fetching inventory from the master server."
|
||||
|
||||
elseif acquire_response.status == "busy" then
|
||||
reason =
|
||||
"Your synced inventory is in in use on " .. acquire_response.message .. ". " ..
|
||||
"If you are logged in there you will have to log out there before you can sync the inventory here. " ..
|
||||
"If you just logged out from there it might take some time before the inventory is available."
|
||||
|
||||
elseif acquire_response.status == "error" then
|
||||
reason = "Error fetching inventory from the master server: " .. acquire_response.message .. "."
|
||||
|
||||
else
|
||||
reason = "Unknown status " .. acquire_response.status .. " fetching inventory from the master server."
|
||||
end
|
||||
|
||||
local p1 = content.add {
|
||||
type = "label",
|
||||
caption = reason
|
||||
}
|
||||
p1.style.single_line = false
|
||||
|
||||
local p2 = content.add {
|
||||
type = "label",
|
||||
caption =
|
||||
"You may choose to retry fetching the inventory or play with a temporary inventory which will be merged " ..
|
||||
"with your synced inventory the next time you join this server."
|
||||
,
|
||||
}
|
||||
p2.style.single_line = false
|
||||
p2.style.top_margin = 8
|
||||
|
||||
local dialog_row = frame.add {
|
||||
type = "flow",
|
||||
}
|
||||
dialog_row.style.top_margin = 8
|
||||
dialog_row.drag_target = frame
|
||||
|
||||
local abort = dialog_row.add {
|
||||
name = "inventory_sync_failed_download_abort",
|
||||
type = "button",
|
||||
style = "back_button",
|
||||
caption = "Use temporary inventory",
|
||||
tooltip = "Items you obtain here will be merged with your synced inventory the next time you join this server.",
|
||||
}
|
||||
|
||||
local dialog_filler = dialog_row.add {
|
||||
type = "empty-widget",
|
||||
style = "draggable_space_header",
|
||||
}
|
||||
dialog_filler.style.horizontally_stretchable = "on"
|
||||
dialog_filler.style.right_margin = 4
|
||||
dialog_filler.style.height = 28
|
||||
dialog_filler.drag_target = frame
|
||||
|
||||
local retry = dialog_row.add {
|
||||
name = "inventory_sync_failed_download_retry",
|
||||
type = "button",
|
||||
style = "forward_button",
|
||||
caption = "Retry",
|
||||
}
|
||||
|
||||
frame.force_auto_center()
|
||||
player.opened = frame
|
||||
end
|
||||
|
||||
dialog_failed_download.events = {
|
||||
[defines.events.on_gui_click] = function(event)
|
||||
if not event.element.valid then
|
||||
return
|
||||
end
|
||||
|
||||
if event.element.name == "inventory_sync_failed_download_abort" then
|
||||
-- Give the player a charatcer and let them play with that
|
||||
local player = game.get_player(event.player_index)
|
||||
ensure_character(player)
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
global.inventory_sync.players[player.name].dirty = true
|
||||
global.inventory_sync.players[player.name].sync = false
|
||||
|
||||
elseif event.element.name == "inventory_sync_failed_download_retry" then
|
||||
-- Retry acquiring the player from the master
|
||||
local player = game.get_player(event.player_index)
|
||||
inventory_sync.acquire(player)
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
end
|
||||
end,
|
||||
|
||||
[defines.events.on_player_left_game] = function(event)
|
||||
local player = game.get_player(event.player_index)
|
||||
if player.gui.screen.dialog_failed_download then
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
return dialog_failed_download
|
||||
@@ -0,0 +1,98 @@
|
||||
-- Show inventory download progressbar
|
||||
local progress_dialog = {}
|
||||
|
||||
function progress_dialog.create(player, progress, total)
|
||||
local frame = player.gui.screen.add {
|
||||
name = "inventory_sync_progress",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
}
|
||||
|
||||
local titlebar = frame.add {
|
||||
type = "flow",
|
||||
}
|
||||
titlebar.drag_target = frame
|
||||
|
||||
local title = titlebar.add {
|
||||
type = "label",
|
||||
style = "frame_title",
|
||||
caption = "Inventory sync",
|
||||
}
|
||||
title.drag_target = frame
|
||||
|
||||
local filler = titlebar.add {
|
||||
type = "empty-widget",
|
||||
style = "draggable_space_header",
|
||||
}
|
||||
filler.style.horizontally_stretchable = "on"
|
||||
filler.style.right_margin = 4
|
||||
filler.style.height = 24
|
||||
filler.drag_target = frame
|
||||
|
||||
local content = frame.add {
|
||||
name = "inventory_sync_progress_dialog_content",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
style = "inside_shallow_frame_with_padding",
|
||||
}
|
||||
content.style.width = 400
|
||||
|
||||
local p1 = content.add {
|
||||
type = "label",
|
||||
caption = "Downloading inventory chunk " .. progress .. " / " .. total,
|
||||
}
|
||||
p1.style.single_line = false
|
||||
p1.style.bottom_margin = 8
|
||||
|
||||
local progressbar = content.add {
|
||||
type = "progressbar",
|
||||
value = progress / total,
|
||||
}
|
||||
progressbar.style.width = 376 -- inside_shallow_frame_with_padding has 12px padding on each side
|
||||
|
||||
frame.force_auto_center()
|
||||
player.opened = frame
|
||||
end
|
||||
|
||||
function progress_dialog.remove(player)
|
||||
if player.gui.screen.inventory_sync_progress then
|
||||
player.gui.screen.inventory_sync_progress.destroy()
|
||||
end
|
||||
end
|
||||
|
||||
function progress_dialog.update(player, progress, total)
|
||||
-- Be defensive here in case an old GUI is present
|
||||
local frame = player.gui.screen.inventory_sync_progress
|
||||
if not frame then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = frame.inventory_sync_progress_dialog_content
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
|
||||
local p1 = content.children[1]
|
||||
if not p1 or p1.type ~= "label" then
|
||||
return false
|
||||
end
|
||||
|
||||
local progressbar = content.children[2]
|
||||
if not progressbar or progressbar.type ~= "progressbar" then
|
||||
return false
|
||||
end
|
||||
|
||||
p1.caption = "Downloading inventory chunk " .. progress .. " / " .. total
|
||||
progressbar.value = progress / total
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function progress_dialog.display(player, progress, total)
|
||||
if not progress_dialog.update(player, progress, total) then
|
||||
progress_dialog.remove(player)
|
||||
progress_dialog.create(player, progress, total)
|
||||
end
|
||||
end
|
||||
|
||||
return progress_dialog
|
||||
@@ -0,0 +1,368 @@
|
||||
--[[
|
||||
|
||||
Inventory Sync
|
||||
|
||||
When a player leaves the game, serialize the player state and upload it to the master.
|
||||
When a player joins the game, request the player data from master and deserialize the player state from it.
|
||||
|
||||
]]
|
||||
|
||||
local clusterio_api = require("modules/clusterio/api")
|
||||
local upload_inventory = require("modules/inventory_sync/upload_inventory")
|
||||
local download_inventory = require("modules/inventory_sync/download_inventory")
|
||||
local progress_dialog = require("modules/inventory_sync/gui/progress_dialog")
|
||||
local dialog_failed_download = require("modules/inventory_sync/gui/dialog_failed_download")
|
||||
|
||||
|
||||
-- Returns true if the player is currently in a cutscene
|
||||
local function is_in_cutscene(player)
|
||||
return player.controller_type == defines.controllers.cutscene
|
||||
end
|
||||
|
||||
-- Create player record for bookeeping
|
||||
local function create_player(player, dirty)
|
||||
global.inventory_sync.players[player.name] = {
|
||||
dirty = dirty, -- Player inventory has changes that should be persisted
|
||||
sync = false, -- Player inventory is synced from the master
|
||||
generation = 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- Remove all stored player data by this module
|
||||
local function remove_player(player)
|
||||
global.inventory_sync.saved_crafting_queue[player.name] = nil
|
||||
global.inventory_sync.players_waiting_for_acquire[player.name] = nil
|
||||
global.inventory_sync.players_in_cutscene_to_sync[player.name] = nil
|
||||
global.inventory_sync.active_downloads[player.name] = nil
|
||||
global.inventory_sync.active_uploads[player.name] = nil
|
||||
global.inventory_sync.players[player.name] = nil
|
||||
end
|
||||
|
||||
|
||||
inventory_sync = {}
|
||||
-- This function is called by instance.js as /sc inventory_sync.download_inventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the master
|
||||
inventory_sync.download_inventory = download_inventory
|
||||
-- This function is called internally when a player leaves the game for their inventory to upload to the master
|
||||
inventory_sync.upload_inventory = upload_inventory
|
||||
|
||||
inventory_sync.events = {}
|
||||
inventory_sync.events[clusterio_api.events.on_server_startup] = function(event)
|
||||
-- Set up global table
|
||||
if not global.inventory_sync then
|
||||
global.inventory_sync = {}
|
||||
end
|
||||
if not global.inventory_sync.saved_crafting_queue then
|
||||
global.inventory_sync.saved_crafting_queue = {}
|
||||
end
|
||||
|
||||
if not global.inventory_sync.players then
|
||||
global.inventory_sync.players = {}
|
||||
end
|
||||
|
||||
global.inventory_sync.players_waiting_for_acquire = {}
|
||||
global.inventory_sync.players_in_cutscene_to_sync = {}
|
||||
|
||||
if not global.inventory_sync.active_downloads then
|
||||
global.inventory_sync.active_downloads = {}
|
||||
end
|
||||
if not global.inventory_sync.active_uploads then
|
||||
global.inventory_sync.active_uploads = {}
|
||||
end
|
||||
|
||||
for _, player in pairs(game.players) do
|
||||
if not global.inventory_sync.players[player.name] then
|
||||
create_player(player, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cleanup
|
||||
inventory_sync.events[defines.events.on_pre_player_removed] = function(event)
|
||||
remove_player(game.get_player(event.player_index))
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_player_created] = function(event)
|
||||
create_player(game.get_player(event.player_index), false)
|
||||
end
|
||||
|
||||
function inventory_sync.acquire(player)
|
||||
global.inventory_sync.players_waiting_for_acquire[player.name] = {
|
||||
start_tick = game.ticks_played,
|
||||
}
|
||||
clusterio_api.send_json("inventory_sync_acquire", {
|
||||
player_name = player.name,
|
||||
})
|
||||
end
|
||||
|
||||
function inventory_sync.check_players_waiting_for_acquire()
|
||||
for player_name, record in pairs(global.inventory_sync.players_waiting_for_acquire) do
|
||||
-- Check if the acquire player request timed out
|
||||
if game.ticks_played > record.start_tick + 600 then
|
||||
inventory_sync.acquire_response(game.table_to_json({
|
||||
status = "timeout",
|
||||
player_name = player_name,
|
||||
}))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.acquire_response(data)
|
||||
local response = game.json_to_table(data)
|
||||
if not global.inventory_sync.players_waiting_for_acquire[response.player_name] then
|
||||
clusterio_api.send_json("inventory_sync_release", { player_name = response.player_name })
|
||||
return
|
||||
end
|
||||
global.inventory_sync.players_waiting_for_acquire[response.player_name] = nil
|
||||
|
||||
local player = game.get_player(response.player_name)
|
||||
if is_in_cutscene(player) then
|
||||
response.player = player
|
||||
global.inventory_sync.players_in_cutscene_to_sync[response.player_name] = response
|
||||
else
|
||||
inventory_sync.sync_player(response)
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.check_players_in_cutscene()
|
||||
for player_name, response in pairs(global.inventory_sync.players_in_cutscene_to_sync) do
|
||||
if not is_in_cutscene(response.player) then
|
||||
global.inventory_sync.players_in_cutscene_to_sync[player_name] = nil
|
||||
inventory_sync.sync_player(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.sync_player(acquire_response)
|
||||
local player = game.get_player(acquire_response.player_name)
|
||||
|
||||
-- Editor mode is not supported.
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
player.toggle_map_editor()
|
||||
end
|
||||
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
log("ERROR: Inventory sync failed, unable to switch " .. player.name .. " out of editor mode")
|
||||
player.print("ERROR: Inventory sync failed, unable to switch out of editor mode")
|
||||
return
|
||||
end
|
||||
|
||||
local download_record = global.inventory_sync.active_downloads[acquire_response.player_name]
|
||||
if download_record then
|
||||
-- Ignore the response if it affirms the current active download
|
||||
if acquire_response.status == "acquired" and download_record.generation == acquire_response.generation then
|
||||
game.print("Continuing active download")
|
||||
return
|
||||
end
|
||||
|
||||
-- This is the excaptional case where a long download started from
|
||||
-- the previous time the player joined is still ongoing and we just
|
||||
-- received a message saying that it's not valid any more.
|
||||
|
||||
if acquire_response.status == "error" or acquire_response.status == "timeout" then
|
||||
-- Let the current download finish or fail on its own, we may end up
|
||||
-- duplicating items or giving the player a stale inventory here
|
||||
-- but it's better than throwing away all the downloaded data.
|
||||
return
|
||||
end
|
||||
|
||||
if acquire_response.status == "acquired" then
|
||||
-- The stored data on the master server has changed since the
|
||||
-- download was started. Restart it.
|
||||
game.print("Restarting download")
|
||||
download_record.restart = true
|
||||
return
|
||||
end
|
||||
|
||||
if acquire_response.status == "busy" then
|
||||
-- Somehow another instance has claimed the player while we're
|
||||
-- downloading it. Abort it and send failure to the player.
|
||||
global.inventory_sync.active_downloads[acquire_response.player_name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local player_record = global.inventory_sync.players[acquire_response.player_name]
|
||||
if acquire_response.status ~= "acquired" then
|
||||
if player_record.sync and not player_record.dirty then
|
||||
if player.controller_type == defines.controllers.character then
|
||||
local character = player.character
|
||||
player.set_controller({ type = defines.controllers.spectator })
|
||||
character.destroy()
|
||||
end
|
||||
player_record.sync = false
|
||||
end
|
||||
dialog_failed_download.create(player, acquire_response)
|
||||
return
|
||||
end
|
||||
|
||||
if
|
||||
acquire_response.has_data
|
||||
and (not player_record.sync or acquire_response.generation > player_record.generation)
|
||||
then
|
||||
inventory_sync.initiate_inventory_download(player, player_record, acquire_response.generation)
|
||||
|
||||
else
|
||||
if acquire_response.generation == 0 then
|
||||
player.print(
|
||||
"Your inventory will be automatically synchronized between servers in this cluster. " ..
|
||||
"To improve sync performance, please avoid storing large blueprints in your inventory.",
|
||||
{0.75, 0.75, 1}
|
||||
)
|
||||
end
|
||||
|
||||
player_record.sync = true
|
||||
player_record.dirty = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Download inventory from master
|
||||
inventory_sync.events[defines.events.on_player_joined_game] = function(event)
|
||||
-- Send acquire request even if an active download is currently in plogress
|
||||
local player = game.get_player(event.player_index)
|
||||
inventory_sync.acquire(player, false)
|
||||
|
||||
-- Clear active upload if it existsi
|
||||
global.inventory_sync.active_uploads[player.name] = nil
|
||||
end
|
||||
|
||||
inventory_sync.on_nth_tick = {}
|
||||
inventory_sync.on_nth_tick[33] = function(event)
|
||||
-- Periodically check players currently in the process of syncing
|
||||
inventory_sync.check_players_waiting_for_acquire()
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
inventory_sync.check_active_downloads()
|
||||
inventory_sync.check_active_uploads()
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_cutscene_waypoint_reached] = function(event)
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_cutscene_cancelled] = function(event)
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
end
|
||||
|
||||
-- Upload inventory when a player leaves the game. Triggers on restart after crash if player was online during crash.
|
||||
inventory_sync.events[defines.events.on_pre_player_left_game] = function(event)
|
||||
-- for some reason, on_pre_player_left_game gets called before on_server_startup so global isn't ready yet
|
||||
if not global.inventory_sync then
|
||||
log("ERROR: Global inventory sync not defined")
|
||||
return
|
||||
end
|
||||
|
||||
local player = game.get_player(event.player_index)
|
||||
local player_record = global.inventory_sync.players[player.name]
|
||||
|
||||
-- If player has an active download release our acquisition but let the download continue
|
||||
if global.inventory_sync.active_downloads[player.name] then
|
||||
clusterio_api.send_json("inventory_sync_release", { player_name = player.name })
|
||||
return
|
||||
end
|
||||
|
||||
if not player_record.dirty or not player_record.sync then
|
||||
return
|
||||
end
|
||||
|
||||
player_record.generation = player_record.generation + 1
|
||||
global.inventory_sync.active_uploads[player.name] = {
|
||||
last_attempt = game.ticks_played,
|
||||
timeout = math.random(600, 1200), -- Start with 10-20 seconds for the timeout
|
||||
}
|
||||
inventory_sync.upload_inventory(player, player_record)
|
||||
end
|
||||
|
||||
-- Invoked by the instance code to confirm the master has received the uploaded data
|
||||
function inventory_sync.confirm_upload(player_name, generation)
|
||||
local player = game.get_player(player_name)
|
||||
if not player or player.connected then
|
||||
log("no player, or player connected")
|
||||
return
|
||||
end
|
||||
local player_record = global.inventory_sync.players[player.name]
|
||||
if not player_record or player_record.generation ~= generation then
|
||||
return
|
||||
end
|
||||
|
||||
if not player_record.dirty then
|
||||
log("ERROR: Received upload confirmation for " .. player_name .. " without the dirty flag")
|
||||
end
|
||||
if not player_record.sync then
|
||||
log("ERROR: Received upload confirmation for " .. player_name .. " without the sync flag")
|
||||
end
|
||||
log("Confirmed upload of " .. player_name)
|
||||
global.inventory_sync.active_uploads[player_name] = nil
|
||||
player_record.dirty = false
|
||||
end
|
||||
|
||||
function inventory_sync.initiate_inventory_download(player, player_record, generation)
|
||||
player.print("Initiating inventory download...")
|
||||
local record = {
|
||||
started = game.ticks_played,
|
||||
last_active = game.ticks_played,
|
||||
generation = generation,
|
||||
data = ""
|
||||
}
|
||||
global.inventory_sync.active_downloads[player.name] = record
|
||||
|
||||
clusterio_api.send_json("inventory_sync_download", {
|
||||
player_name = player.name
|
||||
})
|
||||
|
||||
-- If this is a synced player turn them into a spectator while the
|
||||
-- player data is downloading
|
||||
if player_record.sync then
|
||||
local character = player.character
|
||||
player.set_controller({ type = defines.controllers.spectator })
|
||||
if character ~= nil then character.destroy() end
|
||||
|
||||
-- Store original position to teleport back to
|
||||
record.surface = player.surface
|
||||
record.position = player.position
|
||||
|
||||
-- Indicate this state shouldn't be persisted
|
||||
player_record.dirty = false
|
||||
end
|
||||
|
||||
progress_dialog.display(player, 0, 1)
|
||||
end
|
||||
|
||||
function inventory_sync.check_active_downloads()
|
||||
-- Used for detecting the inventory download failing, like when the map is saved and
|
||||
-- re-loaded in the middle of a download. The player will be returned to a playable state.
|
||||
for player_name, record in pairs(global.inventory_sync.active_downloads) do
|
||||
if record.last_active <= game.ticks_played - 600 then
|
||||
global.inventory_sync.active_downloads[player_name] = nil
|
||||
local player = game.get_player(player_name)
|
||||
if player then
|
||||
log("ERROR: Inventory download failed for " .. player_name)
|
||||
|
||||
if player.connected then
|
||||
-- Show GUI with option to retry or abort download
|
||||
dialog_failed_download.create(player, {
|
||||
player_name = player_name,
|
||||
status = "error",
|
||||
message = "Inventory download failed",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.check_active_uploads()
|
||||
for player_name, record in pairs(global.inventory_sync.active_uploads) do
|
||||
if record.last_attempt + record.timeout < game.ticks_played then
|
||||
local player_record = global.inventory_sync.players[player_name]
|
||||
local player = game.get_player(player_name)
|
||||
if not player_record or not player then
|
||||
global.inventory_sync.active_uploads[player_name] = nil
|
||||
else
|
||||
log("Retrying upload for " .. player_name)
|
||||
record.last_attempt = game.ticks_played
|
||||
record.timeout = math.random(record.timeout, record.timeout * 2)
|
||||
inventory_sync.upload_inventory(player, player_record)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return inventory_sync
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Load crafting queue from a table
|
||||
local function load_crafting_queue(crafting_queue, player)
|
||||
local inventory = player.get_main_inventory()
|
||||
|
||||
-- Give player some more inventory space to avoid duplicating items
|
||||
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1000
|
||||
|
||||
-- Add items to inventory
|
||||
for _, item in pairs(crafting_queue.ingredients) do
|
||||
inventory.insert(item)
|
||||
end
|
||||
|
||||
-- Load crafting queue
|
||||
for _, queueItem in pairs(crafting_queue.crafting_queue) do
|
||||
-- Start crafting (consume items)
|
||||
player.begin_crafting {
|
||||
count = queueItem.count,
|
||||
recipe = queueItem.recipe,
|
||||
-- silent = true, -- Fail silently if items are missing
|
||||
}
|
||||
end
|
||||
|
||||
-- Remove extra inventory slots
|
||||
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1000
|
||||
end
|
||||
|
||||
return load_crafting_queue
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "inventory_sync",
|
||||
"load": ["inventory_sync.lua", "save_crafts.lua", "restore_crafts.lua", "gui/dialog_failed_download.lua"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
local load_crafting_queue = require("modules/inventory_sync/load_crafting_queue")
|
||||
|
||||
local restore_crafts = {}
|
||||
|
||||
function restore_crafts.command(event)
|
||||
if event.player_index then
|
||||
local player = game.get_player(event.player_index)
|
||||
local crafting_queue = global.inventory_sync.saved_crafting_queue[player.name]
|
||||
|
||||
if crafting_queue == nil then
|
||||
player.print("No saved crafting queue")
|
||||
return
|
||||
end
|
||||
|
||||
load_crafting_queue(crafting_queue, player)
|
||||
|
||||
global.inventory_sync.saved_crafting_queue[player.name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function restore_crafts.add_commands()
|
||||
commands.add_command("restore-crafts", "Load crafting queue from global", restore_crafts.command)
|
||||
commands.add_command("crc", "Shorthand for /restore-crafts", restore_crafts.command)
|
||||
end
|
||||
|
||||
return restore_crafts
|
||||
@@ -0,0 +1,103 @@
|
||||
local save_crafts = {}
|
||||
|
||||
function save_crafts.command(event)
|
||||
if event.player_index then
|
||||
local player = game.get_player(event.player_index)
|
||||
local crafting_queue = {}
|
||||
|
||||
-- Avoid overwriting old save
|
||||
if global.inventory_sync.saved_crafting_queue[player.name] ~= nil then
|
||||
player.print("You already have a saved crafting queue, restore it with /restore-crafts")
|
||||
return
|
||||
end
|
||||
|
||||
-- Give player some more inventory space to avoid duplicating items
|
||||
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1000
|
||||
|
||||
-- Save current items
|
||||
local inventory = player.get_main_inventory()
|
||||
local old_items = inventory.get_contents()
|
||||
|
||||
-- Cancel old crafts to get the items back
|
||||
while player.crafting_queue_size > 0 do
|
||||
local old_queue = player.crafting_queue
|
||||
local queueItem = player.crafting_queue[1]
|
||||
|
||||
-- Cancel craft
|
||||
player.cancel_crafting {
|
||||
index = 1,
|
||||
count = 1,
|
||||
}
|
||||
|
||||
local rightmost_right_index = 0 -- 0 indexed since it is subtractive in a 1 indexed language
|
||||
local new_queue = player.crafting_queue
|
||||
while
|
||||
new_queue ~= nil and
|
||||
new_queue[#new_queue - rightmost_right_index] ~= nil and
|
||||
new_queue[#new_queue - rightmost_right_index].count >= old_queue[#old_queue - rightmost_right_index].count
|
||||
do
|
||||
rightmost_right_index = rightmost_right_index + 1
|
||||
end
|
||||
local oldItem = old_queue[#old_queue - rightmost_right_index]
|
||||
local newItem = nil
|
||||
if new_queue ~= nil then
|
||||
newItem = new_queue[#new_queue - rightmost_right_index]
|
||||
end
|
||||
|
||||
-- Figure out how many items to add to queue
|
||||
local added = oldItem.count
|
||||
if newItem ~= nil then
|
||||
added = oldItem.count - newItem.count
|
||||
if oldItem.recipe ~= newItem.recipe then
|
||||
log("ERROR: Old item "..oldItem.recipe.." is not equal "..newItem.recipe)
|
||||
end
|
||||
end
|
||||
|
||||
-- If the last item we added was of the same type, merge them in the queue
|
||||
if #crafting_queue > 0 and crafting_queue[#crafting_queue].recipe == oldItem.recipe then
|
||||
crafting_queue[#crafting_queue].count = crafting_queue[#crafting_queue].count + added
|
||||
else
|
||||
-- If the last item was of a different type, add a new item to the queue
|
||||
table.insert(crafting_queue, {
|
||||
recipe = oldItem.recipe,
|
||||
count = added,
|
||||
})
|
||||
end
|
||||
-- game.print("Saved craft "..oldItem.recipe)
|
||||
end
|
||||
|
||||
-- Find amount of items added and remove from inventory
|
||||
local new_items = inventory.get_contents()
|
||||
local difference = {}
|
||||
for k,v in pairs(new_items) do
|
||||
local old_count = old_items[k]
|
||||
local diff = v
|
||||
if old_count ~= nil then
|
||||
diff = diff - old_count
|
||||
end
|
||||
if diff > 0 then
|
||||
local ingredient = {
|
||||
name = k,
|
||||
count = diff,
|
||||
}
|
||||
inventory.remove(ingredient)
|
||||
table.insert(difference, ingredient)
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove extra inventory slots
|
||||
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1000
|
||||
|
||||
global.inventory_sync.saved_crafting_queue[player.name] = {
|
||||
crafting_queue = crafting_queue,
|
||||
ingredients = difference,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function save_crafts.add_commands()
|
||||
commands.add_command("save-crafts", "Save crafting queue for transport to a different server", save_crafts.command)
|
||||
commands.add_command("csc", "Shorthand for /save-crafts", save_crafts.command)
|
||||
end
|
||||
|
||||
return save_crafts
|
||||