Compare commits

...
Author SHA1 Message Date
Hornwitser 67002e4b71 v2.0.0-alpha.5 2021-03-23 21:23:22 +01:00
Hornwitser 166416339a Merge pull request #373 from Hornwitser/alpha-5
Alpha 5 preparations
2021-03-23 21:18:11 +01:00
Hornwitser ead2c6d2c1 Add release checklist
Add instructions for doing releases to npm to the contributing docs.
2021-03-23 20:37:32 +01:00
Hornwitser bfea6cb28e Don't log print permission denied stack traces
Use PermissionError to signal when permission is denied for an action
and prevent them being logged as stack traces in the master log.
2021-03-23 20:09:09 +01:00
Hornwitser 7ecac539f9 Fix path stripping for relative and Windows paths
Fix path stripping logic to take into account the possibility of the
writePath being relative, and that Windows uses \ as a path separator
for some paths but not all.
2021-03-23 20:09:09 +01:00
Hornwitser e73599a800 Merge pull request #372 from Hornwitser/storage-page
Storage page
2021-03-23 20:02:31 +01:00
Arvids Godjuks f46f40ad4a Merge pull request #363 from drouarb/master
MacOS compatibility
2021-03-23 20:59:44 +02:00
Benjamin DROUARD 058197dc99 Using factorio.app/Contents for MacOS 2021-03-21 16:43:32 +01:00
Hornwitser 8fb6784964 Load plugin infos in integration tests
Make sure the permisisons defined by plugins are included in the
integration test environment by loading the plugin infos.
2021-03-20 17:03:23 +01:00
Hornwitser c16dc74ad9 Reimplement the Storage page
Reimplement the functionality of the Storage page which shows which
items are stored in the shared subspace on the master server.
2021-03-20 03:35:05 +01:00
Hornwitser cebe83542f Add access to icons and locale from export
Add models for accessing and using the icons and locale that's exported
when an instance export is runned.
2021-03-20 03:35:05 +01:00
Hornwitser 6c19145734 Add control reference to web plugins
Add reference to the Control instance in BaseWebPlugin for web plugin
code to use in a similar manner to the instance reference in instance
plugin code.
2021-03-20 03:35:05 +01:00
Hornwitser 0a6e26bb2d Add connection events for control connections
Add hooks for plugins to be able to handle connection state changes for
control connections on the master server and in the web interface.
2021-03-20 03:35:05 +01:00
Hornwitser de629d9416 Pass link to plugin message handlers
For master plugins a message can be received from any of the clients
corrently connected to the master.  In order for plugins to be able to
handle link specific requests they need a reference to the link messages
was received from.  Add link as the third argument to plugin message
handlers.
2021-03-20 03:35:05 +01:00
Hornwitser 48786f1e59 Fix --dev-plugin breaking after bd53339
The automatic determination of the public path for plugins works
correctly when they are build for production and loaded from their
respective paths.  But when the dev middelware compiles it, it doesn't
know the correct path to host it at withouth the publicPath being set.

Fix by setting publicPath explicitly when compiling with the dev
middleware.
2021-03-20 03:35:05 +01:00
Hornwitser 3d26371fa9 Make web pages pluggable
Refactor the page handling of the web interface to allow plugins to add
their own pages.
2021-03-20 03:35:05 +01:00
Hornwitser 73c713462c Merge pull request #371 from Hornwitser/account-button
Account button
2021-03-20 03:11:16 +01:00
Hornwitser 16d2ab1646 Fix logging in not saving token
Fix regression in login logic causing the token to be saved as null
instead of the real value.
2021-03-20 02:52:34 +01:00
Hornwitser f9048d7027 Set UI color on chrome mobile 2021-03-18 23:05:59 +01:00
Hornwitser d627c5e094 Fix issues with reconnect and logging out
Fix problems encountered logging out and in while the connection has
dropped and is in the process of reconnecting.
2021-03-18 23:05:59 +01:00
Hornwitser 6458810571 Add name to account model
Add the name of the account that's connected to the control link on the
web interface via an additional property on the ready payload for the
control link.
2021-03-18 23:05:47 +01:00
Hornwitser e1549c1b0c Add account button with log out
Add a button on the right of the top nav bar to access account related
functions and add a log out button there.  To make it fit on the navbar
the text and version is hidden on narrow layouts.
2021-03-18 21:52:36 +01:00
Hornwitser 9542246414 Use a diff and author friendly markdown formatting (#370)
Simplify the the way markdown files are formatted to make them easier to
author without editors that do auto line breaking, and make the diffs
from them simpler to read.
2021-03-17 21:30:51 +01:00
Hornwitser a2d56d4a8d Fix crash on continuing an active connection (#369)
Fix ServerConnector reaching an invalid state if a continue operation is
performed on an active connection.  The continue would cause the
connection established logic to be runned before the connection lost
logic of the pervious connection is perfomed, resulting in a leaked
heartbeat interval and the connection lost logic being performed on the
new connection instead of the old.  Prevent this by closing both the new
and the old connection in this case.
2021-03-17 20:20:12 +01:00
Hornwitser 72d0994ca6 Add Player Auth plugin (#368)
Add authentication plugin that does a simple challenge response protocol
to verify that the player trying to log in is who they claim to be.  The
process goes as follows:

1. A player code is generated for a player that wants to log in and is
   displayed in-game.
2. The player code is input into the web interface and the master
   generates a verification code as a challenge back along with a token.
3. The player inputs the verification code in-game and is logged in to
   the web interface in doing so.

To do the actual login the web interface polls a verify endpoint every 2
seconds until the code has either been verified or expired.  The
challenge comes from being unable to control the verification code and
having to input it as a player logging into one of the servers in order
to login.

Some additional steps are taken in order to increase the difficulty for
an attacker to subvert the challenge:

- The login process must be started from in-game.
- Generated codes are only valid for a limited timespan which is
  configurable and 2 minutes by default.
- The verification code comes with a token that only works with one
  player code and verification code combo and must be provided to pass
  validation.
2021-03-17 18:48:22 +01:00
Hornwitser f07082b52d Merge pull request #367 from Hornwitser/player-auth-plugin-preparations
Web Plugin preparations
2021-03-17 18:16:19 +01:00
Hornwitser 941639666d Use auto publicPath deduction
Change to "auto" as the publicPath to have webpack deduce the correct
path at runtime.  This fixes an issue with plugins being unable to be
loaded due to trying to load bundles from the wrong path when the web
interface is hosted under a proxied sub-path.
2021-03-17 18:00:12 +01:00
Hornwitser bd53339532 Fix web root path confusion
Remove basename in favor of making webRoot absolute.  The distinction
between the two were supposed to be that basename was absolute and did
not have a trailing slash while webRoot was relative and had a traling
slash.  This was not how it ended up working in the code, which lead to
double forward slashes in requests sent out when webRoot was not equal
to './'.
2021-03-17 17:59:35 +01:00
Hornwitser 28da8dcc58 Merge pull request #366 from Hornwitser/plugin-improvements
Plugin improvements
2021-03-15 21:54:39 +01:00
Hornwitser ceae2b25b5 Make the login form pluggable
Allow extending the login form with additional authentication methods by
adding them to the web plugin's loginForms array.
2021-03-13 21:01:53 +01:00
Hornwitser 579f6421e4 Export web_ui as a single module
Due to the way the imports and exports works with module fedderation
each require path needs it's own shared module in the plugin.  To avoid
having to specify every possible import in the module federation plugin
config, export all useful components to plugins in the browser
entrypoint for web_ui.

Note that for some reason the module wouldn't exist for import by
plugins unless some part of the build imported it.  Which is the reason
for the master index.jsx to import bootstrap via it.
2021-03-13 21:01:53 +01:00
Hornwitser a904a8fb9d Add hit counter for HTTP requests
Add a complementary total hit counter for all HTTP requests.  The
endpointHitCounter doesn't cover all endpoints and fixing it to cover
all endpoints without making it possible for it to create an infinite
number of time seriers is not straight forward.
2021-03-13 21:01:53 +01:00
Hornwitser b70303c437 Add error boundary to App
Catch errors in the component stack that reaches the App component and
display an error message for them instead of a black screen.
2021-03-13 21:01:53 +01:00
Hornwitser a8e1971483 Fix data loss on bad name passed --dev-plugin
If data from the database is not loaded but a shutdown is triggered then
the master server will overwrite the database with empty data, causing
unexpected data loss.  This occurs for example when the --dev-plugin
option throws.

Fix by setting the database entries to null before they are loaded so
that should an attempt be made to save them without having been loaded
first it will throw an error instead.
2021-03-13 21:01:53 +01:00
Hornwitser 78ce6fc382 Move /plugin/name/ to /plugins/name/
The web files for plugins were hosted at /plugin/plugin_name/ while the
view page for a plugin is hosted at /plugins/plugin_name/view.  Reduce
confusion by making both urls use the plural version.
2021-03-13 21:01:53 +01:00
Hornwitser 71804df3e0 Add --dev-plugin to compile plugins on the fly
Add hidden development option to run the webpack compiller for the given
plugin(s) similar to the --dev option for the built-in web interface.

This requires publicPath to be set in the webpack config and an updated
version of webpack-dev-middleware to function correctly.
2021-03-13 21:01:53 +01:00
Hornwitser 5ce3fb6cc6 Add loading of web plugins in interface
Add separate BaseWebPlugin class which plugins can extend and expose
through a webEntrypoint in a similar fasion to the existing master,
instance and control entrypoints.

To simplify the logic for the rest of the web interface code the plugins
context is changed to be a mapping of loaded WebPlugin instances.  This
in turn means the plugins and plugin view pages fetches the plugin list
upon load in order to show plugins that failed to load (which
incidentally also gives them an up to date list).
2021-03-13 21:01:53 +01:00
Hornwitser 6c4e094d44 Mention plugins by Hornwitser in README 2021-03-07 15:56:27 +01:00
Hornwitser f007dd640d Fix statistics exporter name and description 2021-03-07 15:32:32 +01:00
Hornwitser e1c7d35ce4 Fix plugin shutdown not called on bad startup
During startup the plugins are loaded one by one and init is called for
each of them.  But due to the already loaded plugins being discarded if
one of the plugins throw an error an unclean shutdown that might hang
on timers started by plugins occurs.  This leads to the master server
starting up, printing an error, displaying the shutdown sequence and
then hang forever.

Fix by storing the plugins loaded into masterPlugins as they are loaded
instead only after all of them have been loaded.
2021-03-07 15:18:39 +01:00
Hornwitser a17e9295e7 Use relative imports in lib
Use relative imports in lib to avoid an issue with webpack thinking
absolute imports inside @clusterio/lib are for a separate module and
giving out warnings for it being missing in package.json.
2021-03-07 15:14:38 +01:00
Hornwitser b814d8a400 Share each individual lib import webpack config
The module sharing in Webpack works on a per import basis and therefore
doesn't trigger when the value in the config is @clusterio/lib while the
module imported is @clusterio/lib/config.  This in turn caused a copy of
lib to be bundled with each plugin.  Fix by making sure each lib import
is shared and specified in the configs.
2021-03-07 15:12:23 +01:00
Hornwitser fe9cef5e1e Log stack trace on errors loading web plugins 2021-03-07 14:56:48 +01:00
Hornwitser 415f574e2d Remove obsolete "updater" code
Remove commented out reference to an updater module which was never
completed in master.js.
2021-03-07 14:55:57 +01:00
Hornwitser 63f76ce87a Rename enabled to load_plugin
Rename the config and status indicating whether to load a plugin or not
from "Enabled" to "Load Plugin".  The latter better describes what it
does, and unlike the former doesn't imply plugins can be enabled or
disabled on the fly.
2021-03-07 14:53:45 +01:00
David Hiendl 19259b2226 Add .json files to symlinkMods whitelist (#365) 2021-03-07 13:35:46 +01:00
Hornwitser 53edd7cce6 Merge pull request #364 from Hornwitser/fix-windows-2
Fix windows test failure
2021-03-07 13:34:57 +01:00
Hornwitser 2069689780 Remove --exit flag from tests
When setting up the tests there were issues with code that didn't
properly clean up after itself and left lingering connections, intervals
or timers that kept the event loop running after the tests where done.
To fix this the --exit flag was passed as a temporary measure but then
subsequently forgotten about.  The issues with keeping the event loop
alive have long since been fixed on Linux, but on Windows there was a
problem with the control connector not exiting because the master server
was killed instead of shutten down properly.

With that issue fixed there are no longer any connections or timers
keeping the event loop alive and --exit can thus be removed.
2021-03-06 18:40:07 +01:00
Hornwitser d6f09f4c1a Fix close during reconnect wait period
If the connect close method is called while the connection is waiting
for a reconnection attempt then the attempt is cancelled but the close
method then erroneously waited for a close event that would never
happen.  Fix by resetting the connector and rasing the close event in
the close method if it's in the waiting for reconnect state.
2021-03-06 18:38:30 +01:00
Hornwitser 85a984734b Fix node-forge dependency
The removal of the automatic TLS certificate generation removed the use
of node-forge in the master server, while the addition of the HttpCloser
added it as dependency of the tests.  Fix node-forge dependency being
listed in lib and master instead of as a dev dependency in package.json.
2021-03-06 18:38:30 +01:00
Benjamin DROUARD b78eafaedb MacOS compatibility 2021-03-05 17:31:14 +01:00
Hornwitser 641d8e9fc5 Merge pull request #362 from Hornwitser/plugin-list
Add plugin list
2021-03-05 16:58:15 +01:00
Hornwitser bdd70e53dc Fix errors detected by linter
I meant to fix these changes before pushing the PR, but forgot about it.
2021-03-03 14:41:55 +01:00
Hornwitser 7797ee708a Add ctl master plugin list command
Add command to list the plugins enabled on the master server and their
versions to clusterioctl.
2021-03-01 15:03:25 +01:00
Hornwitser 6c9d3d8893 Add Plugins page to web interface
Add page listing the plugins installed on the master along with a
detailed page for each plugin with fields from their package.json.
Failure to load the module and version missmatch is also reported.
This adds ./packages.json as required part of the webpack module.
2021-03-01 15:03:25 +01:00
Hornwitser 3a3a7597bf Add PluginsContext to web_ui
Add a React context to access the plugins loaded in the web interface.
The info, /api/plugins metadata and webpack container is provided for
each plugin.
2021-03-01 15:03:25 +01:00
Hornwitser d0befe5944 Add error boundnary for sidebar paths too
b538960 added an error boundnary for pages not directly linked on the
sidebar, but forgot to add it for pages on the sidebar.  This add the
missing error boundnary for those pages.
2021-03-01 15:03:25 +01:00
Hornwitser a3729504e5 Log error stack on bootstrap failure
Log the stack of the error caught during failure to load the web
interface.  This ensure the error is printed to the console instead of
"[fatal] undefined".
2021-03-01 15:03:25 +01:00
Hornwitser 4efd09de31 Expose computed tls_ca config
Provide the loaded file of the tls_ca config available to slave,
instance and ctl code.  This ensures they can easily use it when
connecting to the master server.
2021-03-01 15:03:25 +01:00
Hornwitser 6a29cd9715 Resolve webpack modules from web_ui
Remove the need for installing babel and the webpack loaders used in the
webpack.common.js file separately by resolving them from the web_ui
package.  This allows future changes to done to the modules used without
having to update them in each plugin that uses the web_ui common config.
2021-03-01 14:59:59 +01:00
96 changed files with 3747 additions and 2009 deletions

No files matched your search

+107 -168
View File
@@ -1,60 +1,48 @@
Changelog
=========
# Changelog
Version 2.0.0
-------------
## Version 2.0.0
### Major Features
- Replaced Hotpatch with save patching. Removed the Hotpatch scenario and
the depency on it for getting code into the game. Added a save patcher than
runs before starting up Factorio that patches in lua modules based on the
event_loader lib into the savegame. Regular freeplay games can now be
used with Clusterio and will be compatible without having to convert them
to Hotpatch.
- Daemonized slaves. Slaves now have the ability to run multiple Factorio
instances, starting and stopping them individually. To manage this the
local command line interface has been replaced with a remote interface on
the master server that can be accessed through the clusterioctl cli tool.
- Rewritten the communication between slaves and the master from scratch. The
new system is based on a WebSocket connection between the slaves and the
master server and provides efficient validated bi-directonal communication.
- Rewritten the plugin system from scratch. Plugins now inherit from
a base class and use the same WebSocket connection Clusterio uses to
communicate.
- New configuration system with support for initializing configs when needed,
modifying config entries using built-in commands, and updating instance
configuration remotely.
- Packaged and uploaded to npm again, greatly simplifying distribution and
installation of both Clusterio and plugins.
- Added centralized logging for the cluster. All logs from both Clusterio
and Factorio is stored in a shared log which can be inspected and queried.
- Replaced Hotpatch with save patching.
Removed the Hotpatch scenario and the depency on it for getting code into the game.
Added a save patcher than runs before starting up Factorio that patches in lua modules based on the event_loader lib into the savegame.
Regular freeplay games can now be used with Clusterio and will be compatible without having to convert them to Hotpatch.
- Daemonized slaves.
Slaves now have the ability to run multiple Factorio instances, starting and stopping them individually.
To manage this the local command line interface has been replaced with a remote interface on the master server that can be accessed through the clusterioctl cli tool.
- Rewritten the communication between slaves and the master from scratch.
The new system is based on a WebSocket connection between the slaves and the master server and provides efficient validated bi-directonal communication.
- Rewritten the plugin system from scratch.
Plugins now inherit from a base class and use the same WebSocket connection Clusterio uses to communicate.
- New configuration system with support for initializing configs when needed, modifying config entries using built-in commands, and updating instance configuration remotely.
- Packaged and uploaded to npm again, greatly simplifying distribution and installation of both Clusterio and plugins.
- Added centralized logging for the cluster.
All logs from both Clusterio and Factorio is stored in a shared log which can be inspected and queried.
### Features
- Added export of pollution statitics.
- Reconnection logic that esures no data is dropped talking to the master
server provided the session can be resumed.
- Added support for having multiple Factorio installs and selecting which
version to use on a per instance basis.
- Added config to specify which path the master interface is accessed under,
allowing it to be proxied behind web-server.
- Reconnection logic that esures no data is dropped talking to the master server provided the session can be resumed.
- Added support for having multiple Factorio installs and selecting which version to use on a per instance basis.
- Added config to specify which path the master interface is accessed under, allowing it to be proxied behind web-server.
- Added WebSocket usage statistics.
- Added commands sent statistic.
- Added Factorio server CPU and memory usage statistic.
- Added Factorio autosave size statistic.
- Added support for extracting locale and item icons from Factorio and mods.
- Added integrated user management for controlling access and storing per
player data.
- Added synchronization of in-game adminlist, banlist and whitelist to
Clusterio core. Previously this was handled by the playerManager plugin.
- Added integrated user management for controlling access and storing per player data.
- Added synchronization of in-game adminlist, banlist and whitelist to Clusterio core.
Previously this was handled by the playerManager plugin.
- Added stripping of long paths in the Factorio server log.
- Added option to configure maximum number of commands to send in parallel.
- Added option to auto start instances on slave startup.
- Added metric mapping of slave and instance ids to their names, allowing
them to be displayed by name in queries that join with the mapping.
- Added metric mapping of slave and instance ids to their names, allowing them to be displayed by name in queries that join with the mapping.
- Added option to configure timeout for exported Prometheus metrics.
- Added option to set address to bind HTTP(S) port to.
- Added an overview of the installed plugins to the web interface.
- Added clusterioctl command to list plugins on the master server.
- Added Player Auth plugin for logging in to the web interface by proving the ability to log in to a server as a given user.
### Changes
@@ -62,186 +50,137 @@ Version 2.0.0
- Factorio game and rcon port now defaults to a random port above 49151.
- Removed unimplemented mods update command.
- Fixed rcon password being generated with Math.random().
- Added plugins directory to the views path. This makes it possible for
plugins to render their own ejs views or pages in their own folders by
using paths of the format "pluginName/path/to/page-or-view".
- Replaced the per instance copy of the shared Factorio mods with
symlinks. On Windows hard links are used instead due to the
privileges requirements of symlinks.
- Changed the per instance scenario folder to be linked instead of
copied on instance creation.
- Instance id is no longer derived from the rcon password, instead it's
generated upon instance creation and stored in the instance config.
- The game port, RCON port and RCON password instance config entries are are
now null by default, indicating that a random one will be generated every
time the instance is started.
- Removed the broken client download command as this would download
pre-releases.
- Moved the item database and HTTP definitions for /api/place, /api/remove,
/api/inventory, /api/inventoryAsObject and the web interface view for the
storage page into the clusterioMod plugin. If you disable this plugin
then these things will not be available.
- Added plugins directory to the views path.
This makes it possible for plugins to render their own ejs views or pages in their own folders by using paths of the format "pluginName/path/to/page-or-view".
- Replaced the per instance copy of the shared Factorio mods with symlinks.
On Windows hard links are used instead due to the privileges requirements of symlinks.
- Changed the per instance scenario folder to be linked instead of copied on instance creation.
- Instance id is no longer derived from the rcon password, instead it's generated upon instance creation and stored in the instance config.
- The game port, RCON port and RCON password instance config entries are are now null by default, indicating that a random one will be generated every time the instance is started.
- Removed the broken client download command as this would download pre-releases.
- Moved the item database and HTTP definitions for /api/place, /api/remove, /api/inventory, /api/inventoryAsObject and the web interface view for the storage page into the clusterioMod plugin.
If you disable this plugin then these things will not be available.
- Removed undocumented --port and --rcon-port arguments to client.
- Removed undocumented FACTORIOPORT and RCONPORT enviroment variable
handling in client.
- Removed undocumented FACTORIOPORT and RCONPORT enviroment variable handling in client.
- Removed redundant call to /api/slaves in globalChat plugin.
- Removed broken serverManager plugin.
- Removed factorio_version from config. The version installed is auto
detected and used instead.
- Removed factorio_version from config.
The version installed is auto detected and used instead.
- Removed the playerManager specific command CLI tools/delete_player.js.
- Creating an instance, assigning it to a slave, creating a save for an
instance and starting an instance is now four separate commands.
- Creating an instance, assigning it to a slave, creating a save for an instance and starting an instance is now four separate commands.
- Removed oddball limits to slaves.json size.
- Moved slave specific and instance specific configuration into their own
configuration files.
- Moved slave specific and instance specific configuration into their own configuration files.
- Removed unused binary option from plugin config.
- Removed info and shout command from globalChat plugin
- Removed mirrorAllChat and enableCrossServerShout configuration options for
globalChat plugin.
- Removed UPSdisplay plugin. UPS statistics is exported by the statistics
exporter plugin.
- Removed info and shout command from globalChat plugin - Removed mirrorAllChat and enableCrossServerShout configuration options for globalChat plugin.
- Removed UPSdisplay plugin.
UPS statistics is exported by the statistics exporter plugin.
- Master server now defaults to hosting on https on port 8443.
- Renamed client to slave.
### Breaking Changes
- Removed lib/authenticate. Breaks playerManager.
- Removed lib/authenticate.
Breaks playerManager.
- The masterIP and masterPort config entries has been merged into masterURL.
Breaks discordChat, playerManager, serverSelect, trainTeleports and
clusterioModel.
Breaks discordChat, playerManager, serverSelect, trainTeleports and clusterioModel.
- Instance names can no longer be invalid Windows file names.
- Removed config management from the command line and the server manager.
- Moved ejs templates into views folder and changed their extension to
.ejs. Breaks playerManager.
- Moved ejs templates into views folder and changed their extension to .ejs.
Breaks playerManager.
- Mods are no longer copied from the per instance instanceMods directory.
If you need per instance mods you can now place them directly in the mods
directory inside the instance folder.
If you need per instance mods you can now place them directly in the mods directory inside the instance folder.
- Hotpatch scenarios and code loading is no longer compatible with Clusterio.
Breaks playerManager, serverSelect, and tranTeleports.
- Removed getLua and getCommand from lib/clusterTools. If you need to run
more than the most trivial of code in commands use the save patcher and
add in a remote interface.
- Removed mod uploading and distributing from the HTTP interface. Breaks
the old unmaintained and no longer needed factorioClusterioClient.
- Removed the remoteCommands plugin and the old runCommand interface. Breaks
playerManager and external tools sending commands.
- Removed getLua and getCommand from lib/clusterTools.
If you need to run more than the most trivial of code in commands use the save patcher and add in a remote interface.
- Removed mod uploading and distributing from the HTTP interface.
Breaks the old unmaintained and no longer needed factorioClusterioClient.
- Removed the remoteCommands plugin and the old runCommand interface.
Breaks playerManager and external tools sending commands.
- Removed broken serverManager plugin.
- Removed fields info, time, rconPort, rconPassword, serverPort, unique,
mods, instanceName, playerCount, mac, and meta from the slaves in the
slave database.
- Removed getInstanceName and lib/clusterTools. Breaks playerManager,
and discordChat.
- Removed the /api/rconPasswords, and /api/slaves endpoints. Breaks web
interface, trainTeleports, and discordChat.
- Removed the /api/getSlaveMeta and /api/editSlaveMeta endpoints. Breaks
researchSync, and UPSdisplay.
- Removed the hello event from the socket connection handshake. Breaks
playerManager, trainTeleports, serverSelect, and discordChat
- Removed fields info, time, rconPort, rconPassword, serverPort, unique, mods, instanceName, playerCount, mac, and meta from the slaves in the slave database.
- Removed getInstanceName and lib/clusterTools.
Breaks playerManager, and discordChat.
- Removed the /api/rconPasswords, and /api/slaves endpoints.
Breaks web interface, trainTeleports, and discordChat.
- Removed the /api/getSlaveMeta and /api/editSlaveMeta endpoints.
Breaks researchSync, and UPSdisplay.
- Removed the hello event from the socket connection handshake.
Breaks playerManager, trainTeleports, serverSelect, and discordChat
- Changed the format of database/slaves.json.
- Removed the output file subscription system. Breaks inventoryImports,
playerManager, trainTeleports, serverSelect and researchSync.
- Removed the factorioOutput hook from instance plugins. The onOutput
hook provides parsed output instead.
- Removed the output file subscription system.
Breaks inventoryImports, playerManager, trainTeleports, serverSelect and researchSync.
- Removed the factorioOutput hook from instance plugins.
The onOutput hook provides parsed output instead.
- Removed the onLoadFinish hook from master plugins.
- Moved plugins from the sharedPlugins directory to plugins directory.
- Implemented a new plugin system that replaces the old. Breaks all plugins.
- Implemented a new plugin system that replaces the old.
Breaks all plugins.
- Removed express metric http_request_duration_milliseconds metric.
- Removed socket.io metrics socket_io_connected, socket_io_connect_total,
socket_io_disconnect_total, socket_io_events_received_total,
socket_io_events_sent_total, socket_io_recieve_bytes, and
socket_io_transmit_bytes,
- Removed clusterio_connected_instaces_gauge and added
clusterio_master_connected_clients_count in its place.
- Renamed clusterio_player_count_gauge to
clusterio_statistics_exporter_instance_player_count
- Removed clusterio_UPS_gauge and added
clusterio_statistics_exporter_instance_game_ticks_total in its place.
- Removed socket.io metrics socket_io_connected, socket_io_connect_total, socket_io_disconnect_total, socket_io_events_received_total, socket_io_events_sent_total, socket_io_recieve_bytes, and socket_io_transmit_bytes,
- Removed clusterio_connected_instaces_gauge and added clusterio_master_connected_clients_count in its place.
- Renamed clusterio_player_count_gauge to clusterio_statistics_exporter_instance_player_count
- Removed clusterio_UPS_gauge and added clusterio_statistics_exporter_instance_game_ticks_total in its place.
- Renamed clusterio_endpoint_hit_gauge to clusterio_http_enpoint_hits_total
- Renamed clusterio_statistics_gauge to
clusterio_statistics_exporter_instance_force_flows
- Renamed clusterio_statistics_gauge to clusterio_statistics_exporter_instance_force_flows
- Renamed clusterio_nn_dole_gauge to clusterio_subspace_storage_nn_dole_gauge.
- Renamed clusterio_dole_factor_gauge to
clusterio_subspace_storage_dole_factor_gauge.
- Renamed clusterio_dole_factor_gauge to clusterio_subspace_storage_dole_factor_gauge.
- Renamed clusterio_import_gauge to clusterio_subspace_storage_import_total.
- Renamed clusterio_export_gauge to clusterio_subspace_storage_export_total.
- Renamed clusterio_master_inventory_gauge to
clusterio_subspace_storage_master_inventory.
- Removed clusterioMod plugin specific config options logItemTransfers,
disableFairItemDistribution useNeuralNetDoleDivider, autosaveInterval, and
disableImportsOfEverythingExceptElectricity from the master config.
- Removed msBetweenCommands config option. The RCON is instead limited to 5
concurrent commands.
- Removed allowRemoteCommandExecution config option. Remote commands are
always allowed with the move to master managing slaves/instances.
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line
arguments from the master server.
- Implemented a new config system that replaces the old. Breaks all plugins.
- Renamed clusterio_master_inventory_gauge to clusterio_subspace_storage_master_inventory.
- Removed clusterioMod plugin specific config options logItemTransfers, disableFairItemDistribution useNeuralNetDoleDivider, autosaveInterval, and disableImportsOfEverythingExceptElectricity from the master config.
- Removed msBetweenCommands config option.
The RCON is instead limited to 5 concurrent commands.
- Removed allowRemoteCommandExecution config option.
Remote commands are always allowed with the move to master managing slaves/instances.
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line arguments from the master server.
- Implemented a new config system that replaces the old.
Breaks all plugins.
- Removed usage of socket.io entirely in favor of a plain WebSocket connection.
- Renamed clusterioMod plugin to subspace_storage.
- Master no longer creates secret-api-token.txt.
- Removed automatic creation of self-signed TLS certificate.
Version 1.2.4
-------------
## Version 1.2.4
- Removed broken remote combinator signaling.
- Fixed research sync endlessly updating already researched technologies
- Removed obsolete item/fluid statistics from clusterioMod
- Removed per mod upload logging when config.uploadModsToMaster disabled
- Fixed bcrypt failing to install on windows due to the new version not
having Windows binaries.
- Fixed bcrypt failing to install on windows due to the new version not having Windows binaries.
Version 1.2.3
-------------
## Version 1.2.3
- Disabled uploading mods to the master server by default as this is mostly
just a waste of bandwidth with the mod portal being integrated into the
game now.
- Fixed researchSync breaking with heavily modded games due to the tech tree
exceeding the default 100kb limit on JSON payloads.
- Fixed the command inventoryImport sends referencing player instead of
character, and trying to count the non-existant quick bar slots.
- Fixed inventoryImport never receiving the script output after the default
mode for reading script output changed to tail mode.
- Fixed desync caused by the mods and loaded mods arrays in Hotpatch getting
out of sync when plugins update their scenario mod code. To fix existing
games you will need copy lib/scenarios/Hotpatch/hotpatch/mod-tools.lua
over the existing mod-tools.lua in the save's hotpatch folder.
- Disabled uploading mods to the master server by default as this is mostly just a waste of bandwidth with the mod portal being integrated into the game now.
- Fixed researchSync breaking with heavily modded games due to the tech tree exceeding the default 100kb limit on JSON payloads.
- Fixed the command inventoryImport sends referencing player instead of character, and trying to count the non-existant quick bar slots.
- Fixed inventoryImport never receiving the script output after the default mode for reading script output changed to tail mode.
- Fixed desync caused by the mods and loaded mods arrays in Hotpatch getting out of sync when plugins update their scenario mod code.
To fix existing games you will need copy lib/scenarios/Hotpatch/hotpatch/mod-tools.lua over the existing mod-tools.lua in the save's hotpatch folder.
- Updated the Windows install instructions.
Version 1.2.2
-------------
## Version 1.2.2
- Fixed possible crash with modded technologies named the same as a built-in
Object prototype property in researchSync.
- Fixed progress of a current infinite tech carrying over to the next one
when researching it and another node completes it in researchSync.
- Fixed progress of a previous infinite tech from another node being applied
to the current one in researchSync.
- Fixed crash in researchSync when modded technologies are present only on some
nodes.
- Fixed install failing due to bcrypt version less than 3 not being supported
on node v10.
- Reordered install instructions to avoid problem with npm creating files owned
by root in the home directory.
- Swapped curl out with wget in the install instructions as the latter comes
pre-installed on Ubuntu.
- Fixed possible crash with modded technologies named the same as a built-in Object prototype property in researchSync.
- Fixed progress of a current infinite tech carrying over to the next one when researching it and another node completes it in researchSync.
- Fixed progress of a previous infinite tech from another node being applied to the current one in researchSync.
- Fixed crash in researchSync when modded technologies are present only on some nodes.
- Fixed install failing due to bcrypt version less than 3 not being supported on node v10.
- Reordered install instructions to avoid problem with npm creating files owned by root in the home directory.
- Swapped curl out with wget in the install instructions as the latter comes pre-installed on Ubuntu.
### Breaking Changes
- Node.js versions below 10 are no longer supported.
Version 1.2.1
-------------
## Version 1.2.1
- Updated node-factorio-api to v0.3.8 to fix mod downloads randomly breaking
([#229][#229].)
- Fixed SIGINT being sent twice to Factorio server when interrupted by CTRL+C
on Linux ([#217][#217].)
- Updated node-factorio-api to v0.3.8 to fix mod downloads randomly breaking ([#229][#229]).
- Fixed SIGINT being sent twice to Factorio server when interrupted by CTRL+C on Linux ([#217][#217]).
- Fixed package.json incorrectly reporting the license as ISC.
[#217]: https://github.com/clusterio/factorioClusterio/issues/217
+114 -130
View File
@@ -10,21 +10,20 @@ Discord for development/support/play: https://discord.gg/5XuDkje
## Important notice
This is the development branch for factorioClusterio 2.0 which is currently undergoing heavy
restructuring and refactoring. Expect plugins and existing installations to frequently break when
using this branch. If you don't want to be an alpha tester for 2.0 please use the stable
[1.2.x branch][1.2.x] or
[latest stable release](https://github.com/clusterio/factorioClusterio/releases/latest).
This is the development branch for factorioClusterio 2.0 which is currently undergoing heavy restructuring and refactoring.
Expect plugins and existing installations to frequently break when using this branch.
If you don't want to be an alpha tester for 2.0 please use the stable [1.2.x branch][1.2.x] or [latest stable release](https://github.com/clusterio/factorioClusterio/releases/latest).
Installation instructions below are for the unstable master branch. Go to the page for
the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
Installation instructions below are for the unstable master branch.
Go to the page for the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
[1.2.x]: https://github.com/clusterio/factorioClusterio/tree/1.2.x
### Ways to support me/the project:
* Contribute with code/documentation. See [Contributing](docs/contributing.md) for how to make pull
requests. Always nice to move the project forward.
* Contribute with code/documentation.
See [Contributing](docs/contributing.md) for how to make pull requests.
Always nice to move the project forward.
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
@@ -35,6 +34,7 @@ the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
* [Plugins](#plugins)
* [Ubuntu setup](#ubuntu-setup)
* [Windows setup](#windows-setup)
* [MacOS setup](#macos-setup)
* [Installing Plugins](#installing-plugins)
* [Configure Master Server](#configure-master-server)
* [Hosting Behind Proxy](#hosting-behind-proxy)
@@ -48,64 +48,54 @@ the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
## Introduction
Clusterio is a clustered Factorio server manager that provides the
tooling for implementing cross server interactions in Factorio. It
was previously best known for implementing cross server transfer and
cloud storage of items via teleporter chests. But this functionality
has been pulled out of Clusterio into its own plugin for Clusterio named
[Subspace Storage](https://github.com/clusterio/factorioClusterioMod).
Clusterio is a clustered Factorio server manager that provides the tooling for implementing cross server interactions in Factorio.
It was previously best known for implementing cross server transfer and cloud storage of items via teleporter chests.
But this functionality has been pulled out of Clusterio into its own plugin for Clusterio named [Subspace Storage](https://github.com/clusterio/factorioClusterioMod).
By itself Clusterio doesn't change the gameplay in any way, you could
even use Clusterio to manage completely vanilla Factorio servers.
Plugins do the work of modding in the visible changes into the game, see
the [Plugins section](#plugins) for ready made plugins you can install
into a Clusterio cluster.
By itself Clusterio doesn't change the gameplay in any way, you could even use Clusterio to manage completely vanilla Factorio servers.
Plugins do the work of modding in the visible changes into the game, see the [Plugins section](#plugins) for ready made plugins you can install into a Clusterio cluster.
## Features
- Clustered Factorio server management allowing you manage the running
of Factorio servers across a fleet of physical servers from both a web
interface and a command line interface.
- Clustered Factorio server management allowing you manage the running of Factorio servers across a fleet of physical servers from both a web interface and a command line interface.
- User list management for synchronizing in-game admins, whitelisted
users, and bans to all the servers in the cluster.
- User list management for synchronizing in-game admins, whitelisted users, and bans to all the servers in the cluster.
- Integrated support for exporting statistics for the whole cluster to
Prometheus via a single metrics endpoint.
- Integrated support for exporting statistics for the whole cluster to Prometheus via a single metrics endpoint.
- Extensive plugin support for adding your own cross server features to
Factorio using Clusterio's communication backbone.
- Extensive plugin support for adding your own cross server features to Factorio using Clusterio's communication backbone.
## Plugins
The heart of Clusterio is its plugin system. Plugins add functionality
to Factorio servers, Clusterio itself or both. These are the plugins
supported and maintained by the Clusterio developers:
The heart of Clusterio is its plugin system.
Plugins add functionality to Factorio servers, Clusterio itself or both.
These are the plugins supported and maintained by the Clusterio developers:
- [Global Chat](/plugins/global_chat/README.md): share the in-game chat
between servers.
- [Research Sync](/plugins/research_sync/README.md): synchronize
research progress and technologies unlocked between servers.
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect
in-game statistics from all the servers and makes it available to the
Prometheus endpoint on the master server.
- [Subspace Storage](https://github.com/clusterio/factorioClusterioMod):
Provide shared storage that can transport items between servers via
teleport chests.
- [Global Chat](/plugins/global_chat/README.md): share the in-game chat between servers.
- [Research Sync](/plugins/research_sync/README.md): synchronize research progress and technologies unlocked between servers.
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the master server.
- [Subspace Storage](https://github.com/clusterio/factorioClusterioMod): Provide shared storage that can transport items between servers via teleport chests.
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
Want to make your own plugin? Check out the documentation on [Writing
Plugins](/docs/writing-plugins.md) for where to start.
There's also plugins developed and maintained by the community:
- [Discord Bridge](https://github.com/Hornwitser/discord_bridge): Bridges chat between instances and Discord.
By Hornwitser.
- [Server Select](https://github.com/Hornwitser/server_select): In-game GUI for connecting to other server in the cluster.
Originally by Godmave, ported to 2.0 by Hornwitser.
Want to make your own plugin?
Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for where to start.
## Ubuntu setup
**Warning**: These instructions are for the unstable master version and is not
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
**Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
Clusterio runs on Node.js v12 and up, and v10.16.0+. Node.js itself is not
supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu.
Clusterio runs on Node.js v12 and up, and v10.16.0+.
Node.js itself is not supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu.
Master and all slaves:
@@ -118,12 +108,13 @@ Master and all slaves:
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.gz
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
downloads and installs nodejs, git and clusterio.
To specify a version, change "latest" in the link to a version number like 0.14.21.
**Ubuntu with Docker**
The Docker support for Clusterio is curently broken. If you need it
open an issue about it.
The Docker support for Clusterio is curently broken.
If you need it open an issue about it.
<!--
Clusterio has *very* limited support for using docker.
@@ -134,18 +125,18 @@ Clusterio has *very* limited support for using docker.
sudo docker run --name slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there.
The -v flag is used to specify the instance directory.
Your instances (save files etc) will be stored there.
-->
## Windows setup
**Warning**: These instructions are for the unstable master version and is not
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
**Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
**Requirements**
download and install nodeJS 12 from http://nodejs.org. Clusterio runs on Node.js v12 and
up, and v10.16.0+.
download and install nodeJS 12 from http://nodejs.org.
Clusterio runs on Node.js v12 and up, and v10.16.0+.
**Master**
@@ -160,31 +151,48 @@ up, and v10.16.0+.
- Via the stand alone version on from their website
1. Create a new folder named "factorio" in the the factorioClusterio
folder.
1. Create a new folder named "factorio" in the the factorioClusterio folder.
2. Download the MS Windows (64-bit zip package) from
https://www.factorio.com/download .
2. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download .
3. Open the zip file and drag the folder called "Factorio_x.y.z" into
the factorio folder created in step 1.
3. Open the zip file and drag the folder called "Factorio_x.y.z" into the factorio folder created in step 1.
- Via steam installation
1. Create a new folder named "factorio" in the the factorioClusterio
folder.
1. Create a new folder named "factorio" in the the factorioClusterio folder.
2. Locate the game files by right clicking the game in steam,
selecting properties, then Local Files, then Browse local files.
2. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files.
3. Go to the parent folder of the folder that Steam opened and copy
the Factorio folder into the factorio folder created in step 1.
3. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorio folder created in step 1.
## MacOS Setup
**Requirements**
download and install nodeJS 12 from http://nodejs.org or brew (`brew install node`). Clusterio runs on Node.js v12 and
up, and v10.16.0+.
1. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
```bash
mkdir clusterio
cd clusterio
npm init -y
npm install @clusterio/master @clusterio/slave @clusterio/ctl
```
2. Obtain Factorio:
Install Factorio and copy the factorio.app/Contents in your clusterio install folder as factorio.
If you installed factorio on your mac you can run
```bash
cp -r /Applications/factorio.app/Contents/ factorio
```
## Installing Plugins
Installing plugins to make them work with Clusterio consists of two
steps. First install the package via npm, for example
Installing plugins to make them work with Clusterio consists of two steps.
First install the package via npm, for example
npm install @clusterio/plugin-subspace_storage
@@ -192,22 +200,17 @@ Then tell clusterio that this plugin exists by adding it as a plugin.
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
This adds it to the default `plugin-list.json` file which in the shared
folder setup is loaded by master, slave and ctl. If you have slaves or
ctl installed on separate computers (or directories) then you need to
repeat the plugin install process for all of them. The clusteriomaster,
clusterioslave and clusterioctl commands has the plugin sub-command
so you do not need to install clusteriomaster to add plugins.
This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by master, slave and ctl.
If you have slaves or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them.
The clusteriomaster, clusterioslave and clusterioctl commands has the plugin sub-command so you do not need to install clusteriomaster to add plugins.
For development purposes the `plugin add` command supports adding
plugins by the absolute path to them, or a relative path that must start
with either . or .. (which will then be resolved to an absolute path).
For development purposes the `plugin add` command supports adding plugins by the absolute path to them, or a relative path that must start with either . or .. (which will then be resolved to an absolute path).
## Configure Master Server
By default the master server will listen for HTTP on port 8080. You can
change the port used with the command
By default the master server will listen for HTTP on port 8080.
You can change the port used with the command
npx clusteriomaster config set master.http_port 1234
@@ -216,67 +219,50 @@ that it will be accessible under with, for example
npx clusteriomaster config set master.external_address http://203.0.113.4:1234/
Change the url to reflect the IP, protocol, and port the master server
is accessible under, dns names are also supported. If you're planning
on making the master server accessible on the internet it's recommended
to set up TLS, see the [Setting Up TLS](/docs/setting-up-tls.md)
document for more details.
Change the url to reflect the IP, protocol, and port the master server is accessible under, dns names are also supported.
If you're planning on making the master server accessible on the internet it's recommended to set up TLS, see the [Setting Up TLS](/docs/setting-up-tls.md) document for more details.
You can list the config of the master server with the `npx
clusteriomaster config list` command. See the [readme for
@clusterio/master](/packages/master/README.md) for more
information.
You can list the config of the master server with the `npx clusteriomaster config list` command.
See the [readme for @clusterio/master](/packages/master/README.md) for more information.
### Setting up an admin account
Before you can manage the cluster you need to bootstrap an admin account
for it. Replace `<username>` with your Factorio username (do not make
up a new username here).
Before you can manage the cluster you need to bootstrap an admin account for it.
Replace `<username>` with your Factorio username (do not make up a new username here).
npx clusteriomaster bootstrap create-admin <username>
npx clusteriomaster bootstrap create-ctl-config <username>
The first command creates a user account with the given name and
promotes it to a cluster admin. The second one sets up a
`config-control.json` config for clusterioctl to connect to the master
server under the given user account.
The first command creates a user account with the given name and promotes it to a cluster admin.
The second one sets up a `config-control.json` config for clusterioctl to connect to the master server under the given user account.
## Running Clusterio
After following the installation and master configuration instructions
you can use the following commands to run Clusterio.
After following the installation and master configuration instructions you can use the following commands to run Clusterio.
### Master Server
It's necessary to run the master server in order for anything to work.
Once you've completed the setup run the following command to start it
up:
Once you've completed the setup run the following command to start it up:
npx clusteriomaster run
### Slaves
Slaves connect to the master server and are managed remotely from the
master server. In order for slaves to connect to the master server they
need a valid authentication token, you can create a slave config with
a valid token with the following command.
Slaves connect to the master server and are managed remotely from the master server.
In order for slaves to connect to the master server they need a valid authentication token, you can create a slave config with a valid token with the following command.
npx clusterioctl slave create-config --name Local --generate-token
This will write a new `config-slave.json` file in the current directory
(you can change the location with the `--output` option) with the name,
token and url to connect to the master server with. If you are making
the config for a remote slave you will need to have set the
`master.external_address` option to the URL the master server can be
reached on.
This will write a new `config-slave.json` file in the current directory (you can change the location with the `--output` option) with the name, token and url to connect to the master server with.
If you are making the config for a remote slave you will need to have set the `master.external_address` option to the URL the master server can be reached on.
You can list the config of a slave on the slave itself with the `npx
clusterioslave config list` command. See the [readme for
@clusterio/slave](/packages/slave/README.md) for more information.
You can list the config of a slave on the slave itself with the `npx clusterioslave config list` command.
See the [readme for @clusterio/slave](/packages/slave/README.md) for more information.
Once the config is set up run the slave with
@@ -285,13 +271,9 @@ Once the config is set up run the slave with
### Instances
Instances are created, managed and started from the master server. For
now the only interface available is the `clusterioctl` command line tool
included in Clusterio. You can run this tool from any slave, or the
master server without having to set up a config, if you want to manage
the cluster from somewhere else you will need to set the
`control.master_url` and `control.master_token` options with the `node
clusterioctl control-config set` command:
Instances are created, managed and started from the master server.
For now the only interface available is the `clusterioctl` command line tool included in Clusterio.
You can run this tool from any slave, or the master server without having to set up a config, if you want to manage the cluster from somewhere else you will need to set the `control.master_url` and `control.master_token` options with the `node clusterioctl control-config set` command:
The basic operations to start a new instance is the following
@@ -300,21 +282,19 @@ The basic operations to start a new instance is the following
npx clusterioctl instance start "My instance"
The first line creates the instance configuration on the master server.
The second assigns the instance to a slave which creates the instance
directory and files needed to run the instance on the given slave. The
third line starts the instance, which creates a new save if there are no
save games present.
The second assigns the instance to a slave which creates the instance directory and files needed to run the instance on the given slave.
The third line starts the instance, which creates a new save if there are no save games present.
There are many more commands available with clusterioctl. See the
[Managing a Cluster](/docs/managing-a-cluster.md) document or
`npx clusterioctl --help` for a full list of them.
There are many more commands available with clusterioctl.
See the [Managing a Cluster](/docs/managing-a-cluster.md) document or `npx clusterioctl --help` for a full list of them.
## Common problems
### EACCESS [...] LISTEN 443
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
Some systems don't let non root processes listen to ports below 1000.
Either run with `sudo` or change config.json to use higher port numbers.
According to [this link](https://askubuntu.com/questions/839520/open-port-443-for-a-node-web-app) if you manually installed node.js following the above instructions, you may need to run the following command to fix this issue:
@@ -322,7 +302,11 @@ According to [this link](https://askubuntu.com/questions/839520/open-port-443-fo
### Portforwarding doesn't work on the master server when running under WSL
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip. This is also true when you correctly port-forwarded the correct ports. Even when routing this server through nginx in WSL, the issue persists. Then, on a hunch, I tried to run nginx from windows itself and found that this DID work. It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip.
This is also true when you correctly port-forwarded the correct ports.
Even when routing this server through nginx in WSL, the issue persists.
Then, on a hunch, I tried to run nginx from windows itself and found that this DID work.
It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
TLDR: the tested fix is:
+21 -44
View File
@@ -1,59 +1,36 @@
Communication Layer
===================
# Communication Layer
<sub>This document describes the communication layer in Clusterio and
unless you're developing Clusterio it's probably not going to be very
useful for you.</sub>
<sub>This document describes the communication layer in Clusterio and unless you're developing Clusterio it's probably not going to be very useful for you.</sub>
Communication is facilitated by the lib/link library and is based on
three core abstractions: links, connectors and messages. Links
represents one side of a two way communication pipe, connectors deal
with connecting and establishing communication, and messages are the
content sent over the link.
Communication is facilitated by the lib/link library and is based on three core abstractions: links, connectors and messages.
Links represents one side of a two way communication pipe, connectors deal with connecting and establishing communication, and messages are the content sent over the link.
Link
----
## Link
The Link class represents one side of a two way communication channel
where the actual communication is done by a connector associated with
the link. The link also knows what the endpoint types it's connected to
are via the source and target attributes.
The Link class represents one side of a two way communication channel where the actual communication is done by a connector associated with the link.
The link also knows what the endpoint types it's connected to are via the source and target attributes.
The endpoint types currently implemented are master, slave, instance,
and control. Although slave and instances run in the same Node.js
program they use virtual links to communicate between them in order to
simplify the communication architecture.
The endpoint types currently implemented are master, slave, instance, and control.
Although slave and instances run in the same Node.js program they use virtual links to communicate between them in order to simplify the communication architecture.
Upon creation the link will register with the connector in order to
receive and process messages from it. The messages received are
validated using the validator registered for the type of the message. A
message for which there is no validator for is considered an error.
Upon creation the link will register with the connector in order to receive and process messages from it.
The messages received are validated using the validator registered for the type of the message.
A message for which there is no validator for is considered an error.
Connector
---------
## Connector
The connector used with a link is responsible for establishing and/or
maintain the connection that messages are sent over. Reconnect logic is
expected to be implemented in the connector, though this has not yet
been done.
The connector used with a link is responsible for establishing and/or maintain the connection that messages are sent over.
Reconnect logic is expected to be implemented in the connector, though this has not yet been done.
There's also the virtual connector which lets two links in the same
program be connected to each other without having to establish a
network connection.
There's also the virtual connector which lets two links in the same program be connected to each other without having to establish a network connection.
Message
-------
## Message
There's two types of messages currently implemented apart from the
Link builtin close message and that's Request and Event. The builtin
messages are defined in
[packages/lib/link/messages.js](/packages/lib/link/messages.js) and are
attached to links via the attachAllMessages function, which is usally
called in the Link subclass constructor.
There's two types of messages currently implemented apart from the Link builtin close message and that's Request and Event.
The builtin messages are defined in [packages/lib/link/messages.js](/packages/lib/link/messages.js) and are attached to links via the attachAllMessages function, which is usally called in the Link subclass constructor.
The messages define which links they are valid on and only attaches
handlers for those links. There's also a forwarding mechanism for when
a request or event is to be forwarded another hop.
The messages define which links they are valid on and only attaches handlers for those links.
There's also a forwarding mechanism for when a request or event is to be forwarded another hop.
+36 -70
View File
@@ -1,55 +1,34 @@
Configuration System
====================
# Configuration System
<sub>This document describes the implementation and inner working of the
configuration system in Clusterio, and unless you're developing
Clusterio it's probably not going to be very useful for you.</sub>
<sub>This document describes the implementation and inner working of the configuration system in Clusterio, and unless you're developing Clusterio it's probably not going to be very useful for you.</sub>
Clusterio uses a group based configuration system to manage settings for
the master server, instances, slaves, and the clusterioctl utility.
Plugins get their own config group for the master server and instances
which they can add their own fields to. The fields are defined early in
the startup, and the groups and their fields for disabled plugins will
still be created.
Clusterio uses a group based configuration system to manage settings for the master server, instances, slaves, and the clusterioctl utility.
Plugins get their own config group for the master server and instances which they can add their own fields to.
The fields are defined early in the startup, and the groups and their fields for disabled plugins will still be created.
The built-in config groups and their fields are defined in
[packages/lib/config/definitions.js](/packages/lib/config/definitions.js)
while plugins can add their own config groups to the `MasterConfigGroup`
and `InstanceConfigGroup` properties of the `info.js` export using
subclasses of `PluginConfigGroup`. The plugin group has the enabled
field pre-defined on it, and it's used to enable/disable plugins.
The built-in config groups and their fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config groups to the `MasterConfigGroup` and `InstanceConfigGroup` properties of the `info.js` export using subclasses of `PluginConfigGroup`.
The plugin group has the enabled field pre-defined on it, and it's used to enable/disable plugins.
Defing Fields
-------------
## Defing Fields
Fields are defined using the `define` class method on group classes
after the groupName have been set and before the class has been
finalized. The `define` method takes an object as argument with the
following properties:
Fields are defined using the `define` class method on group classes after the groupName have been set and before the class has been finalized.
The `define` method takes an object as argument with the following properties:
**access**:
The locations this config field can be read and modified from. This
is an array of strings which can contain the values `"master"`,
`"slave"` and `"control"`. If a party is missing in this array then
that party will not receive the value of this field when the config
is shared, and will be unable to modify the field. Takes the value
of `defaultAccess` set on the config group class if not passed.
Note that instance config groups should have both `"master"` and
`"slave"` for all of its fields or unexpected behaviour may occur.
The locations this config field can be read and modified from.
This is an array of strings which can contain the values `"master"`, `"slave"` and `"control"`.
If a party is missing in this array then that party will not receive the value of this field when the config is shared, and will be unable to modify the field.
Takes the value of `defaultAccess` set on the config group class if not passed.
Note that instance config groups should have both `"master"` and `"slave"` for all of its fields or unexpected behaviour may occur.
**type**:
The type of config value this field will support. The supported
values are boolean, string, number, and object, and if an attept is
made to set the field to a value that is not of the right type it
will be reject unless it's a string that can be converted to the
right type.
The type of config value this field will support.
The supported values are boolean, string, number, and object, and if an attept is made to set the field to a value that is not of the right type it will be reject unless it's a string that can be converted to the right type.
**name**:
Name of the configuration field, this is the primary way the field
is accessed, both in code and on the command line. As such it
should follow the lower\_case\_underscore style and not contain any
spaces or dots.
Name of the configuration field, this is the primary way the field is accessed, both in code and on the command line.
As such it should follow the lower\_case\_underscore style and not contain any spaces or dots.
**title** (optional):
Text used to identify the config field in user interfaces.
@@ -58,27 +37,24 @@ following properties:
Text used to describe the config field in user interfaces.
**enum** (optional):
Array of acceptable values this field can have. Attempts to set a
value not in this array will be rejected, which makes it useful for
making enumareted list of choices.
Array of acceptable values this field can have.
Attempts to set a value not in this array will be rejected, which makes it useful for making enumareted list of choices.
**optional** (optional):
True if this field can be set to null. Defaults to false.
True if this field can be set to null.
Defaults to false.
**initial_value** (optional if optional is true):
Value this config field should take in a newly initialized config.
This can also be an async function returning the value to use.
If the value for a field stored in on disk somehow ends up being invalid
the field will take on the initial\_value instead when the config is
loaded.
If the value for a field stored in on disk somehow ends up being invalid the field will take on the initial\_value instead when the config is loaded.
Accessing Fields
----------------
## Accessing Fields
On the Config instance there's two ways to access fields: through the
set and get methods using dot notation. E.g.:
On the Config instance there's two ways to access fields: through the set and get methods using dot notation.
E.g.:
let value = configInstance.get("group.field");
@@ -87,26 +63,16 @@ E.g.:
let value = configInstance.group("group").get("field")
The set method takes an extra argument, which is the value to set the
field to. If the value is not valid this will throw an InvalidValue
exception, if the group or field doesn't exist it will throw an
InvalidField exception.
The set method takes an extra argument, which is the value to set the field to.
If the value is not valid this will throw an InvalidValue exception, if the group or field doesn't exist it will throw an InvalidField exception.
Config Lifecycle
----------------
## Config Lifecycle
Values for all config fields are created when the config is initialized,
as well as for any fields with missing values when a config is loaded.
Once a field has a value it is never automatically removed, even if the
field or group it was part of no longer exists. This ensures that
plugin configuration is not lost should the server be started up without
the plugins that defined the configuration installed.
Values for all config fields are created when the config is initialized, as well as for any fields with missing values when a config is loaded.
Once a field has a value it is never automatically removed, even if the field or group it was part of no longer exists.
This ensures that plugin configuration is not lost should the server be started up without the plugins that defined the configuration installed.
For instance configs, they are kept on the master server and serialized
and sent over to the relevant slaves on startup. This makes it possible
to edit instance configs when the slave the instance is on is offline.
A copy is also stored in the instance directory on the slave, and this
config will be deliverd to the master server on slave startup, which
enables instances to be copied between clusters as long as their IDs are
unique.
For instance configs, they are kept on the master server and serialized and sent over to the relevant slaves on startup.
This makes it possible to edit instance configs when the slave the instance is on is offline.
A copy is also stored in the instance directory on the slave, and this config will be deliverd to the master server on slave startup, which enables instances to be copied between clusters as long as their IDs are unique.
+123 -200
View File
@@ -1,54 +1,51 @@
Configuration
=============
# Configuration
Master Configuration
--------------------
## Master Configuration
### master.name
Name of the cluster. Used to distinguish it from other clusters and is
prepended to the name of servers in the server list.
Name of the cluster.
Used to distinguish it from other clusters and is prepended to the name of servers in the server list.
Defaluts to "Your Cluster".
### master.database_directory
Directory used to store cluster data to. Needs to be writeable by the
master server. This is also used by plugins to store its data.
Directory used to store cluster data to.
Needs to be writeable by the master server.
This is also used by plugins to store its data.
Defaults to "database".
### master.http_port
Port to host HTTP server on. If set to null no HTTP server will be
exposed. At least one of this and master.https_port needs to be set to
a port.
Port to host HTTP server on.
If set to null no HTTP server will be exposed.
At least one of this and master.https_port needs to be set to a port.
Defaults to 8080.
### master.https_port
Port to host HTTPS server on. Uses master.tls_certificate and
master.tls_private_key as the certificate and private key for the HTTPS
server. If set to null no HTTPS server will be exposed. At least one
of this and master.http_port needs to be set to a port.
Port to host HTTPS server on.
Uses master.tls_certificate and master.tls_private_key as the certificate and private key for the HTTPS server.
If set to null no HTTPS server will be exposed.
At least one of this and master.http_port needs to be set to a port.
See the [Setting up TLS](/docs/setting-up-tls.md) document for a guide
to setting up HTTPS with Clusterio.
See the [Setting up TLS](/docs/setting-up-tls.md) document for a guide to setting up HTTPS with Clusterio.
Defaults to null.
### master.bind_address
IP address to bind the HTTP and HTTPS ports on. Useful to limit which
interface to accept connections from. If set to null the unspecified
IPv6 address will be used, which on most systems means it will also
listen on the unspecified IPv4 address and accept connections from all
IP address to bind the HTTP and HTTPS ports on.
Useful to limit which interface to accept connections from.
If set to null the unspecified IPv6 address will be used, which on most systems means it will also listen on the unspecified IPv4 address and accept connections from all
interfaces.
Defaults to null.
@@ -56,122 +53,107 @@ Defaults to null.
### master.external_address
External address the master server is accessible on. Currently only
used for `clusteriomaster bootstrap create-ctl-config` in order to give
the right url to connect to. This should be a full URL ending with a /.
External address the master server is accessible on.
Currently only used for `clusteriomaster bootstrap create-ctl-config` in order to give the right url to connect to.
This should be a full URL ending with a /.
Defaults to null meaning assume localhost.
### master.tls_certificate
Path to TLS certificate to use for the HTTPS server when
master.https_port is configured.
Path to TLS certificate to use for the HTTPS server when master.https_port is configured.
Defaults to "database/certificates/cert.crt".
### master.tls_private_key
Path to TLS private key to use for the HTTPS server when
master.https_port is configured.
Path to TLS private key to use for the HTTPS server when master.https_port is configured.
Defaults to "database/certificates/cert.key".
### master.auth_secret
Base64 encoded authentication secret used to verify tokens issued to
slaves, users, and sessions. This should contain 256 bytes of random
data, and if changed will cause all tokens become invalid. Keep this
secret if leaked an attacker could easily compromise the cluster.
Base64 encoded authentication secret used to verify tokens issued to slaves, users, and sessions.
This should contain 256 bytes of random data, and if changed will cause all tokens become invalid.
Keep this secret if leaked an attacker could easily compromise the cluster.
Defaults to null which means generate a secure secret on startup.
### master.heartbeat_interval
Interval in seconds heartbeats are sent out at for WebSocket
connections. If a connection hasn't received a heartbeat in 2 times the
heartbeat interval it will be considered stale and closed. A lower
value means less time between connections going stale and them being
closed.
Interval in seconds heartbeats are sent out at for WebSocket connections.
If a connection hasn't received a heartbeat in 2 times the heartbeat interval it will be considered stale and closed.
A lower value means less time between connections going stale and them being closed.
Defaults to 45
Defaults to 45.
### master.connector_shutdown_timeout
Timeout in seconds to use while shutting down the master server for link
sessions that currently don't have an active connection and haven't been
closed down properly. This happens when the network between a slave and
the master server goes down unexpectedly, or the slave goes down without
closing the connection properly either do to crashing or the machine
it's hosted on going down. If the timeout expires the session is
terminated and data loss may occur, if the slave went down this is
unavoidable.
Timeout in seconds to use while shutting down the master server for link sessions that currently don't have an active connection and haven't been closed down properly.
This happens when the network between a slave and the master server goes down unexpectedly, or the slave goes down without closing the connection properly either do to crashing or the machine it's hosted on going down.
If the timeout expires the session is terminated and data loss may occur, if the slave went down this is unavoidable.
Defaults to 30.
### master.metrics_timeout
Timeout in seconds before a call to gather metrics from a slave times
out. This should be less than both the timeout and collection interval
configured for the collection job in Prometheus.
Timeout in seconds before a call to gather metrics from a slave times out.
This should be less than both the timeout and collection interval configured for the collection job in Prometheus.
Defaults to 8.
### master.default_role_id
Role to automatically grant to new users. If null no role is granted.
Role to automatically grant to new users.
If null no role is granted.
Defaults to 1 which correspond to the default Player role.
### <plugin_name>.enabled
### <plugin_name>.load_plugin
Whether to enable the given plugin on the master server. Plugins
disabled on the master server will not be loaded on instances.
Whether to load the plugin on the master server.
Plugins not loaded on the master server will not be loaded on instances.
Defaults to true.
Slave Configuration
-------------------
## Slave Configuration
### slave.name
Name of the slave. Shows up in slave lists and is used to reference
this slave in the clusterioctl command line interface.
Name of the slave.
Shows up in slave lists and is used to reference this slave in the clusterioctl command line interface.
Defaults to "New Slave".
### slave.id
Immutable numeric id of the slave which uniquely identifies this slave in
the cluster.
Immutable numeric id of the slave which uniquely identifies this slave in the cluster.
Defaults to a random 31 bit number.
### slave.factorio_directory
Directory to look for the Factorio server(s) in. This can either point
to a Factorio server directory, or to a directory containing multiple
versions of the Factorio server.
Directory to look for the Factorio server(s) in.
This can either point to a Factorio server directory, or to a directory containing multiple versions of the Factorio server.
Defaults to "factorio".
### slave.instances_directory
Directory to store instances in. One sub-directory will be created in
it for each instance assigned to this slave and these sub-directories
will contain logs, saves, and various auto generated configuration files
for the instances.
Directory to store instances in.
One sub-directory will be created in it for each instance assigned to this slave and these sub-directories will contain logs, saves, and various auto generated configuration files for the instances.
Defaults to "instances".
@@ -185,164 +167,140 @@ Defaults to "http://localhost:8080/".
### slave.master_token
Access token used for authenticating with the master server. You can
generate an access token with `clusterioctl slave generate-token
--id <slave-id>`, or use the `clusterioctl slave create-config` to
create a new slave config with the correct url and token in it.
Access token used for authenticating with the master server.
You can generate an access token with `clusterioctl slave generate-token --id <slave-id>`, or use the `clusterioctl slave create-config` to create a new slave config with the correct url and token in it.
Defaults to "enter token here".
### slave.tls_ca
Path to certificate in PEM format to use for validating a TLS connection
to the master server. If you have a self signed certificate on the
master server you will need to copy the certificate to your slaves and
set it as the certificate authority with this option.
Path to certificate in PEM format to use for validating a TLS connection to the master server.
If you have a self signed certificate on the master server you will need to copy the certificate to your slaves and set it as the certificate authority with this option.
Defaults to null meaning use Node.js's default set of trusted
certificate authorities.
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
### slave.public_address
External address instances hosted on this slave can be accessed on.
This is used by plugins like Server Select to give the correct address
to switch between instances in-game.
This is used by plugins like Server Select to give the correct address to switch between instances in-game.
Defaults to "localhost".
### slave.reconnect_delay
Delay in seconds to wait after connection to the master server is
dropped before attempting to reconnect to it. The actual delay on each
reconnect will be a random number between 0 and this configured value to
avoid all slaves trying to reconnect at the same time.
Delay in seconds to wait after connection to the master server is dropped before attempting to reconnect to it.
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all slaves trying to reconnect at the same time.
Defaults to 5.
Instance Configuration
----------------------
## Instance Configuration
### instance.name
Name of the instance, shown in instance lists and used to reference to this
instance in the clusterioctl command line interface.
Name of the instance, shown in instance lists and used to reference to this instance in the clusterioctl command line interface.
Defaults to "New Instance"
Defaults to "New Instance".
### instance.id
Imutable numeric id of the instance which uniquely identifies this
instance in the cluster.
Imutable numeric id of the instance which uniquely identifies this instance in the cluster.
Defaluts to a random 31 bit integer.
### instance.assigned_slave
Slave this instance is assigned to. To change this you need to use the
separate assign instance interface. After being assigned to a slave the
directory for the instance is created on the slave.
Slave this instance is assigned to.
To change this you need to use the separate assign instance interface.
After being assigned to a slave the directory for the instance is created on the slave.
Defaults to null meaning not assigned.
### instance.auto_start
If enabled start this instance when the slave it is assigned to is
started up. Does not affect start or stop of the instance while the
slave is running.
If enabled start this instance when the slave it is assigned to is started up.
Does not affect start or stop of the instance while the slave is running.
Defaults to false
Defaults to false.
### factorio.version
Version of Factorio to use for this instance. Can be a version like
"1.0.0" or special string "latest" meaning the latest version of
Factorio found on the slave.
Version of Factorio to use for this instance.
Can be a version like "1.0.0" or special string "latest" meaning the latest version of Factorio found on the slave.
Defaults to "latest"
Defaults to "latest".
### factorio.game_port
UDP port to run the Factorio server on. If null a random port in the
dynamyc range assigned each time the instance starts.
UDP port to run the Factorio server on.
If null a random port in the dynamyc range assigned each time the instance starts.
Defaults to null
Defaults to null.
### factorio.rcon_port
TCP port to start the RCON interface on the Factorio server on. If null
a random port in the dynamic range is assigned each time the instance
starts.
TCP port to start the RCON interface on the Factorio server on.
If null a random port in the dynamic range is assigned each time the instance starts.
Defaults to null
Defaults to null.
### factorio.rcon_password
Password to use for the RCON interafec. If null a random secure
password is generated and used each time the instance starts.
Password to use for the RCON interafec.
If null a random secure password is generated and used each time the instance starts.
Defaults to null
Defaults to null.
### factorio.enable_save_patching
Whether to use save patching or not. When enabled lua code is patched
into the game save before starting it. Most plugins require the use of
save patching to function. Turning it off makes it possible to use
Clusterio to run and manage regular vanilla games and scenarios not
compatible with Clusterio.
Whether to use save patching or not.
When enabled lua code is patched into the game save before starting it.
Most plugins require the use of save patching to function.
Turning it off makes it possible to use Clusterio to run and manage regular vanilla games and scenarios not compatible with Clusterio.
Defaults to true
Defaults to true.
### factorio.enable_whitelist
Turn on the whitelist on the server.
Defaults to false
Defaults to false.
### factorio.settings
Object with the settings to put into `server-settings.json` for the
Factorio server. The settings in `data/server-settings.example.json` of
the Factorio installation is used as the base and properties specified
here overrides the properties there.
Object with the settings to put into `server-settings.json` for the Factorio server.
The settings in `data/server-settings.example.json` of the Factorio installation is used as the base and properties specified here overrides the properties there.
Changes to the following properties will be applied live if the instance
is running while it is changed: `afk_autokick_interval`,
`allow_commands`, `autosave_interval`, `autosave_only_on_server`,
`description`, `ignore_player_limit_for_returning_players`,
`max_players`, `max_upload_slots`, `max_upload_in_kilobytes_per_second`,
`name`, `only_admins_can_pause_the_game`, `game_password`,
`require_user_verification`, `tags`, `visibility`.
Changes to the following properties will be applied live if the instance is running while it is changed: `afk_autokick_interval`, `allow_commands`, `autosave_interval`, `autosave_only_on_server`, `description`, `ignore_player_limit_for_returning_players`, `max_players`, `max_upload_slots`, `max_upload_in_kilobytes_per_second`, `name`, `only_admins_can_pause_the_game`, `game_password`, `require_user_verification`, `tags`, `visibility`.
Defaults to {"tags":["clusterio"],"auto_pause":false}.
### factorio.verbose_logging
Pass the `--verbose` flag to Factorio when starting the server. This
prints more messages to the log.
Pass the `--verbose` flag to Factorio when starting the server.
This prints more messages to the log.
Defaults to false
Defaults to false.
### factorio.strip_paths
Strip down absolute paths in the server log going to files inside the
instance directory such that they are relative to the instance
directory. This improves the signal to noise ratio of the log.
Strip down absolute paths in the server log going to files inside the instance directory such that they are relative to the instance directory.
This improves the signal to noise ratio of the log.
Sample output when disabled:
@@ -356,86 +314,59 @@ Sample output when enabled:
0.614 Checksum for script temp/currently-playing/control.lua: 2390553941
1.277 Script @modules/example/test.lua:7: Example log line.
Defaults to true
Defaults to true.
### factorio.sync_adminlist
Synchronize in-game admin list with admin status of the users in
cluster. If enabled Clusterio will generate a new
`server-adminlist.json` on instance startup based on the users in the
cluster with admin status set to true, and keep the admins in sync with
the users that have admin status while the instance is running.
Synchronize in-game admin list with admin status of the users in cluster.
If enabled Clusterio will generate a new `server-adminlist.json` on instance startup based on the users in the cluster with admin status set to true, and keep the admins in sync with the users that have admin status while the instance is running.
Defaults to true
Defaults to true.
### factorio.sync_whitelist
Synchronize in-game whitelist with the whitelisted status of the users
in the cluster. If enabled Clusterio will generate a new
`server-whitelist.json` on instance startup based on the users in the
cluster with whitelisted status set to true, and keep the whitelisted
users in sync with the users that have whitelisted status while the
instance in running.
Synchronize in-game whitelist with the whitelisted status of the users in the cluster.
If enabled Clusterio will generate a new `server-whitelist.json` on instance startup based on the users in the cluster with whitelisted status set to true, and keep the whitelisted users in sync with the users that have whitelisted status while the instance in running.
Defaults to true
Defaults to true.
### factorio.sync_banlist
Synchronize in-game banlist with the banned status of users in the
cluster. If enabled Clusterio will generate a new `server-banlist.json`
on instance startup based on users in the cluster with banned status set
to true, and keep the banned users in sync with the users that have the
banned status while the instance is running.
Synchronize in-game banlist with the banned status of users in the cluster.
If enabled Clusterio will generate a new `server-banlist.json` on instance startup based on users in the cluster with banned status set to true, and keep the banned users in sync with the users that have the banned status while the instance is running.
Defaults to true
Defaults to true.
### factorio.max_concurrent_commands
Maximum number of commands being transmitted into the game in parallel.
Since the rate at which long commands are streamed into the game is by
default limeted to 100 bytes/tick and scales down to 25 bytes/tick when
there's 20 players connected, long commands can hold up the command
interface and cause large latencies in getting data pushed into the
game. (The rates are controlled by the `segment_size` options in the
server settings.) To counteract this Clusterio sends up to this
configured value number of commands in parallel over the RCON interface.
This causes the game updates sent to clients to have this many command
streams in parallel contained in them, drastically increasing the
maxiumum size they can become. Since commands are only processed so
ofter the maximum rate of commands that can be sustained is roughly 30
times this configured value per second, though note that large commands
will lower this value.
Since the rate at which long commands are streamed into the game is by default limeted to 100 bytes/tick and scales down to 25 bytes/tick when there's 20 players connected, long commands can hold up the command interface and cause large latencies in getting data pushed into the game.
(The rates are controlled by the `segment_size` options in the server settings.)
To counteract this Clusterio sends up to this configured value number of commands in parallel over the RCON interface.
This causes the game updates sent to clients to have this many command streams in parallel contained in them, drastically increasing the maxiumum size they can become.
Since commands are only processed so ofter the maximum rate of commands that can be sustained is roughly 30 times this configured value per second, though note that large commands will lower this value.
The game updates sent by the server is split into roughly 500 byte
packets, and if any one of those parts are lost the entire update is
resent. This means a high number of concurrent commands can amplify
resends due to packet loss and degrade the player experience up to the
point of becomming completely unplayable.
The game updates sent by the server is split into roughly 500 byte packets, and if any one of those parts are lost the entire update is resent.
This means a high number of concurrent commands can amplify resends due to packet loss and degrade the player experience up to the point of becomming completely unplayable.
Prior to 0.18.7 the maximum practical size of game update sent where
arround 8 kB due to the UDP receive buffer on windows defaulting to this
value, larger updates would not get through the receive buffer and cause
the connection to drop. To stay below the 8 kB limit the maximum safe
value for this option would be around 7000 / (3 * game_speed *
maximum_segment_size), which is around 20 in normal circumstances.
Prior to 0.18.7 the maximum practical size of game update sent where arround 8 kB due to the UDP receive buffer on windows defaulting to this value, larger updates would not get through the receive buffer and cause the connection to drop.
To stay below the 8 kB limit the maximum safe value for this option would be around 7000 / (3 * game_speed * maximum_segment_size), which is around 20 in normal circumstances.
Defaults to 5
Defaults to 5.
### <plugin_name>.enabled
### <plugin_name>.load_plugin
Whether to enable the given plugin on the instance. Note that plugins
need to be enabled on the master server in order for them to be loaded
on instances.
Whether to load the given plugin on the instance.
Note that plugins need to be loaded on the master server in order for them to be loaded on instances.
Defaults to true
Defaults to true.
Control Configuration
---------------------
## Control Configuration
### control.master_url
@@ -446,30 +377,22 @@ Defaults to null meaning complain about it not being set and exit.
### control.master_token
Access token used for authenticating with the master server. You can
generate an access token with `clusteriomaster bootstrap
generate-user-token <username>`, or use the `clusteriomaster bootstrap
create-ctl-config <username>` to create a new ctl config with the
correct url and token in it.
Access token used for authenticating with the master server.
You can generate an access token with `clusteriomaster bootstrap generate-user-token <username>`, or use the `clusteriomaster bootstrap create-ctl-config <username>` to create a new ctl config with the correct url and token in it.
Defaults to null meaning complain about it not being set and exit.
### control.tls_ca
Path to certificate in PEM format to use for validating a TLS connection
to the master server. If you have a self signed certificate on the
master server you will need to copy the certificate to the computer you
run clusterctl from and set it as the certificate authority with this
option.
Path to certificate in PEM format to use for validating a TLS connection to the master server.
If you have a self signed certificate on the master server you will need to copy the certificate to the computer you run clusterctl from and set it as the certificate authority with this option.
Defaults to null meaning use Node.js's default set of trusted
certificate authorities.
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
### control.reconnect_delay
Duration in seconds to wait before attempting to reconnect with the
master server after the connection is dropped.
Duration in seconds to wait before attempting to reconnect with the master server after the connection is dropped.
Defaults to 2.
+133 -125
View File
@@ -1,13 +1,11 @@
Contributing to Clusterio
=========================
# Contributing to Clusterio
You are probably reading this because you want to contribute code to
Clusterio. Great! We need people like you. Though we have some
conventions and workflows that we kindly ask you to follow.
You are probably reading this because you want to contribute code to Clusterio.
Great! We need people like you.
Though we have some conventions and workflows that we kindly ask you to follow.
Contents
--------
## Contents
- [Workflow](#workflow)
- [Starting a Feature Branch](#starting-a-feature-branch)
@@ -23,205 +21,161 @@ Contents
- [Naming Style](#naming-style)
- [Indenting](#indenting)
- [Line Length](#line-length)
- [Markdown](#markdown)
- [Pushing releases to npm](#pushing-releases-to-npm)
Workflow
--------
## Workflow
While we don't require you to follow this workflow, it makes things much
simpler for both you and us if you choose to do so. Especially when it
comes to making the commit history reflect what happened and not be a
giant ball of mess. GitHub's git workflow is less than ideal for
dealing with small feature-branches so you might see things being done
here a little diffrent than the usual.
While we don't require you to follow this workflow, it makes things much simpler for both you and us if you choose to do so.
Especially when it comes to making the commit history reflect what happened and not be a giant ball of mess.
GitHub's git workflow is less than ideal for dealing with small feature-branches so you might see things being done here a little diffrent than the usual.
It is recommended that you use the command line git version for doing
the operations described here. GUI wrappers tend to have different
ideas of what is what and you might find it difficult to make it do the
things described here. For doing the actual commits though, your
faviorit git GUI should work just fine.
It is recommended that you use the command line git version for doing the operations described here.
GUI wrappers tend to have different ideas of what is what and you might find it difficult to make it do the things described here.
For doing the actual commits though, your faviorit git GUI should work just fine.
If you pressed the fork button on GitHub and used `git clone` _on your
own fork_ you can skip to the next paragraph. Otherwise you will have
to click the fork button and clone your fork. If you already cloned the
main GitHub repository you can change your clone to point at your own
fork using the `git remote` command, but it's probably easier to delete
your local clone and clone it again.
If you pressed the fork button on GitHub and used `git clone` _on your own fork_ you can skip to the next paragraph.
Otherwise you will have to click the fork button and clone your fork.
If you already cloned the main GitHub repository you can change your clone to point at your own fork using the `git remote` command, but it's probably easier to delete your local clone and clone it again.
To keep your local repository up to to date with the main repository add
the main repository as a remote.
To keep your local repository up to to date with the main repository add the main repository as a remote.
git remote add upstream https://github.com/clusterio/factorioClusterio
After this running `git fetch upstream` will get the updated branches
from the main repository. Do not run `git pull upstream` it will mess
thing up!
After this running `git fetch upstream` will get the updated branches from the main repository.
Do not run `git pull upstream` it will mess thing up!
### Setting up the Development Environment
The project is organized into several packages that are all managed
through Lerna. In order to get the development environment up and
running you will need to run the following commands:
The project is organized into several packages that are all managed through Lerna.
In order to get the development environment up and running you will need to run the following commands:
npm install
npx lerna bootstrap
This installs dependencies needed by tests and links the packages up so
they work from the git work tree.
This installs dependencies needed by tests and links the packages up so they work from the git work tree.
### Starting a Feature Branch
Development is done exclusively in feature branches. This means the
first thing to do before starting any development at all is to make a
new branch starting from the master or 1.2.x branch of the main
repository. To do this check out that branch from the main repository
(substitute master for 1.2.x if you're working on that branch.)
Development is done exclusively in feature branches.
This means the first thing to do before starting any development at all is to make a new branch starting from the master or 1.2.x branch of the main repository.
To do this check out that branch from the main repository (substitute master for 1.2.x if you're working on that branch).
git fetch upstream
git checkout upstream/master
This will give a warning about detached HEAD which means you did it
correctly. Now you can make a feature branch based on this with.
This will give a warning about detached HEAD which means you did it correctly.
Now you can make a feature branch based on this with.
git checkout -b my-feature-branch
The name of the branch is not important, as long as it's not a name
you've used before. But it's nice to have its name be somewhat
descriptive of what it implements.
The name of the branch is not important, as long as it's not a name you've used before.
But it's nice to have its name be somewhat descriptive of what it implements.
### Checking your code for errors
There's automated tests and linting setup for the project. Please do
add tests for any code you add and run both:
There's automated tests and linting setup for the project.
Please do add tests for any code you add and run both:
npm run test
npm run lint
To check that your changes pass the integration tests as well as the
ESLint rules set up for the project.
To check that your changes pass the integration tests as well as the ESLint rules set up for the project.
### Submitting a Pull Request
Once you've commited changes to your feature branch that you want to
have included into the main repository push it to you fork with
Once you've commited changes to your feature branch that you want to have included into the main repository push it to you fork with
git push origin my-feature-branch
and make a pull request through the GitHub web interface. You can also
pass the `--set-upstream` (or `-u`) flag to make `git push` do the same
thing for this branch. After your feature branch have been merged you
should consider it final. If there are more changes you want to have
merged or mistakes you need to fix you must start a new feature branch
and submit a new pull request
and make a pull request through the GitHub web interface.
You can also pass the `--set-upstream` (or `-u`) flag to make `git push` do the same thing for this branch.
After your feature branch have been merged you should consider it final.
If there are more changes you want to have merged or mistakes you need to fix you must start a new feature branch and submit a new pull request.
### Editing your Feature Branch
Until your feature branch is merged you can edit it as you see fit by
using the rebase tool. If your feature branch has been merged however
it's final and you will have to start a new feature branch and submit
that in a new pull request. The base usage of rebase assuming you are
checked out on your feature branch is the following command (substitute
master with 1.2.x if your feature branch is based on that)
Until your feature branch is merged you can edit it as you see fit by using the rebase tool.
If your feature branch has been merged however it's final and you will have to start a new feature branch and submit that in a new pull request.
The base usage of rebase assuming you are checked out on your feature branch is the following command (substitute master with 1.2.x if your feature branch is based on that)
git rebase -i upstream/master
This will open up an editor with the commits of your feature branch and
let you reorder, edit, and remove commits as you see fit.
This will open up an editor with the commits of your feature branch and let you reorder, edit, and remove commits as you see fit.
If the main repository has been updated and you have fetched down those
updates with `git fetch upstream` this will also move your feature
branch so that it is based on the latest commit from the main
repository.
If the main repository has been updated and you have fetched down those updates with `git fetch upstream` this will also move your feature branch so that it is based on the latest commit from the main repository.
If you have already pushed your feature branch to your fork a simple
`git push origin my-feature-branch` will no longer work as the histories
are different, instead you will have to run
If you have already pushed your feature branch to your fork a simple `git push origin my-feature-branch` will no longer work as the histories are different, instead you will have to run
git push origin +my-feature-branch
The + sign signifies a force push and should be used with care. If you
have a pull request that has not yet been merged submitted for this
feature branch this will update the pull request too.
The + sign signifies a force push and should be used with care.
If you have a pull request that has not yet been merged submitted for this feature branch this will update the pull request too.
### Resolving Merge Conflicts
The best way to resolve merge conflicts in feature braches is to rebase
it on top of the updated upstream. Run the following commands
(substitute master with 1.2.x if your feature branch is based on that)
The best way to resolve merge conflicts in feature braches is to rebase it on top of the updated upstream.
Run the following commands (substitute master with 1.2.x if your feature branch is based on that)
git fetch upstream
git rebase upstream/master
This will stop on the commit(s) that caused the merge conflict and let
you resolve them manually before continuing. Once you're done force
push your updated feature branch with
This will stop on the commit(s) that caused the merge conflict and let you resolve them manually before continuing.
Once you're done force push your updated feature branch with
git push origin +my-feature-branch
### Updating Your GitHub Fork
No. This is pointless busywork that accomplishes nothing. Fetch the
main repository directly as described above and base your feature
branches on the refs from it. GitHub has no interface or functionality
for keeping the branches in your fork up to date with the branches in
the main repository and there is no point in doing this for our feature
branch based workflow.
No.
This is pointless busywork that accomplishes nothing.
Fetch the main repository directly as described above and base your feature branches on the refs from it.
GitHub has no interface or functionality for keeping the branches in your fork up to date with the branches in the main repository and there is no point in doing this for our feature branch based workflow.
Changelog
---------
## Changelog
There's a [Changelog](../CHANGELOG.md) in the root of the project. If
you make changes that are visible to users of this project you should
add an entry to the changelog describing what has changed. The format
of this log should be self explanatory, please follow it carefully.
There's a [Changelog](../CHANGELOG.md) in the root of the project.
If you make changes that are visible to users of this project you should add an entry to the changelog describing what has changed.
The format of this log should be self explanatory, please follow it carefully.
Supported Node.js and Factorio Versions
---------------------------------------
## Supported Node.js and Factorio Versions
Clusterio run on Node.js v12 and up, and v10 from v10.16.0, make sure
to check that the Node builtin API's and npm packages you use are
supported on these versions. The Travis CI tests are runned on whatever
version of Node.js v10 is the default, which at the time of writing is
v10.18.0.
Clusterio run on Node.js v12 and up, and v10 from v10.16.0, make sure to check that the Node builtin API's and npm packages you use are supported on these versions.
The Travis CI tests are runned on whatever version of Node.js v10 is the default, which at the time of writing is v10.18.0.
For Factorio Clusterio 2.0 aims to support version 0.17.69 and up,
including the latest experimental release. It's recommended that you
use the lua API reference for 0.17.69, as there's no information on what
version Factorio API's were introduced in. The Travis CI tests are
runned against latest experimental release.
For Factorio Clusterio 2.0 aims to support version 0.17.69 and up, including the latest experimental release.
It's recommended that you use the lua API reference for 0.17.69, as there's no information on what version Factorio API's were introduced in.
The Travis CI tests are runned against latest experimental release.
Code Style
----------
## Code Style
The style of the code is a bit of a mixed bag at the moment. But there
are at least a few things that have been agreed upon.
The style of the code is a bit of a mixed bag at the moment.
But there are at least a few things that have been agreed upon.
### Naming style
JavaScript variables in general uses camelCase. The exception is for
classes/constructors which use PascalCase and variables destructured
from over the wire objects.
JavaScript variables in general uses camelCase.
The exception is for classes/constructors which use PascalCase and variables destructured from over the wire objects.
Fields in configs and messages sent over links uses
lowercase_underscore.
Fields in configs and messages sent over links uses lowercase_underscore.
JavaScript files are named using lowercase_underscore for files that
export multiple items. Files containing and exporting a single class
should be named the same as the class in PascalCase.
JavaScript files are named using lowercase_underscore for files that export multiple items.
Files containing and exporting a single class should be named the same as the class in PascalCase.
Imported modules from the lib package are prefixed with lib to make them
easier to distinguish, as the names tend to be very generic. E.g.:
Imported modules from the lib package are prefixed with lib to make them easier to distinguish, as the names tend to be very generic.
E.g.:
const libLink = require("@clusterio/lib/link");
const libLuaTools = require("@clusterio/lib/lua_tools");
@@ -232,9 +186,7 @@ For lua code lowercase_underscore is used for everything.
### Strings
JavaScript and Lua code should use double quoted strings (`"string"`)
for all strings with the only exception that strings that contain double
quotes may use single quotes (i.e., `'String with "quotes"'`).
JavaScript and Lua code should use double quoted strings (`"string"`) for all strings with the only exception that strings that contain double quotes may use single quotes (i.e., `'String with "quotes"'`).
JavaScript template literals may also be used where appropriate.
@@ -248,8 +200,8 @@ Lua code should be indented with 4 spaces.
### Line Length
Lines are limited to 120 characters where tabs count as 4 characters.
If the argument list for a function or expressino for compound statement
gets very long break them out on their own line(s). For example:
If the argument list for a function or expressino for compound statement gets very long break them out on their own line(s).
For example:
function foo(
really, long, argument, list,
@@ -262,3 +214,59 @@ gets very long break them out on their own line(s). For example:
// code
}
})
### Markdown
Markdown files are formatted using one line per sentence to improve diff reports when making changes.
Headings have two spaces before them and on space after them to make them stand out, the heading at the top of the file does not have any spaces before it, and heading immediadly followed by another heading have one space between them.
For example:
```md
# A markdown File
Sentence of text which end in a period.
Each sentence is on it's own line.
And there's two spaces before the next heading.
## Subheading
### Sub-subheading
Headings use the `# heading` format.
```
## Pushing releases to npm
The releases are done by creating a new release tag with lerna and then uploading that with lerna.
You will need commit access to the repository as well publish access to the @clusterio org on npm to do this.
1. To do a release switch to the master branch and make sure it's up to date with the repository before proceeding.
```sh
git switch master
git pull --ff-only upstream master
```
Check and make sure there are no untracked files with `git status`, otherwise they will be auto commited!
2. Run a lerna version to create and tag a new version.
This will push a commit to the master branch.
```sh
npx lerna version prerelease --git-remote upstream --no-granular-pathspec
```
3. Run a full build in every package to update the dist/ files.
```sh
npx lerna run build
```
4. Publish the packages with lerna.
```sh
npx lerna publish from-package
```
+104 -198
View File
@@ -1,13 +1,9 @@
Developing for Clusterio
========================
# Developing for Clusterio
This document describes the various different types of content that can
be created for Factorio and/or Clusterio and how this is made compatible
with Clusterio.
This document describes the various different types of content that can be created for Factorio and/or Clusterio and how this is made compatible with Clusterio.
Contents
--------
## Contents
- [Factorio Mods](#factorio-mods)
- [Factorio Scenarios](#factorio-scenarios)
@@ -18,35 +14,21 @@ Contents
- [Clusterio Plugins](#clusterio-plugins)
Factorio Mods
-------------
## Factorio Mods
Factorio mods that do not interact with Clusterio should not have any
considerations that need to be taken into account for them to work with
Clusterio.
Factorio mods that do not interact with Clusterio should not have any considerations that need to be taken into account for them to work with Clusterio.
Factorio Scenarios
------------------
## Factorio Scenarios
Clusterio patches saves and because of this scenarios have a few
limitations and must explicitly mark themselves as compatible with
Clusterio to work. The main limitation is that the
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
lib must be used, and control.lua cannot contain any code other than
calls to event_handler loading the relevant libs of the scenario.
This is because the control.lua file will be overwritten by the save
patcher. Scenarios also cannot use the event registering functions
exposed by `script` such as `script.on_event` or `script.on_nth_tick` as
using these will overwrite the handlers registered by the
event_handler lib.
Clusterio patches saves and because of this scenarios have a few limitations and must explicitly mark themselves as compatible with Clusterio to work.
The main limitation is that the [event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua) lib must be used, and control.lua cannot contain any code other than calls to event_handler loading the relevant libs of the scenario.
This is because the control.lua file will be overwritten by the save patcher.
Scenarios also cannot use the event registering functions exposed by `script` such as `script.on_event` or `script.on_nth_tick` as using these will overwrite the handlers registered by the event_handler lib.
A brief description of the usage of the event_handler library is
provided in the [event_handler interface](#event_handler-interface)
section.
A brief description of the usage of the event_handler library is provided in the [event_handler interface](#event_handler-interface) section.
Once a scenario is made using the event_handler it should have a
control.lua file that looks something like this:
Once a scenario is made using the event_handler it should have a control.lua file that looks something like this:
```lua
-- control.lua
@@ -55,8 +37,7 @@ event_handler.add_lib(require("module_foo"))
event_handler.add_lib(require("module_bar"))
```
To make Clusterio recognize and correctly handle this scenario it
needs to include a clusterio.json file with following content:
To make Clusterio recognize and correctly handle this scenario it needs to include a clusterio.json file with following content:
```json
{
@@ -70,60 +51,43 @@ needs to include a clusterio.json file with following content:
}
```
This tells Clusterio that it should load both module_foo and module_bar
by passing the result of requiring them to the `add_lib` function when
writing a new control.lua file to the save.
This tells Clusterio that it should load both module_foo and module_bar by passing the result of requiring them to the `add_lib` function when writing a new control.lua file to the save.
### event_handler interface
<sub>**Note:** At the time of writing the event_handler lib has been
a part of Factorio since at least 0.17.4, and was moved from base to
core in 0.17.63 but is still not documented anywhere. A brief
description is provided here for this reason.</sub>
<sub>**Note:** At the time of writing the event_handler lib has been a part of Factorio since at least 0.17.4, and was moved from base to core in 0.17.63 but is still not documented anywhere.
A brief description is provided here for this reason.</sub>
The event_handler lib provides a simple interface for registering
multiple callbacks to the same events without having to be concerned
with callback chaining or overwriting callbacks defined elsewhere. It
works by taking over the task of registering the actual callbacks with
Factorio, providing its own interface for the rest of the code to use.
The event_handler lib provides a simple interface for registering multiple callbacks to the same events without having to be concerned with callback chaining or overwriting callbacks defined elsewhere.
It works by taking over the task of registering the actual callbacks with Factorio, providing its own interface for the rest of the code to use.
The sole function of interest exported by event_handler is `add_lib`
which accepts a table with event handler callbacks definitions that it
will register. The following entries are recognized by `add_lib`, all
of which are optional:
The sole function of interest exported by event_handler is `add_lib` which accepts a table with event handler callbacks definitions that it will register.
The following entries are recognized by `add_lib`, all of which are optional:
- `on_init`:
Callback called when the callback of `script.on_init` is invoked.
- `on_load`:
Callback called when the callback of `script.on_load` is invoked.
- `on_configuration_changed`:
Callback called when the callback of
`script.on_configuration_changed` is invoked.
Callback called when the callback of `script.on_configuration_changed` is invoked.
- `events`:
Table mapping event ids as defined in `defines.events` to callbacks.
For example the table `{ [defines.events.on_player_died] = foo }`
will call bar with the usual `event` table as argument every time a
player dies.
For example the table `{ [defines.events.on_player_died] = foo }` will call bar with the usual `event` table as argument every time a player dies.
- `on_nth_tick`:
Table mapping the nth tick number to a callback. For example the
table `{ [30] = bar }` will call bar every 30th tick.
Table mapping the nth tick number to a callback.
For example the table `{ [30] = bar }` will call bar every 30th tick.
- `add_remote_interface`:
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
has no special meaning and receives no arguments but should be used
for registering remote interfaces.
Callback called before on_init/on_load callbacks<sup>[1]</sup>.
It has no special meaning and receives no arguments but should be used for registering remote interfaces.
- `add_commands`:
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
has no special meaning and receives no arguments but should be used
for registering commands.
Callback called before on_init/on_load callbacks<sup>[1]</sup>.
It has no special meaning and receives no arguments but should be used for registering commands.
<sub>1: Before 0.17.69 these callbacks were called after
on_init/on_load.</sub>
<sub>1: Before 0.17.69 these callbacks were called after on_init/on_load.</sub>
The usual way to use `add_lib` is to define the table of events to
register in a separate file and return it, then load it in control.lua
via `require` before passing it to `add_lib`. For example a module
could be defined as
The usual way to use `add_lib` is to define the table of events to register in a separate file and return it, then load it in control.lua via `require` before passing it to `add_lib`.
For example a module could be defined as
```lua
-- example.lua
@@ -148,45 +112,27 @@ local event_handler = require("event_handler")
event_handler.add_lib(require("example"))
```
Because the event_handler lib registers events itself you may not use
`script.on_load` or `script.on_init` at all in your code and any usage
of `script.on_configuration_changed`, `script.on_nth_tick` and/or
`script.on_event` will cause the corresponding events registered with
event_handler to break and should therefore not be used.
Because the event_handler lib registers events itself you may not use `script.on_load` or `script.on_init` at all in your code and any usage of `script.on_configuration_changed`, `script.on_nth_tick` and/or `script.on_event` will cause the corresponding events registered with event_handler to break and should therefore not be used.
There's also the `add_libraries` function exported by event_handler,
which accepts a table and calls `add_lib` for each value in the table.
There's also the `add_libraries` function exported by event_handler, which accepts a table and calls `add_lib` for each value in the table.
Communicating with Clusterio
----------------------------
## Communicating with Clusterio
Clusterio uses a homebrew protocol based on sending JSON payloads over
a WebSocket connection. See [socket.md](socket.md) for the
implementation details of it.
Clusterio uses a homebrew protocol based on sending JSON payloads over a WebSocket connection.
See [socket.md](socket.md) for the implementation details of it.
It's also possible to write a plugin for Clusterio that exposes a custom
interface over HTTP, WebSocket or any other technology supported by
Node.js.
It's also possible to write a plugin for Clusterio that exposes a custom interface over HTTP, WebSocket or any other technology supported by Node.js.
Clusterio Modules
-----------------
## Clusterio Modules
Modules are primarily used by plugins to inject code into Factorio games
with the save patcher, though it's also possible to make stand alone
modules that are loaded into the game if you don't need the capabilites
of the plugin system. The save patcher puts modules into the `modules`
folder of the Factorio save and adds code to `control.lua` to load the
module according to the `load` and `require` options to the module.json
file. Like with scenarios for Clusterio the [event_handler
interface](#event_handler-interface) has to be used for any event
subscriptions in modules.
Modules are primarily used by plugins to inject code into Factorio games with the save patcher, though it's also possible to make stand alone modules that are loaded into the game if you don't need the capabilites of the plugin system.
The save patcher puts modules into the `modules` folder of the Factorio save and adds code to `control.lua` to load the module according to the `load` and `require` options to the module.json file.
Like with scenarios for Clusterio the [event_handler interface](#event_handler-interface) has to be used for any event subscriptions in modules.
Stand alone modules are placed into the modules folder of Clusterio,
plugin modules are located in the module folder of the plugin. In
either case a `module.json` file is required and has the following
structure:
Stand alone modules are placed into the modules folder of Clusterio, plugin modules are located in the module folder of the plugin.
In either case a `module.json` file is required and has the following structure:
```json
{
@@ -204,68 +150,44 @@ structure:
The following entries are supported in the module.json file:
- `name`:
Name of the module, must match the folder the module is located in
for stand alone modules or the name of the plugin the module is
located in.
Name of the module, must match the folder the module is located in for stand alone modules or the name of the plugin the module is located in.
- `version`:
Version of the module. Must be compatible with [Semantic Versioning
2.0.0](https://semver.org/). In plugin modules this defaults to the
version of the plugin otherwise it's required.
Version of the module.
Must be compatible with [Semantic Versioning 2.0.0](https://semver.org/).
In plugin modules this defaults to the version of the plugin otherwise it's required.
- `dependencies`:
Optional mapping of modules this module depends on and their
version. In the example `>=0.4.2` means that the foo module must be
at least version 0.4.2. See the [Ranges
syntax](https://www.npmjs.com/package/semver#ranges) of the
node-semver package for a full description of what operators are
supported. The events for dependencies is invoked before the events
of the dependent, and starting an instance will fail if the
dependencies cannot be satisfied. If not specified it will default
to depending on `clusterio`. Make sure to add `clusterio` to your
dependencies if you add other dependencies and depend on the Lua API.
Optional mapping of modules this module depends on and their version.
In the example `>=0.4.2` means that the foo module must be at least version 0.4.2.
See the [Ranges syntax](https://www.npmjs.com/package/semver#ranges) of the node-semver package for a full description of what operators are supported.
The events for dependencies is invoked before the events of the dependent, and starting an instance will fail if the dependencies cannot be satisfied.
If not specified it will default to depending on `clusterio`.
Make sure to add `clusterio` to your dependencies if you add other dependencies and depend on the Lua API.
- `require`:
Lua files to require in `control.lua`. This should only really be
needed if you want to make a global function available for use in
Lua commands. The paths are relative to the module's own folder and
should be specified using forward slashes as directory sepparators
if it's located in a sub directory in the module.
Lua files to require in `control.lua`.
This should only really be needed if you want to make a global function available for use in Lua commands.
The paths are relative to the module's own folder and should be specified using forward slashes as directory sepparators if it's located in a sub directory in the module.
- `load`:
Lua files to load with the `event_handler` lib, the result of
requiring the file will be passed to the `add_lib` function of the
`event_handler` lib in `control.lua`. See the section on the
[event_handler interface](#event_handler-interface) for a detailed
description on how the `event_handler` lib works. The paths are
relative to the module's own folder and should be specified using
forward slashes as directory sepparators if it's located in a sub
directory in the module.
Lua files to load with the `event_handler` lib, the result of requiring the file will be passed to the `add_lib` function of the `event_handler` lib in `control.lua`.
See the section on the [event_handler interface](#event_handler-interface) for a detailed description on how the `event_handler` lib works.
The paths are relative to the module's own folder and should be specified using forward slashes as directory sepparators if it's located in a sub directory in the module.
It's recommended to use the new style of defining modules in Lua, as
well as avoid the use of global variables as much as possible. This
means always declearing your top level variables and functions local and
exporting only the things you need in other files. You can require
other files in your module by prefixing your require paths with
`modules/your_module_name/`. It's also possible to require files from
other modules this way too.
It's recommended to use the new style of defining modules in Lua, as well as avoid the use of global variables as much as possible.
This means always declearing your top level variables and functions local and exporting only the things you need in other files.
You can require other files in your module by prefixing your require paths with `modules/your_module_name/`.
It's also possible to require files from other modules this way too.
The global variables and data stored in the global table is shared by
all Clusterio modules as well as the scenario, so it's important that
you use unique names. It's recommended to prefix the global variables
and data in the global table that your module has with the name of your
module.
The global variables and data stored in the global table is shared by all Clusterio modules as well as the scenario, so it's important that you use unique names.
It's recommended to prefix the global variables and data in the global table that your module has with the name of your module.
Because modules can be patched into an existing game you cannot rely on
the `on_init` callback to be called in Clusterio Modules. Nor can you
rely on the `on_configuration_changed` callback, as this is not called
when level code changes. The Clusterio Lua API provides the custom
`on_server_startup` event that can be used as a substitute, see the next
section.
Because modules can be patched into an existing game you cannot rely on the `on_init` callback to be called in Clusterio Modules.
Nor can you rely on the `on_configuration_changed` callback, as this is not called when level code changes.
The Clusterio Lua API provides the custom `on_server_startup` event that can be used as a substitute, see the next section.
Clusterio Lua API
-----------------
## Clusterio Lua API
Clusterio provides a save patched module as well as a regular Factorio
mod for interfacing with Clusterio. This includes tools sending data to
plugins, script events to listen for, and an item serialization module.
Clusterio provides a save patched module as well as a regular Factorio mod for interfacing with Clusterio.
This includes tools sending data to plugins, script events to listen for, and an item serialization module.
### clusterio_api library
@@ -276,30 +198,27 @@ From a module
From a mod
`local clusterio_api = require("__clusterio_lib__/api")`
Provides the main interface to Clusterio from within the game. When
using the mod version you must call `init` before it'll function
properly.
Provides the main interface to Clusterio from within the game.
When using the mod version you must call `init` before it'll function properly.
#### init
<sub>Mod version only</sub>
Initialize the API. This should be called from both the `on_init` and
`on_load` events in your mod, and before this is called certain features
will not be available, most notably the events table.
Initialize the API.
This should be called from both the `on_init` and `on_load` events in your mod, and before this is called certain features will not be available, most notably the events table.
#### clusterio_api.events
Table of events raised by clusterio. If the game is started outside of
Clusterio then none of these events will be raised.
Table of events raised by clusterio.
If the game is started outside of Clusterio then none of these events will be raised.
In the mod version this table does not exist before the `init` function
is called.
In the mod version this table does not exist before the `init` function is called.
##### on_instance_updated
Raised after the name and id of an instance has been updated. This may
occur even if the id and name didn't change.
Raised after the name and id of an instance has been updated.
This may occur even if the id and name didn't change.
Event data:
- `instance_id`: the id of the instance.
@@ -307,33 +226,26 @@ Event data:
##### on_server_startup
Raised when Clusterio is starting up the server. It can be used to
initialize clusterio related features in a mod, and as a stand-in for
the `on_init` and `on_configuration_changed` event for modules. It is
invoked on the first tick the server runs after the save has been
patched, before most other events.
Raised when Clusterio is starting up the server.
It can be used to initialize clusterio related features in a mod, and as a stand-in for the `on_init` and `on_configuration_changed` event for modules.
It is invoked on the first tick the server runs after the save has been patched, before most other events.
Use this event to initialize and/or migrate the data structures
you need. Keep in mind that both mods and Clusterio modules can switch
from any version to any version so it should be able to handle both
forwards and backwards migrations gracefully.
Use this event to initialize and/or migrate the data structures you need.
Keep in mind that both mods and Clusterio modules can switch from any version to any version so it should be able to handle both forwards and backwards migrations gracefully.
#### clusterio_api.send_json(channel, data)
Send JSON data to Clusterio over the given channel. The `data` argument
should be table that can be serialized with `game.table_to_json`.
Clusterio plugins can listen to channels and will receive an event with
the data sent here. See the [Communicating with Factorio
section](writing-plugins.md#communicating-with-factorio) in the Writing
Plugins document for more information.
Send JSON data to Clusterio over the given channel.
The `data` argument should be table that can be serialized with `game.table_to_json`.
Clusterio plugins can listen to channels and will receive an event with the data sent here.
See the [Communicating with Factorio section](writing-plugins.md#communicating-with-factorio) in the Writing Plugins document for more information.
If the game is started outside of Clusterio the data will be sent, but
since there's no code following stdout to pick it up it will be lost.
If the game is started outside of Clusterio the data will be sent, but since there's no code following stdout to pick it up it will be lost.
**Note**: Payloads greater than 4 MB will cause stuttering in the game.
**Note**: This is not a binary safe way of sending data. Strings
embedded into the tables sent must be valid UTF-8 text.
**Note**: This is not a binary safe way of sending data.
Strings embedded into the tables sent must be valid UTF-8 text.
Parameters:
- `channel`: string identifying which channel to send it on.
@@ -348,19 +260,17 @@ From a module
From a mod
`local serialize = require("__clusterio_lib__/serialize")`
Serializes inventories and item stacks into a table that in turn can be
converted into JSON with `game.table_to_json`. Properly handles the
extra data for most items.
Serializes inventories and item stacks into a table that in turn can be converted into JSON with `game.table_to_json`.
Properly handles the extra data for most items.
#### serialize.serialize_equipment_grid(LuaEquipmentGrid)
Returns a table containing a serialized representation of the equipment
grid passed.
Returns a table containing a serialized representation of the equipment grid passed.
#### serialize.deserialize_equipment_grid(LuaEquipmentGrid, serialized_grid)
Deserialize a previously serialized equipment grid into the target
LuaEquipmentGrid. Overwrites all content in the target.
Deserialize a previously serialized equipment grid into the target LuaEquipmentGrid.
Overwrites all content in the target.
#### serialize.serialize_item_stack(LuaItemStack, destination_table)
@@ -369,23 +279,19 @@ The destination_table should be a new empty table.
#### serialize.deserialize_item_stack(LuaItemStack, serialized_stack)
Deserialize a previously serialized item stack into the target
LuaItemStack. Overwrites the content of the item stack.
Deserialize a previously serialized item stack into the target LuaItemStack.
Overwrites the content of the item stack.
#### serialize.serialize_inventory(LuaInventory)
Returns a table containing a serialized representation of the inventory
passed.
Returns a table containing a serialized representation of the inventory passed.
#### serialize.deserialize_inventory(LuaInventory, serialized_inventory)
Deserialize a previously serialized inventory into the target
LuaInventory. Overwrites slots that have content in the serialized
inventory.
Deserialize a previously serialized inventory into the target LuaInventory.
Overwrites slots that have content in the serialized inventory.
Clusterio Plugins
-----------------
## Clusterio Plugins
See the [Writing Plugins](writing-plugins.md) document for details on
the plugin interface and how to write plugins for Clusterio.
See the [Writing Plugins](writing-plugins.md) document for details on the plugin interface and how to write plugins for Clusterio.
+10 -24
View File
@@ -1,30 +1,16 @@
Save Patching
-------------
## Save Patching
In order to efficiently get data in and out of running Factorio servers
and to provide custom interfaces Clusterio uses save
patching<sup>[1]</sup> to add code to the game. The save patching is
based on the built in
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
library to Factorio and requires scenarios to be coded specifically for
it. Mods on the other hand work without modifications.
In order to efficiently get data in and out of running Factorio servers and to provide custom interfaces Clusterio uses save patching<sup>[1]</sup> to add code to the game.
The save patching is based on the built in [event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua) library to Factorio and requires scenarios to be coded specifically for it.
Mods on the other hand work without modifications.
This can be disabled with the `factorio.enable_save_patching` config on
a per instance basis, however disabling it means that modules are not
loaded into the game and most plugins will not function.
This can be disabled with the `factorio.enable_save_patching` config on a per instance basis, however disabling it means that modules are not loaded into the game and most plugins will not function.
<sub>1: Before version 2.0 of Clusterio this was done at runtime
with a patcher called Hotpatch, but due to the difficulty in supporting
run time patching and the lack of documentation it was replace it with
save patching instead.</sub>
<sub>1: Before version 2.0 of Clusterio this was done at runtime with a patcher called Hotpatch, but due to the difficulty in supporting run time patching and the lack of documentation it was replace it with save patching instead.</sub>
User Accounts
-------------
## User Accounts
The master server creates and stores accounts for all Factorio users
that log into instances under its control. These accounts are used to
store per player data and authorize management of the cluster. By
default users accounts are assigned a Player role that gives a limited
read access to the cluster resources, and admins of the cluster can be
assigned a Cluster Admin role to get access to manage it.
The master server creates and stores accounts for all Factorio users that log into instances under its control.
These accounts are used to store per player data and authorize management of the cluster.
By default users accounts are assigned a Player role that gives a limited read access to the cluster resources, and admins of the cluster can be assigned a Cluster Admin role to get access to manage it.
+31 -62
View File
@@ -1,39 +1,27 @@
Managing a Cluster
==================
# Managing a Cluster
Clusterio clusters are managed through the master server by using the
`clusterioctl` command line interface which is invoked by running
`npx clusterioctl <command>` in the clusterio directory. This document
uses the shorthand `ctl> foo` to indicate `npx clusterioctl foo` should
be executed. Mandatory parameters are shown in `<angles bracket>` and
optional pameters are in `[square brackets]`.
Clusterio clusters are managed through the master server by using the `clusterioctl` command line interface which is invoked by running `npx clusterioctl <command>` in the clusterio directory.
This document uses the shorthand `ctl> foo` to indicate `npx clusterioctl foo` should be executed.
Mandatory parameters are shown in `<angles bracket>` and optional pameters are in `[square brackets]`.
Before `clusterioctl` can be used it needs to be configured for the
cluster it will connect to. The easiest way to do this is to run
`npx clusteriomaster bootstrap create-ctl-config <username>` on the
master server, which creates the necessary `config-control.json` for
managing the cluster as the given user.
Before `clusterioctl` can be used it needs to be configured for the cluster it will connect to.
The easiest way to do this is to run `npx clusteriomaster bootstrap create-ctl-config <username>` on the master server, which creates the necessary `config-control.json` for managing the cluster as the given user.
Slaves
------
## Slaves
To be written.
Instances
---------
## Instances
To be written.
Users
-----
## Users
Clusterio automatically creates user accounts for all players that join
an instance when save patching is enabled. These accounts are used to
store per player data shared between instances, like if the player
should be whitelisted, admin or banned.
Clusterio automatically creates user accounts for all players that join an instance when save patching is enabled.
These accounts are used to store per player data shared between instances, like if the player should be whitelisted, admin or banned.
### List users
@@ -46,40 +34,32 @@ Lists all user accounts in the cluster along with some data for each.
ctl> user create <name>
Creates a new empty user account for the given Factorio player. Note
that case matters here. This is usually not required as user accounts
are created automatically for players that join instances with save
patching enabled.
Creates a new empty user account for the given Factorio player.
Note that case matters here.
This is usually not required as user accounts are created automatically for players that join instances with save patching enabled.
### Promote User to Server Admin
ctl> user set-admin <name> [--revoke] [--create]
Promotes the user given to in-game admin on instances with the
`sync_adminlist` option enabled. If the `--revoke` switch is used the
user is removed from the adminlist.
Promotes the user given to in-game admin on instances with the `sync_adminlist` option enabled.
If the `--revoke` switch is used the user is removed from the adminlist.
Since admin status is a part of the account data the account must exist
for this to succeed, passing `--create` will create the account if it
does not exist.
Since admin status is a part of the account data the account must exist for this to succeed, passing `--create` will create the account if it does not exist.
**Note:** Being a server admin does not grant any access to manage the
cluster. See [set-roles](#set-cluster-roles) for adding roles which
grant access to managing the clusetr.
**Note:** Being a server admin does not grant any access to manage the cluster.
See [set-roles](#set-cluster-roles) for adding roles which grant access to managing the clusetr.
### Whitelist User
ctl> user set-whitelisted <name> [--remove] [--create]
Add the user given to the whitelist on instances with the
`sync_whitelist` option enabled. If the `--remove` switch is used the
user is removed from the whitelist.
Add the user given to the whitelist on instances with the `sync_whitelist` option enabled.
If the `--remove` switch is used the user is removed from the whitelist.
Since whitelisted status is a part of the account data the account must
exist for this to succeed, passing `--create` will create the account if
it does not exist.
Since whitelisted status is a part of the account data the account must exist for this to succeed, passing `--create` will create the account if it does not exist.
### Ban User
@@ -87,16 +67,12 @@ it does not exist.
ctl> user set-banned <name> [--reason <message>] [--pardon] [--create]
Ban user in-game from instances with the `sync_banlist` option enabled.
Reason is a message that will be shown to the user when attemption to
log in. If the `--pardon` switch is used it removes the ban.
Reason is a message that will be shown to the user when attemption to log in.
If the `--pardon` switch is used it removes the ban.
Since ban status is a part of the account data the account must exist
for this to succeed, passing `--create` will create the account if it
does not exist.
Since ban status is a part of the account data the account must exist for this to succeed, passing `--create` will create the account if it does not exist.
Note: This bans the user from logging in to Factorio servers in the
cluster, it does not revoke access to any cluster management they might
have, see next section on setting roles for revoking tha.
Note: This bans the user from logging in to Factorio servers in the cluster, it does not revoke access to any cluster management they might have, see next section on setting roles for revoking tha.
### Set Cluster Roles
@@ -104,13 +80,9 @@ have, see next section on setting roles for revoking tha.
ctl> user set-roles <name> [roles...]
Replaces the roles the user has in the cluster with a new list of roles.
Calling with an empty roles argument will remove all roles from the
user.
Calling with an empty roles argument will remove all roles from the user.
By default there's a roled named Cluster Admin which grants access to
everything and a role named Player which grants a limited read access to
the cluster, see [the section on roles](#roles) for more information
about setting up roles and permissions.
By default there's a roled named Cluster Admin which grants access to everything and a role named Player which grants a limited read access to the cluster, see [the section on roles](#roles) for more information about setting up roles and permissions.
### Delete user
@@ -119,14 +91,11 @@ about setting up roles and permissions.
Deletes everything stored on the master server for this user.
Note: If the player is banned from the cluster this will effectively
unban them, as the ban status is stored with the user account.
Note: If the player is banned from the cluster this will effectively unban them, as the ban status is stored with the user account.
Note: If the player joins the cluster again a new account will be made
for them automatically.
Note: If the player joins the cluster again a new account will be made for them automatically.
Roles
-----
## Roles
To be written.
+29 -69
View File
@@ -1,54 +1,32 @@
Setting up TLS
==============
# Setting up TLS
By default Clusterio will listen for connections over HTTP which is fine
for local networks but over the Internet it should idealy be over HTTPS
as there are some credentials that could be exposed to eavesdroppers.
The recommended setup is to have an ordinary HTTPS server software like
Apache or Nginx to act as a reverse proxy in front of the Clusterio
master server. The reverse proxy would then be set up with a TLS
certificate from a Certificate Authoritiy like [Let's
Encrypt](https://letsencrypt.org). Alternatively you can have Clusterio
listen for connections over HTTPS directly by providing it with a TLS
certificate and private key.
By default Clusterio will listen for connections over HTTP which is fine for local networks but over the Internet it should idealy be over HTTPS as there are some credentials that could be exposed to eavesdroppers.
The recommended setup is to have an ordinary HTTPS server software like Apache or Nginx to act as a reverse proxy in front of the Clusterio master server.
The reverse proxy would then be set up with a TLS certificate from a Certificate Authoritiy like [Let's Encrypt](https://letsencrypt.org).
Alternatively you can have Clusterio listen for connections over HTTPS directly by providing it with a TLS certificate and private key.
Note that a TLS certificate signed by a recognized Certificate Authority
in general requires that you own a domain. See the last section for how
to create and use a self-signed certificate for an IP address if you
don't have a domain.
Note that a TLS certificate signed by a recognized Certificate Authority in general requires that you own a domain.
See the last section for how to create and use a self-signed certificate for an IP address if you don't have a domain.
Using a reverse proxy
---------------------
## Using a reverse proxy
Configure the proxy to forward requests over HTTP to the port the master
server is listening to so that for example a request to
`https://example.com/instances/` is forwarded to
`http://localhost:8080/instances/`. It is possible to host the
interface under a sub-path, in that case a request under the sub-path
should be mapped to the master server with the sub-path stripped out.
For example `https://example.com/sub/path/instances/` would be mapped to
`http://localhost:8080/instances`.
Configure the proxy to forward requests over HTTP to the port the master server is listening to so that for example a request to `https://example.com/instances/` is forwarded to `http://localhost:8080/instances/`.
It is possible to host the interface under a sub-path, in that case a request under the sub-path should be mapped to the master server with the sub-path stripped out.
For example `https://example.com/sub/path/instances/` would be mapped to `http://localhost:8080/instances`.
Additianlly WebSocket connections to `/api/socket` also needs to be
forwarded. Typically this requires explicit configuration and support
for forwarding WebSocket connections. If the interface hosted under a
sub-path the `/api/socket` address is relative to the sub-path and the
sub-path also needs to be stripped out when forwarded.
Additianlly WebSocket connections to `/api/socket` also needs to be forwarded.
Typically this requires explicit configuration and support for forwarding WebSocket connections.
If the interface hosted under a sub-path the `/api/socket` address is relative to the sub-path and the sub-path also needs to be stripped out when forwarded.
You can also optionally set the master server to listen only on the
loopback interface to prevent it being directly connected to from the
outside.
You can also optionally set the master server to listen only on the loopback interface to prevent it being directly connected to from the outside.
npx clusteriomaster config set master.bind_address 127.0.0.1
### Apache Config
To proxy with Apache make sure that `proxy_module`, `proxy_http_module`
and `proxy_wstunnel_module` is loaded in the config, then add the
following directives to wherever is the approriate place for your
flavour of Apache config organization.
To proxy with Apache make sure that `proxy_module`, `proxy_http_module` and `proxy_wstunnel_module` is loaded in the config, then add the following directives to wherever is the approriate place for your flavour of Apache config organization.
```apache
<Location /sub/path/>
@@ -59,54 +37,36 @@ flavour of Apache config organization.
</Location>
```
Replace `/sub/path/` with the location on the proxy you want to host the
Clusterio interface under. If the whole domain is to be dedicated to
the Clusterio interface then `/sub/path/` should be just `/`. Pay
attention to the use of trailing slashes, as they are important.
Replace `/sub/path/` with the location on the proxy you want to host the Clusterio interface under.
If the whole domain is to be dedicated to the Clusterio interface then `/sub/path/` should be just `/`.
Pay attention to the use of trailing slashes, as they are important.
Hosting HTTPS directly from Node.js
-----------------------------------
## Hosting HTTPS directly from Node.js
Listening on HTTPS is supported by the Clusterio master server via the
`master.https_port` option provided paths to a certificate file and a
private key file is configured for the `master.tls_certificate` and
`master.tls_private_key` options. Both of these are expected to be
unencrypted PEM files.
Listening on HTTPS is supported by the Clusterio master server via the `master.https_port` option provided paths to a certificate file and a private key file is configured for the `master.tls_certificate` and `master.tls_private_key` options.
Both of these are expected to be unencrypted PEM files.
Creating and using a self-signed certificate
--------------------------------------------
## Creating and using a self-signed certificate
Create a self-signed certificate with OpenSSL by running
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 -subj /CN=localhost -addext "subjectAltName=IP:<ip>"
substituting `<ip>` with the IP address the server will be accessed
under, for example `IP:203.0.113.4`. Multiple addresses can be given by
supplying a comma sepparated list of `IP:address` pairs and DNS names
can also be specified using the `DNS:example.com` format. You need to
supply every name you want to connect to the server under the
subjectAltName field, for example to connect to the HTTPS port locally
via an address like `https://localhost:4443/` you need to add
`DNS:localhost`.
substituting `<ip>` with the IP address the server will be accessed under, for example `IP:203.0.113.4`.
Multiple addresses can be given by supplying a comma sepparated list of `IP:address` pairs and DNS names can also be specified using the `DNS:example.com` format.
You need to supply every name you want to connect to the server under the subjectAltName field, for example to connect to the HTTPS port locally via an address like `https://localhost:4443/` you need to add `DNS:localhost`.
This creates a private key file named `key.pem` and a public certificate
file named `cert.pem` and these are used for the
`master.tls_certificate` and `master.tls_private_key` config options
respectively on the master server:
This creates a private key file named `key.pem` and a public certificate file named `cert.pem` and these are used for the `master.tls_certificate` and `master.tls_private_key` config options respectively on the master server:
npx clusteriomaster config set master.tls_certificate cert.pem
npx clusteriomaster config set master.tls_private_key key.pem
Additionally you will need copy the `cert.pem` file to all of the
computers you want to set up as slaves and configure it as the
`slave.tls_ca` option:
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as slaves and configure it as the `slave.tls_ca` option:
npx clusterioslave config set slave.tls_ca cert.pem
You will also need to copy the `cert.pem` file to all of the computers
you want to remotely manage the cluster via clusterctl and configure it
as `control.tls_ca` option:
You will also need to copy the `cert.pem` file to all of the computers you want to remotely manage the cluster via clusterctl and configure it as `control.tls_ca` option:
npx clusterioctl control-config set control.tls_ca cert.pem
+61 -123
View File
@@ -1,13 +1,8 @@
WebSocket protocol
==================
# 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:
<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
@@ -16,98 +11,66 @@ a JSON encoded object with the following three properties:
For the handshake and heartbeats the seq property is null.
Handshake
---------
## 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.
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.
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.
![Client connection flowchart](assets/connection_flow.svg)
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
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.
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).
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).
![Client reconnection flowchart](assets/reconnection_flow.svg)
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.
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.
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.
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.
![Client connection flowchart](assets/server_flow.svg)
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.
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.
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
------------------
## Handshake messages
### `hello`
@@ -115,8 +78,7 @@ 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&lt;string, string&gt; -
Object mapping plugin names to plugin version for plugins that are
loaded on the master server.
Object mapping plugin names to plugin version for plugins that are loaded on the master server.
### `register_slave`
@@ -128,8 +90,7 @@ Slave handshake for establishing a new connection session.
- name - string - Name of the slave.
- id - integer - ID of the slave.
- plugins - Object&lt;string, string&gt; -
Object mapping plugin names to plugin version for plugins that are
available on the slave.
Object mapping plugin names to plugin version for plugins that are available on the slave.
### `register_control`
@@ -141,17 +102,15 @@ Control handshake for enstablishing a new connection session.
### `resume`
Handshake for resuming an existing connection session where the
connection had dropped unexpectedly.
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.
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`
@@ -162,22 +121,19 @@ Sent by the master server to signal resuming the session was successful.
### `ready`
Sent by the master server to signal the connection has been established
with a new session.
Sent by the 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.
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
-----------------
## Example handshake
Server hello after opening the WebSocket.
@@ -221,32 +177,23 @@ Master server reply signaling the session is established and ready.
```
Events
------
## 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.
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.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized events and their contents.
Requests
--------
## 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.
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.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized requests and their contents.
### `*_response`
@@ -259,29 +206,20 @@ the recognized requests and their contents.
- error - string - Human readable text message describing the error.
Graceful Disconnect
-------------------
## 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.
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.
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.
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.
If the error response variant is received it should be logged if possible and treated as a success.
+203 -364
View File
@@ -1,13 +1,10 @@
Writing Plugins
===============
# Writing Plugins
Plugins for Clusterio are classes written in JavaScript that run under
Node.js. The plugin classes have pre-defined hooks that are called
during various stages and operations of Clusterio.
Plugins for Clusterio are classes written in JavaScript that run under Node.js.
The plugin classes have pre-defined hooks that are called during various stages and operations of Clusterio.
Contents
--------
## Contents
- [Plugin Structure](#plugin-structure)
- [Defining the plugin class](#defining-the-plugin-class)
@@ -23,8 +20,7 @@ Contents
- [Adding Custom Commands to clusterioctl](#adding-custom-commands-to-clusterioctl)
Plugin Structure
----------------
## Plugin Structure
The basic file structure of a plugin is the following.
@@ -40,21 +36,17 @@ The basic file structure of a plugin is the following.
+- module.json
+- plugin.lua
Clusterio plugins are Node.js packages that can be published on npm for
ease of installation and distribution. The usual guides for creating
such packages apply. At minimum the `package.json` file must contain a
version entry.
Clusterio plugins are Node.js packages that can be published on npm for ease of installation and distribution.
The usual guides for creating such packages apply.
At minimum the `package.json` file must contain a version entry.
A possible workflow for developing plugins is to place the plugin in a
sub-directory of where clusterio has been installed, and rely on Node.js
searching up the folder heirarchy for it to find `@clusterio/lib`. To
add it to `plugin-list.json` so that it gets loaded use the `plugin add
<path>` sub-command to either clusteriomaster, clusterioslave or
clusterioctl. Note that it's important that the path starts with ./ or
../ (use .\ or ..\ on Windows).
A possible workflow for developing plugins is to place the plugin in a sub-directory of where clusterio has been installed, and rely on Node.js searching up the folder heirarchy for it to find `@clusterio/lib`.
To add it to `plugin-list.json` so that it gets loaded use the `plugin add <path>` sub-command to either clusteriomaster, clusterioslave or clusterioctl.
Note that it's important that the path starts with ./ or ../ (use .\ or ..\ on Windows).
For a plugin the most important file is the `info.js` file. Without it
the plugin will not recognized by Clusterio. Here's an example of it:
For a plugin the most important file is the `info.js` file.
Without it the plugin will not recognized by Clusterio.
Here's an example of it:
const libLink = require("@clusterio/lib/link"); // For messages
@@ -72,70 +64,58 @@ the plugin will not recognized by Clusterio. Here's an example of it:
The following properties are recognized:
**name**:
Internal name of the plugin. Must be the same as the plugin's
directory name
Internal name of the plugin.
Must be the same as the plugin's directory name
**title**:
Name of the plugin as shown to users. Currently not used.
Name of the plugin as shown to users.
Currently not used.
**description**:
Brief description of what the plugin does. Currently not used.
Brief description of what the plugin does.
Currently not used.
**instanceEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the InstancePlugin class definition for this plugin. This
is an optional paramater. A plugin may have code only for instances
but it must still be loaded on the master in order for it to be
possible to load it on an instance.
Path to a Node.js module relative to the plugin directory which contains the InstancePlugin class definition for this plugin.
This is an optional paramater.
A plugin may have code only for instances but it must still be loaded on the master in order for it to be possible to load it on an instance.
**InstanceConfigGroup**:
Subclass of `PluginConfigGroup` for defining the per instance
configuration fields for this plugin. See [Plugin
Configuration](#plugin-configuration)
Subclass of `PluginConfigGroup` for defining the per instance configuration fields for this plugin.
See [Plugin Configuration](#plugin-configuration)
**masterEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the MasterPlugin class definiton for this plugin. This is
an optional parameter. A plugin can be made that only runs on the
master server.
Path to a Node.js module relative to the plugin directory which contains the MasterPlugin class definiton for this plugin.
This is an optional parameter.
A plugin can be made that only runs on the master server.
**MasterConfigGroup**:
Subclass of `PluginConfigGroup` for defining the master server
configuration fields for this plugin. See [Plugin
Configuration](#plugin-configuration)
Subclass of `PluginConfigGroup` for defining the master server configuration fields for this plugin.
See [Plugin Configuration](#plugin-configuration)
**controlEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the ControlPlugin class definition for this plugin. This
is an optional paramater. A plugin can be made that only runs on
the clusterioctl side.
Path to a Node.js module relative to the plugin directory which contains the ControlPlugin class definition for this plugin.
This is an optional paramater.
A plugin can be made that only runs on the clusterioctl side.
**messages**:
Object with link messages definitions for this plugin. See guide
for [defining link messages](#defining-link-messages) below.
Object with link messages definitions for this plugin.
See guide for [defining link messages](#defining-link-messages) below.
The optional module folder contains a Clusterio module that will be
patched into the save when the plugin is enabled. See the section on
[Clusterio Modules](developing-for-clusterio.md) in the Developing for
Clusterio document. The only restriction imposed on modules embedded
into plugins is that they must be named the same as the plugin.
The optional module folder contains a Clusterio module that will be patched into the save when the plugin is loaded.
See the section on [Clusterio Modules](developing-for-clusterio.md) in the Developing for Clusterio document.
The only restriction imposed on modules embedded into plugins is that they must be named the same as the plugin.
While there is no standard for how to organize a plugin it's recommended
to put the MasterPlugin class definition into master.js and the
InstancePlugin class definition into instance.js. You can put them into
whatever file you want (even the same one for both).
While there is no standard for how to organize a plugin it's recommended to put the MasterPlugin class definition into master.js and the InstancePlugin class definition into instance.js.
You can put them into whatever file you want (even the same one for both).
For both instanceEntrypoint and masterEntrypoint the path should not end
with .js and it should use forward slashes for directory separators if
any.
For both instanceEntrypoint and masterEntrypoint the path should not end with .js and it should use forward slashes for directory separators if any.
Defining the plugin class
-------------------------
## Defining the plugin class
The plugin class should derive from its respective base class defined in
`lib/plugin`. For example, to define a MasterPlugin class the following
code can be used:
The plugin class should derive from its respective base class defined in `lib/plugin`.
For example, to define a MasterPlugin class the following code can be used:
const libPlugin = require("@clusterio/lib/plugin");
@@ -152,13 +132,11 @@ code can be used:
MasterPlugin,
}
For the instance plugin it's exactly the same except "Master" is
replaced with "Instance", and for the clusterioctl plugin "Control" is
used. The available hooks that you can override are documented in the
base class [in lib/plugin.js](/packages/lib/plugin.js).
For the instance plugin it's exactly the same except "Master" is replaced with "Instance", and for the clusterioctl plugin "Control" is used.
The available hooks that you can override are documented in the base class [in lib/plugin.js](/packages/lib/plugin.js).
It's best to avoid defining a constructor, but if you insist on defining
one, forward all arguments to the base class. E.g.:
It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class.
E.g.:
constructor(...args) {
super(...args);
@@ -166,52 +144,41 @@ one, forward all arguments to the base class. E.g.:
// Code here
}
The arguments passed may change, and attempting to modify them will
result in unpredicatable behaviour. The async init method is always called
immediatly after the constructor, so there's little reason to do this.
The arguments passed may change, and attempting to modify them will result in unpredicatable behaviour.
The async init method is always called immediatly after the constructor, so there's little reason to do this.
Logging Messages
----------------
## Logging Messages
The base plugin classes provide a winston logger for logging messages to
the shared cluster log. For instances a copy of the log is also stored
on the slave the instance is on. To use it, pass a string to one of the
log levels functions, for example:
The base plugin classes provide a winston logger for logging messages to the shared cluster log.
For instances a copy of the log is also stored on the slave the instance is on.
To use it, pass a string to one of the log levels functions, for example:
async init() {
this.logger.info("Initializing frobbing");
}
Metadata covering which plugin and instance (for instance plugin
classes) the log event happened on will automatically be attached to the
log message.
Metadata covering which plugin and instance (for instance plugin classes) the log event happened on will automatically be attached to the log message.
The available levels are fatal, error, warn, audit, info, and verbose.
These levels have roughly the following meanings:
- fatal: Unrecoverable error that prevents continuing execution of
the Node.js process. Note that Plugins shouldn't have fatal errors.
- error: An unexpected error condition occured, usually an exception
thrown when it wasn't expected. Should be investigated and fixed.
- warn: An potential error was detected, but operation continued
gracefully. These may indicate that something is running
sub-optimally, or is not working as intedend.
- audit: Record of actions performed by users of the cluster. This
is usually emitted in response to management requests by a control
link.
- info: A general log message. May indicae a status or operation
completed. This should not be emitted by periodic tasks that run
often.
- verbose: A verbose log message which might be useful when
investigating issues, does not show by default. Note that logging
messages is not a cheap operation even when the messages do not show
up and spammy logging should be avoided for the verbose level.
If the logs are still valuable in certain cases consider putting them
behind a config option.
- fatal: Unrecoverable error that prevents continuing execution of the Node.js process.
Note that Plugins shouldn't have fatal errors.
- error: An unexpected error condition occured, usually an exception thrown when it wasn't expected.
Should be investigated and fixed.
- warn: An potential error was detected, but operation continued gracefully.
These may indicate that something is running sub-optimally, or is not working as intedend.
- audit: Record of actions performed by users of the cluster.
This is usually emitted in response to management requests by a control link.
- info: A general log message.
May indicae a status or operation completed.
This should not be emitted by periodic tasks that run often.
- verbose: A verbose log message which might be useful when investigating issues, does not show by default.
Note that logging messages is not a cheap operation even when the messages do not show up and spammy logging should be avoided for the verbose level.
If the logs are still valuable in certain cases consider putting them behind a config option.
For guarding unexpected errors the best option is to log a short
description along with the`stack` property of the Error:
For guarding unexpected errors the best option is to log a short description along with the`stack` property of the Error:
try {
// Operation that should not throw but may end up throwing
@@ -219,21 +186,14 @@ description along with the`stack` property of the Error:
this.logger.error(`Operation failed:\n${err.stack}`);
)
For plugin hooks expections thrown are automatically catched and logged,
but for event handlers registered on EventEmitters it's critical that
exceptions are catched and handled appropriately.
For plugin hooks expections thrown are automatically catched and logged, but for event handlers registered on EventEmitters it's critical that exceptions are catched and handled appropriately.
Plugin Configuration
--------------------
## Plugin Configuration
Clusterio provides a configuration system that handles storing,
distributing, editing and validating config fields for you. You can
take advantage of it by subclassing `PluginConfigGroup`, setting
`defaultAccess` to where config entries can be accessed from, setting the
`groupName` to your plugin name, defining fields on it, finalizing it,
and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in
the `info.js` export. For example in info.js:
Clusterio provides a configuration system that handles storing, distributing, editing and validating config fields for you.
You can take advantage of it by subclassing `PluginConfigGroup`, setting `defaultAccess` to where config entries can be accessed from, setting the `groupName` to your plugin name, defining fields on it, finalizing it, and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in the `info.js` export.
For example in info.js:
const libConfig = require("@clusterio/lib/config");
@@ -253,26 +213,22 @@ the `info.js` export. For example in info.js:
MasterConfigGroup: MasterConfigGroup,
};
Code inside the `MasterPlugin` class will then be able to access the
level config field through the `Config` object at `this.master.config`,
for example in the MasterPluginClass:
Code inside the `MasterPlugin` class will then be able to access the level config field through the `Config` object at `this.master.config`, for example in the MasterPluginClass:
async init() {
let level = this.master.config.get("foo_frobber.level");
this.logger.info(`I got a frobnication level of ${level}`);
}
The same applies for instance configs, replace "master" with "instance"
where appropriate. See [Configuration System](config-system.md)
for more details on how this system works.
The same applies for instance configs, replace "master" with "instance" where appropriate.
See [Configuration System](config-system.md) for more details on how this system works.
### Handling Invalid Configuration
If the plugin requires a certain feature to be enabled to function it
should throw an error during init if this is not the case. The most
common such feature is the save patching, which can be disabled to run
vanilla or scenarios not compatible with Clusterio. For example:
If the plugin requires a certain feature to be enabled to function it should throw an error during init if this is not the case.
The most common such feature is the save patching, which can be disabled to run vanilla or scenarios not compatible with Clusterio.
For example:
async init() {
if (!this.instance.config.get("factorio.enable_save_patching")) {
@@ -281,11 +237,9 @@ vanilla or scenarios not compatible with Clusterio. For example:
}
Communicating with Factorio
---------------------------
## Communicating with Factorio
For pushing data into Factorio there's RCON, which lets you send
arbitrary Lua commands to invoke whatever code you want in the game.
For pushing data into Factorio there's RCON, which lets you send arbitrary Lua commands to invoke whatever code you want in the game.
This is done by calling the `sendRcon` method on the plugin object.
For example:
@@ -298,19 +252,14 @@ For example:
}
Because data into Factorio is streamed at a rate of 3-6 kB/s by default,
it is recommended to avoid sending large commands as much as possible,
and to strip down the data on the ones you send to only what's strictly
necessary. You can have lua code injected into the game via the module
system and call that from RCON to avoid having to send code through the
commands.
Because data into Factorio is streamed at a rate of 3-6 kB/s by default, it is recommended to avoid sending large commands as much as possible, and to strip down the data on the ones you send to only what's strictly necessary.
You can have lua code injected into the game via the module system and call that from RCON to avoid having to send code through the commands.
For getting data out from Factorio there's both RCON and the `send_json`
API of the Clusterio module. Returning data via RCON is prefered if the
action is initiated from the Node.js side. The `send_json` API allows
sending JSON payloads on channels that plugins can listen to. From a
plugin you listen for an event named `ipc-channel_name` in order to get
data sent by `send_json`. For example in the plugin code:
For getting data out from Factorio there's both RCON and the `send_json` API of the Clusterio module.
Returning data via RCON is prefered if the action is initiated from the Node.js side.
The `send_json` API allows sending JSON payloads on channels that plugins can listen to.
From a plugin you listen for an event named `ipc-channel_name` in order to get data sent by `send_json`.
For example in the plugin code:
async init() {
this.instance.server.on("ipc-my_plugin_foo", content =>
@@ -331,52 +280,35 @@ And then in the module for the plugin:
-- inside some event handler
clusterio_api.send_json("my_plugin_foo", { data = 123 })
It's recommended to either use the plugin name as the channel name or to
prefix the channel name with the name of the plugin if you need multiple
channels. It's also important to catch any errors that might occur as
they will otherwise be propogated to the instance code and kill the
server.
It's recommended to either use the plugin name as the channel name or to prefix the channel name with the name of the plugin if you need multiple channels.
It's also important to catch any errors that might occur as they will otherwise be propogated to the instance code and kill the server.
Data out from Factorio does not have the same limits as data into
Factorio, RCON responses can be in 100kB range without causing issues,
and payloads to the `send_json` API can be in the 4MB range provided the
server has a fast enough storage system.
Data out from Factorio does not have the same limits as data into Factorio, RCON responses can be in 100kB range without causing issues, and payloads to the `send_json` API can be in the 4MB range provided the server has a fast enough storage system.
**Note:** both `send_json` and RCON can operate out of order. For
`send_json` it's possible that payloads greater than 4kB are received
after payloads that were sent at a later point in time. For RCON,
commands longer than 50 characters may end up being executed after
shorter commands sent after it.
**Note:** both `send_json` and RCON can operate out of order.
For `send_json` it's possible that payloads greater than 4kB are received after payloads that were sent at a later point in time.
For RCON, commands longer than 50 characters may end up being executed after shorter commands sent after it.
Defining Link Messages
----------------------
## Defining Link Messages
You will most likely have to communicate with the master or other
instances in your plugin for it to do anything useful. For this there's
a WebSocket communication channel established between the slaves and the
master server that plugins can define their own messages to send over
it. This channel is bi-directional and all messages sent over it are
validated with a JSON schema (see [this guide][guide] for an
introduction to writing JSON schema).
You will most likely have to communicate with the master or other instances in your plugin for it to do anything useful.
For this there's a WebSocket communication channel established between the slaves and the master server that plugins can define their own messages to send over it.
This channel is bi-directional and all messages sent over it are validated with a JSON schema (see [this guide][guide] for an introduction to writing JSON schema).
[guide]: https://json-schema.org/learn/getting-started-step-by-step.html
There are currently two kinds of messages that can be defined: events
and requests. Events are simple one-way notifications that invoke a
handler on the target it's sent to. Requests are pairs of request and
response messages where the request is sent to the target and the
response is the reply back from the target. The requests are similar to
HTTP requests, except both parties of a link may initiate one.
There are currently two kinds of messages that can be defined: events and requests.
Events are simple one-way notifications that invoke a handler on the target it's sent to.
Requests are pairs of request and response messages where the request is sent to the target and the response is the reply back from the target.
The requests are similar to HTTP requests, except both parties of a link may initiate one.
### Defining Events
Events are defined as properties of the messages object exported by
`info.js` that map to instances of the `Event` class from `lib/link`.
The name of the property correspond to the handler invoked on the plugin
class. The Event constructor takes an object of properties that define
the event, for example the following could be defined in `info.js`:
Events are defined as properties of the messages object exported by `info.js` that map to instances of the `Event` class from `lib/link`.
The name of the property correspond to the handler invoked on the plugin class.
The Event constructor takes an object of properties that define the event, for example the following could be defined in `info.js`:
messages: {
startFrobnication: new libLink.Event({
@@ -391,86 +323,65 @@ the event, for example the following could be defined in `info.js`:
}),
},
This specifies an event that can be sent from the master to a slave,
and from a slave to an instance. It also specifies that the event must
contain the property `frobnication_type`, with a string value in the
data payload and that it may optionally contain a boolean `urgent`
property. It will also be forwarded by slaves to a specific instance.
This specifies an event that can be sent from the master to a slave, and from a slave to an instance.
It also specifies that the event must contain the property `frobnication_type`, with a string value in the data payload and that it may optionally contain a boolean `urgent` property.
It will also be forwarded by slaves to a specific instance.
The following properties are recognized by the Event constructor:
#### type
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_event`
appended to it.
The message type sent over the wire.
This must start with the name of the plugin followed by colon and and be unique for the plugin.
The type of the message sent over the socket will have the suffix `_event` appended to it.
#### links
An array of strings describing which links this event can be sent over.
Direction matters, `"master-slave"` means the event can be sent from the
master to the slave, but can't be sent back the other way, unless
`"slave-master"` is also present in the links array.
Direction matters, `"master-slave"` means the event can be sent from the master to the slave, but can't be sent back the other way, unless `"slave-master"` is also present in the links array.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `"master-slave"` and
`"slave-instance"` in the links array). See `forwardTo` and
`broadcastTo` for ways to forward an event to the next link in a chain.
The available endpoints are `master`, `slave`, `instance`, and `control`.
Master talks with slave and control, and slave talks to instance.
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from master to instance it must have both `"master-slave"` and `"slave-instance"` in the links array).
See `forwardTo` and `broadcastTo` for ways to forward an event to the next link in a chain.
#### forwardTo
Target to forward an event to. Can either be `"master"`, to indicate a
slave should forward it to the master server, or `"instance"`, to
indicate it should be forwarded to the instances specified by the
`instance_id` event property. This works by using a default handler for
the event at the links that forward it.
Target to forward an event to.
Can either be `"master"`, to indicate a slave should forward it to the master server, or `"instance"`, to indicate it should be forwarded to the instances specified by the `instance_id` event property.
This works by using a default handler for the event at the links that forward it.
#### broadcastTo
Target to broadcast this message towards. A value of "instance" means
the event will be broadcast to all instances downstream of the target
it's sent to, but not back from where it came from. Currently, only
"instance" is supported. This means that sending an event to a slave
that slave except for the instance it came from. from an instance will
cause it to be broadcast to all instances of
Target to broadcast this message towards.
A value of "instance" means the event will be broadcast to all instances downstream of the target it's sent to, but not back from where it came from.
Currently, only "instance" is supported.
This means that sending the event to a slave from an instance will cause it to be broadcast to all instances of that slave except for the instance it came from.
#### eventRequired
By default all properties defined in `eventProperties` are required to
be present in the payload for the event. This can be overriden by
specifying an array of properties which are required here. This is
equivalent to using the `required` keyword in JSON schema.
By default all properties defined in `eventProperties` are required to be present in the payload for the event.
This can be overriden by specifying an array of properties which are required here.
This is equivalent to using the `required` keyword in JSON schema.
#### eventProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the event. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are by default required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas.
Object with properties mapping to a JSON schema of that property that specifies what's valid to send in the event.
This is equivalent to using the `properties` keyword in JSON schema, except that the properties specified are by default required and additional properties are not allowed.
See [this guide][guide] for an introduction to writing JSON schemas.
The forwardTo and broadcastTo can be combined such that specifying
`"master"` as the forwardTo value and `"instance"` as the broadcastTo
value will cause the event to be broadcast to all instances in the
cluster. For this to work, you will need to specify `instance-slave`,
`slave-master`, `master-slave`, and `slave-instance` as the links.
The forwardTo and broadcastTo can be combined such that specifying `"master"` as the forwardTo value and `"instance"` as the broadcastTo value will cause the event to be broadcast to all instances in the cluster.
For this to work, you will need to specify `instance-slave`, `slave-master`, `master-slave`, and `slave-instance` as the links.
Keep in mind when forwarding events that if the target an event is being
forwarded to is not online, the event will be dropped. Use a request if
you need a confirmation that the message was received.
Keep in mind when forwarding events that if the target an event is being forwarded to is not online, the event will be dropped.
Use a request if you need a confirmation that the message was received.
### Definining Requests
Requests are defined as properties of the messages object exported by
`info.js` that map to instances of the `Request` class from `lib/link`.
The name of the property corresponds to the handler invoked on the plugin
class. The Request constructor takes an object of properties that define
the event. For example, the following could be defined in `info.js`:
Requests are defined as properties of the messages object exported by `info.js` that map to instances of the `Request` class from `lib/link`.
The name of the property corresponds to the handler invoked on the plugin class.
The Request constructor takes an object of properties that define the event.
For example, the following could be defined in `info.js`:
messages: {
reportFrobnication: new libLink.Request({
@@ -491,151 +402,103 @@ the event. For example, the following could be defined in `info.js`:
}),
},
This specifies a request that can be sent from the master to a slave,
and from a slave to an instance. The request data must contain the
property `verbosity` with an integer number as the value, as well as the
`instance_id` property (implied by `forwardTo: "instance"`) and it may
also contain a boolean `special` property. It also defines that the
response sent must contain a `report` property mapping to an array of
strings. When received by a slave, it will also be forwarded to the
instance specified by `instance_id`.
This specifies a request that can be sent from the master to a slave, and from a slave to an instance.
The request data must contain the property `verbosity` with an integer number as the value, as well as the `instance_id` property (implied by `forwardTo: "instance"`) and it may also contain a boolean `special` property.
It also defines that the response sent must contain a `report` property mapping to an array of strings.
When received by a slave, it will also be forwarded to the instance specified by `instance_id`.
The following properties are recognized by the Request constructor:
#### type
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_request`
appended to it for the request and `_response` appended to it for the
response.
The message type sent over the wire.
This must start with the name of the plugin followed by colon and and be unique for the plugin.
The type of the message sent over the socket will have the suffix `_request` appended to it for the request and `_response` appended to it for the response.
#### links
An array of strings describing which links this request can be sent
over. Direction matters; `"master-slave"` means the request can be sent
from the master to the slave and the slave can reply to the master, but
the slave can't send a request to the master unless `"slave-master"` is
also present in the links array.
An array of strings describing which links this request can be sent over.
Direction matters; `"master-slave"` means the request can be sent from the master to the slave and the slave can reply to the master, but the slave can't send a request to the master unless `"slave-master"` is also present in the links array.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `"master-slave"` and
`"slave-instance"` in the links array). See `forwardTo` for ways to
forward a request to the next link in a chain.
The available endpoints are `master`, `slave`, `instance`, and `control`.
Master talks with slave and control, and slave talks to instance.
The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go from master to instance it must have both `"master-slave"` and `"slave-instance"` in the links array).
See `forwardTo` for ways to forward a request to the next link in a chain.
#### forwardTo
Target to forward the request to. Can either be `"master"` to indicate
a slave should forward it to the master server when receiving it from an
instance, or `"instance"` to indicate it should be forwarded to the
instances specified by the `instance_id` request property. This works
by using a default handler for the request by the links that forward it.
Target to forward the request to.
Can either be `"master"` to indicate a slave should forward it to the master server when receiving it from an instance, or `"instance"` to indicate it should be forwarded to the instances specified by the `instance_id` request property.
This works by using a default handler for the request by the links that forward it.
#### requestRequired
By default all properties defined in `requestProperties` are required to
be present in the payload for the request. This can be overriden by
specifying an array of properties which are required here. This is
equivalent to using the `required` keyword in JSON schema.
By default all properties defined in `requestProperties` are required to be present in the payload for the request.
This can be overriden by specifying an array of properties which are required here.
This is equivalent to using the `required` keyword in JSON schema.
#### requestProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the request. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are by default required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas
Object with properties mapping to a JSON schema of that property that specifies what's valid to send in the request.
This is equivalent to using the `properties` keyword in JSON schema, except that the properties specified are by default required and additional properties are not allowed.
See [this guide][guide] for an introduction to writing JSON schemas
#### responseRequired
Same as the requestRequired only for the response sent back by the
target.
Same as the requestRequired only for the response sent back by the target.
#### responseProperties
Same as the requestProperties only for the response sent back by the
target.
Same as the requestProperties only for the response sent back by the target.
Sending Link Messages
---------------------
## Sending Link Messages
Link messages are sent by calling the `.send()` method on the
Event/Request instance with the link you want to send it over and the
data to send. For `InstancePlugin` code the link to the slave is the
`instance` itself, which is accessible through the `.instance` property
of the `InstancePlugin`. The `.info` property of the plugin class exposes
the data exported from the plugin's `info.js` module. In other words:
Link messages are sent by calling the `.send()` method on the Event/Request instance with the link you want to send it over and the data to send.
For `InstancePlugin` code the link to the slave is the `instance` itself, which is accessible through the `.instance` property of the `InstancePlugin`.
The `.info` property of the plugin class exposes the data exported from the plugin's `info.js` module.
In other words:
// In an InstancePlugin class
async frobnicate() {
this.info.messages.exampleEvent.send(this.instance, { foo: "bar" });
}
For the Request class the send method is async and returns the response
data received from the target it was sent to, or throws an error if the
request failed.
For the Request class the send method is async and returns the response data received from the target it was sent to, or throws an error if the request failed.
### Handling connection events
There are a few connection related events that plugins neeed to repsond
to in order to avoid data loss and connection problems. The most
important is the prepare disconnect for the link between master and
slave. This is signaled to `MasterPlugin` classes via the
`onPrepareSlaveDisconnect` hook and to `InstancePlugin` classes via the
`onPrepareMasterDisconnect` hook.
There are a few connection related events that plugins neeed to repsond to in order to avoid data loss and connection problems.
The most important is the prepare disconnect for the link between master and slave.
This is signaled to `MasterPlugin` classes via the `onPrepareSlaveDisconnect` hook and to `InstancePlugin` classes via the `onPrepareMasterDisconnect` hook.
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:
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:
if (!this.slave.connector.closing) {
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
}
If the event or request needs to be sent to the master it can be put
into a queue stored on the plugin instance and sent out when the
connection is established again. The re-establishement of the
connection is notified to plugins via the `connect` event to the
`onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks.
If the event or request needs to be sent to the master it can be put into a queue stored on the plugin instance and sent out when the connection is established again.
The re-establishement of the connection is notified to plugins via the `connect` event to the `onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks.
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. 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.
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.
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.
Collecting Statistics
---------------------
## Collecting Statistics
Clusterio comes with its own Prometheus client implementation, one part
due to Not Invented Here and another part due to collectors in
prom-client being difficult to get to work nicely with collecting data
from plugins optionally loaded at runtime on different computers.
Clusterio comes with its own Prometheus client implementation, one part due to Not Invented Here and another part due to collectors in prom-client being difficult to get to work nicely with collecting data from plugins optionally loaded at runtime on different computers.
In its simplest form collecting data from plugins consists of defining
the metric and updating it somewhere in the plugin code. For example:
In its simplest form collecting data from plugins consists of defining the metric and updating it somewhere in the plugin code.
For example:
const { Counter } = require("@clusterio/lib/prometheus");
@@ -646,13 +509,10 @@ the metric and updating it somewhere in the plugin code. For example:
// Somewhere in the master plugin code
fooMetric.inc();
This works for master plugins, and the metric will be automatically
available through the /metric HTTP endpoint. It's recommended that
plugin metrics follow `clusterio_<plugin_name>_<metric_name>` as the
naming scheme.
This works for master plugins, and the metric will be automatically available through the /metric HTTP endpoint.
It's recommended that plugin metrics follow `clusterio_<plugin_name>_<metric_name>` as the naming scheme.
For metrics that are per-instance, you must define an `instance_id` label and
set it accordingly, for example:
For metrics that are per-instance, you must define an `instance_id` label and set it accordingly, for example:
const { Counter } = require("@clusterio/lib/prometheus");
@@ -664,26 +524,17 @@ set it accordingly, for example:
// Somewhere in the instance plugin code
barMetric.labels(String(this.instance.id)).set(someValue);
Metrics are automatically registered to the default registry, and this
default registry is automatically polled by the master server on slaves.
This means that it's important that you place the definition of the
metric at module level so that it's not created more than once over the
lifetime of a slave. Since the metrics remember their values and would
continue to be exported after an instance is shutdown, there's code at
instance shutdown that removes all the values where the `instance_id`
label matches the id of the instance shut down.
Metrics are automatically registered to the default registry, and this default registry is automatically polled by the master server on slaves.
This means that it's important that you place the definition of the metric at module level so that it's not created more than once over the lifetime of a slave.
Since the metrics remember their values and would continue to be exported after an instance is shutdown, there's code at instance shutdown that removes all the values where the `instance_id` label matches the id of the instance shut down.
For statistics you need to update on collection there's an `onMetrics`
hook on both master and instance plugins that is run before the
metrics in the default registry are collected.
For statistics you need to update on collection there's an `onMetrics` hook on both master and instance plugins that is run before the metrics in the default registry are collected.
Adding Custom Commands to clusterioctl
--------------------------------------
## Adding Custom Commands to clusterioctl
The control entrypoint for plugins allows you to extend clustectl with
your own commands. The creation of custom commands typically starts
with defining a command tree for the plugin:
The control entrypoint for plugins allows you to extend clustectl with your own commands.
The creation of custom commands typically starts with defining a command tree for the plugin:
const { Command, CommandTree } = require("@clusterio/lib/command");
const fooFrobberCommands = new CommandTree({
@@ -708,25 +559,13 @@ Then commands are added to the the plugin's command tree:
},
}));
For a command the `definition` is the arguments to pass to
[yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler)
(see also
[yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt)
and
[yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt)
for setting up positional and optional arguments to commands). The
`handler` is an async function that's invoked when the command is
executed and it's passed the parsed command line arguments and a
reference to the `Control` class of clusterioctl. It's possible to optain
a reference to the plugin class with `control.plugins.get(info.name)`.
For a command the `definition` is the arguments to pass to [yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler) (see also [yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt) and [yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt) for setting up positional and optional arguments to commands).
The `handler` is an async function that's invoked when the command is executed and it's passed the parsed command line arguments and a reference to the `Control` class of clusterioctl.
It's possible to optain a reference to the plugin class with `control.plugins.get(info.name)`.
Note that messages sent from clusterioctl needs to have
`"control-master"` as a part of the links array for it to be accepted by
the master server, see [Defining Link Messages](#defining-link-messages)
for how to define the messages that can be sent to the master.
Note that messages sent from clusterioctl needs to have `"control-master"` as a part of the links array for it to be accepted by the master server, see [Defining Link Messages](#defining-link-messages) for how to define the messages that can be sent to the master.
To have the command tree become part of clusterioctl it needs to be added
to the rootCommand tree in `addCommands` callback of the Control plugin:
To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Control plugin:
const libPlugin = require("@clusterio/lib/plugin");
+1 -1
View File
@@ -3,5 +3,5 @@
"packages/*",
"plugins/*"
],
"version": "2.0.0-alpha.4"
"version": "2.0.0-alpha.5"
}
+4 -3
View File
@@ -2,7 +2,7 @@
"name": "root",
"private": true,
"scripts": {
"test": "mocha --exit --check-leaks -R spec --recursive test plugins/global_chat/test plugins/research_sync/test plugins/statistics_exporter/test plugins/subspace_storage/test",
"test": "mocha --check-leaks -R spec --recursive test plugins/global_chat/test plugins/research_sync/test plugins/statistics_exporter/test plugins/subspace_storage/test plugins/player_auth/test",
"fast-test": "FAST_TEST=y npm test",
"cover": "nyc npm test",
"fast-cover": "FAST_TEST=y nyc npm test",
@@ -21,19 +21,20 @@
"node": ">=10.16 <11 || >= 12"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-react": "^7.12.1",
"@clusterio/ctl": "file:./packages/ctl",
"@clusterio/lib": "file:./packages/lib",
"@clusterio/master": "file:packages/master",
"@clusterio/slave": "file:./packages/slave",
"body-parser": "^1.18.2",
"eslint": "^7.2.0",
"express": "^4.16.2",
"fs-extra": "^8.1.0",
"jsdoc": "^3.6.3",
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"lerna": "^3.22.1",
"mocha": "^6.2.0",
"node-forge": "^0.10.0",
"nyc": "^14.1.1",
"phin": "^3.4.1",
"webpack": "^5.0.0"
+27 -44
View File
@@ -1,70 +1,57 @@
Clusterio Command Line Interface
================================
# Clusterio Command Line Interface
Provides a command line interface for managing a Clusterio cluster.
Note that this document only describes the options and commands for
managing clusterioctl itself, see [Managing a
cluster](/docs/managing-a-cluster.md) for the available commands used
for managing a cluster.
Note that this document only describes the options and commands for managing clusterioctl itself, see [Managing a cluster](/docs/managing-a-cluster.md) for the available commands used for managing a cluster.
Usage
-----
## Usage
npx clusterioctl <command>
Common options:
* `--plugin-list <file>` JSON file to use for storing the list of
plugins that are available to ctl. Defaults to `plugin-list.json`
and will be created if it does not exist. See the `plugin` command
for managing this list.
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to ctl.
Defaults to `plugin-list.json` and will be created if it does not exist.
See the `plugin` command for managing this list.
* `--config <file>` JSON file to use for storing configuration for
ctl. Defaults to `config-control.json` and will be created if it
does not exist. See the `config` command for inspecting and
modifying the configuration.
* `--config <file>` JSON file to use for storing configuration for ctl.
Defaults to `config-control.json` and will be created if it does not exist.
See the `config` command for inspecting and modifying the configuration.
### `plugin <command>`
Configure plugins available to be loaded by ctl. The available plugins
will be automatically loaded and the commands they provide will be added
to the ctl utility.
Configure plugins available to be loaded by ctl.
The available plugins will be automatically loaded and the commands they provide will be added to the ctl utility.
#### `plugin add <path>`
Add plugin either by require path or relative/absolute path to the
plugin directory. A relative path must start with ./ or ../ (or .\ and
..\ on Windows) otherwise it will be assumed to be a require path for an
installed package in node_modules.
Add plugin either by require path or relative/absolute path to the plugin directory.
A relative path must start with ./ or ../ (or .\ and ..\ on Windows) otherwise it will be assumed to be a require path for an installed package in node_modules.
For example, installing the Subspace Storage plugin:
npm install @clusterio/plugin-subspace_storage
npx clusterioctl plugin add @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin remove <name>`
Remove a plugin by its name. This should be done before uninstalling
the plugin, otherwise there will be an error when Clusterio tries to
load the info from the plugin. Removing and unistalling a plugin is
usually not neccessary as the functions provided by the plugin can be
disabled in the config.
Remove a plugin by its name.
This should be done before uninstalling the plugin, otherwise there will be an error when Clusterio tries to load the info from the plugin.
Removing and unistalling a plugin is usually not neccessary as the functions provided by the plugin can be disabled in the config.
For example, uninstalling the Subspace Storage plugin:
npx clusterioctl plugin remove subspace_storage
npm uninstall @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin list`
@@ -74,18 +61,17 @@ Lists the plugins set up to be available by name followed by path.
### `config-control`
Manage the ctl configuration. This allows setting the url and token to
the master server to manage.
Manage the ctl configuration.
This allows setting the url and token to the master server to manage.
#### `config-control set <config-entry> [value]`
Set a config entry to the given value. If value is not provided the
entry is set to null. If the config entry is of type object the value
must be a valid JSON serialization of an object.
Set a config entry to the given value.
If value is not provided the entry is set to null.
If the config entry is of type object the value must be a valid JSON serialization of an object.
See docs/configuration.md in the main repositiory for the available
configuration.
See docs/configuration.md in the main repositiory for the available configuration.
#### `config-control show <config-entry>`
@@ -100,9 +86,6 @@ Lists up all configuration entries with their currently configured values.
## See Also
[Managing a cluster](/docs/managing-a-cluster.md) for the available
commands used for managing a cluster.
[Managing a cluster](/docs/managing-a-cluster.md) for the available commands used for managing a cluster.
[The Clusterio
repository](https://github.com/clusterio/factorioClusterio)
for instructions on how to set up a cluster.
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
+32 -2
View File
@@ -13,6 +13,7 @@ const events = require("events");
const path = require("path");
const winston = require("winston");
const setBlocking = require("set-blocking");
const phin = require("phin");
const libLink = require("@clusterio/lib/link");
const libErrors = require("@clusterio/lib/errors");
@@ -104,6 +105,25 @@ masterConfigCommands.add(new libCommand.Command({
masterCommands.add(masterConfigCommands);
const masterPluginCommands = new libCommand.CommandTree({
name: "plugin", alias: ["p"], description: "master plugin inspection",
});
masterPluginCommands.add(new libCommand.Command({
definition: ["list", "List plugins on master"],
handler: async function(args, control) {
let url = new URL(control.config.get("control.master_url"));
url.pathname += "api/plugins";
let response = await phin({
url,
parse: "json",
core: { ca: control.tlsCa },
});
print(asTable(response.body));
},
}));
masterCommands.add(masterPluginCommands);
const slaveCommands = new libCommand.CommandTree({ name: "slave", description: "Slave management" });
slaveCommands.add(new libCommand.Command({
definition: [["list", "l"], "List slaves connected to the master"],
@@ -698,10 +718,20 @@ class ControlConnector extends libLink.WebSocketClientConnector {
*/
class Control extends libLink.Link {
constructor(connector, controlPlugins) {
constructor(connector, controlConfig, tlsCa, controlPlugins) {
super("control", "master", connector);
libLink.attachAllMessages(this);
/**
* Control config used for connecting to the master.
* @type {module:lib/config.ControlConfig}
*/
this.config = controlConfig;
/**
* Certificate authority used to validate TLS connections to the master.
* @type {?string}
*/
this.tlsCa = tlsCa;
/**
* Mapping of plugin names to their instance for loaded plugins.
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
@@ -917,7 +947,7 @@ async function startControl() {
tlsCa,
controlConfig.get("control.master_token")
);
let control = new Control(controlConnector, controlPlugins);
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
try {
await controlConnector.connect();
} catch (err) {
+3 -2
View File
@@ -3,7 +3,7 @@
"description": "Clusterio command line management tool",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"main": "ctl.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@@ -18,10 +18,11 @@
"node": ">=10.16 <11 || >= 12"
},
"dependencies": {
"@clusterio/lib": "^2.0.0-alpha.4",
"@clusterio/lib": "^2.0.0-alpha.5",
"as-table": "^1.0.31",
"chalk": "^3.0.0",
"fs-extra": "^8.1.0",
"phin": "^3.5.1",
"winston": "^3.3.3",
"yargs": "^14.2.0"
},
+3 -6
View File
@@ -1,8 +1,5 @@
Common Library for Clusterio
============================
# Common Library for Clusterio
This package contains functionality shared between master, slave, ctl
and/or plugins of Clusterio.
This package contains functionality shared between master, slave, ctl and/or plugins of Clusterio.
See [the Clusterio repository](https://github.com/clusterio/factorioClusterio)
for more information on Clusterio.
See [the Clusterio repository](https://github.com/clusterio/factorioClusterio) for more information on Clusterio.
+2 -2
View File
@@ -7,8 +7,8 @@
* @module lib/command
*/
"use strict";
const libLink = require("@clusterio/lib/link");
const libErrors = require("@clusterio/lib/errors");
const libLink = require("./link");
const libErrors = require("./errors");
/**
* Represents a command that can be runned by clusterioctl
+3 -3
View File
@@ -4,7 +4,7 @@
const events = require("events");
const isDeepStrictEqual = require("../is_deep_strict_equal");
const { basicType } = require("@clusterio/lib/helpers");
const { basicType } = require("../helpers");
/**
@@ -813,8 +813,8 @@ class PluginConfigGroup extends ConfigGroup {
if (!this._initialized) {
super._initSubclass();
this.define({
name: "enabled",
description: "Enable plugin",
name: "load_plugin",
title: "Load Plugin",
type: "boolean",
initial_value: true,
});
+1 -1
View File
@@ -8,7 +8,7 @@
"use strict";
const fs = require("fs-extra");
const { basicType } = require("@clusterio/lib/helpers");
const { basicType } = require("./helpers");
/**
+11
View File
@@ -16,6 +16,16 @@ class CommandError extends Error { }
*/
class RequestError extends Error { }
/**
* Thrown when a permission check fails.
*
* Is a subclass of RequestError to prevent logging stack traces when
* requests fail due to permission denied.
*
* @static
*/
class PermissionError extends RequestError { }
/**
* Signal for messages that fail validation
* @static
@@ -66,6 +76,7 @@ class PluginError extends Error {
module.exports = {
CommandError,
RequestError,
PermissionError,
InvalidMessage,
SessionLost,
AuthenticationFailed,
+1 -1
View File
@@ -6,7 +6,7 @@ const ini = require("ini");
const Jimp = require("jimp");
const JSZip = require("jszip");
const libBuildMod = require("@clusterio/lib/build_mod");
const libBuildMod = require("../build_mod");
/**
* Generate the export mod needed for exportData
+1 -1
View File
@@ -7,7 +7,7 @@ const JSZip = require("jszip");
const path = require("path");
const semver = require("semver");
const libHash = require("@clusterio/lib/hash");
const libHash = require("../hash");
const knownScenarios = {
+28 -10
View File
@@ -9,9 +9,9 @@ const crypto = require("crypto");
const ini = require("ini");
const rconClient = require("rcon-client");
const libErrors = require("@clusterio/lib/errors");
const { logger } = require("@clusterio/lib/logging");
const { escapeRegExp } = require("@clusterio/lib/helpers");
const libErrors = require("../errors");
const { logger } = require("../logging");
const { escapeRegExp } = require("../helpers");
/**
@@ -543,14 +543,18 @@ class FactorioServer extends events.EventEmitter {
this._gameReady = false;
if (options.stripPaths) {
let chars = new Set(this.writePath("temp"));
let chars = new Set(path.resolve(this.writePath("temp")));
chars.delete(":"); // Having a colon could lead to matching the line number
chars = [...chars].join("");
let tempPath = `${path.resolve(this.writePath("temp", "currently-playing"))}${path.sep}`;
let writePath = `${path.resolve(this.writePath())}${path.sep}`;
this._stripRegExp = new RegExp(
`(${escapeRegExp(this.writePath("temp", "currently-playing", path.sep))})|` +
`(${escapeRegExp(this.writePath(path.sep))})|` +
`(\\.\\.\\.[${escapeRegExp(chars)}]*?currently-playing\\${path.sep})`,
// The script printer formats paths using / on both windows and linux.
// But the save path is printed with \ on windows and / on linux.
`(${escapeRegExp(tempPath.replace(/\\/g, "/"))})|` +
`(${escapeRegExp(writePath)})|` +
`(\\.\\.\\.[${escapeRegExp(chars)}/]*?currently-playing/)`,
"g"
);
}
@@ -832,7 +836,7 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath("..", "bin", "x64", "factorio"),
this.binaryPath(),
[
"--config", this.writePath("config.ini"),
"--create", this.writePath("saves", name),
@@ -871,7 +875,7 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath("..", "bin", "x64", "factorio"),
this.binaryPath(),
[
"--config", this.writePath("config.ini"),
"--start-server", this.writePath("saves", save),
@@ -906,7 +910,7 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath("..", "bin", "x64", "factorio"),
this.binaryPath(),
[
"--config", this.writePath("config.ini"),
"--start-server-load-scenario", scenario,
@@ -1102,6 +1106,20 @@ class FactorioServer extends events.EventEmitter {
return path.join(this._dataDir, ...parts);
}
/**
* Get Factorio binary path
*
* Get the path to the factorio binary depending on the platform (MacOS support)
*
* @returns {string} Path to factorio binary
*/
binaryPath() {
if (process.platform === "darwin") {
return this.dataPath("..", "MacOS", "factorio");
}
return this.dataPath("..", "bin", "x64", "factorio");
}
/**
* Return path in write directory
*
+16 -5
View File
@@ -3,9 +3,9 @@
const events = require("events");
const WebSocket = require("isomorphic-ws");
const libSchema = require("@clusterio/lib/schema");
const libErrors = require("@clusterio/lib/errors");
const { logger } = require("@clusterio/lib/logging");
const libSchema = require("../schema");
const libErrors = require("../errors");
const { logger } = require("../logging");
/**
@@ -75,6 +75,9 @@ class WebSocketBaseConnector extends events.EventEmitter {
* Start sending heartbeats
*/
startHeartbeat() {
if (this._heartbeatId) {
throw new Error("heartbeat is already running");
}
this._lastHeartbeat = Date.now();
this._heartbeatId = setInterval(() => {
this._doHeartbeat();
@@ -175,7 +178,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
setTimeout(timeout) {
this._timeout = timeout;
if (this._reconnectId && this.timeout < this._reconnectDelay / 2) {
if (this._reconnectId && this._timeout < this._reconnectDelay / 2) {
clearTimeout(this._reconnectId);
this.reconnect();
}
@@ -211,6 +214,13 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
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.emit("close");
return;
}
this.stopHeartbeat();
@@ -270,6 +280,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
this._lastReceivedSeq = null;
this._sessionToken = null;
this._sendBuffer.length = 0;
this._state = "new";
this._connected = false;
this.emit("close");
return;
@@ -391,7 +402,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
this._startedReconnect = null;
this._connected = true;
this.emit("connect");
this.emit("connect", data);
} else if (type === "continue") {
logger.verbose("SOCKET | resuming existing session");
+3 -3
View File
@@ -1,9 +1,9 @@
// Implementation of Link class
"use strict";
const libSchema = require("@clusterio/lib/schema");
const libErrors = require("@clusterio/lib/errors");
const { logger } = require("@clusterio/lib/logging");
const libSchema = require("../schema");
const libErrors = require("../errors");
const { logger } = require("../logging");
const messages = require("./messages");
// Some definitions for the terminology used here:
+3 -3
View File
@@ -1,8 +1,8 @@
// Message definitions for links
"use strict";
const libSchema = require("@clusterio/lib/schema");
const libErrors = require("@clusterio/lib/errors");
const { logger } = require("@clusterio/lib/logging");
const libSchema = require("../schema");
const libErrors = require("../errors");
const { logger } = require("../logging");
class MissingLinkHandlerError extends Error {
constructor(type, source, target) {
+2 -2
View File
@@ -4,8 +4,8 @@ const Transport = require("winston-transport");
const { LEVEL, MESSAGE } = require("triple-beam");
const chalk = require("chalk");
const libLink = require("@clusterio/lib/link");
const libErrors = require("@clusterio/lib/errors");
const libLink = require("./link");
const libErrors = require("./errors");
/**
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@clusterio/lib",
"description": "Shared library for Clusterio",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"repository": "https://github.com/clusterio/factorioClusterio",
"license": "MIT",
"keywords": [
@@ -19,7 +19,6 @@
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"klaw": "^3.0.0",
"node-forge": "^0.10.0",
"rcon-client": "^4.2.0",
"semver": "^7.1.1",
"set-blocking": "^2.0.0",
+166 -1
View File
@@ -329,6 +329,43 @@ class BaseMasterPlugin {
*/
onSlaveConnectionEvent(connection, event) { }
/**
* Called when an avent on a control connection happens
*
* The event param may be one of connect, drop, resume and close and has
* the following meaning:
*
* ##### connect
*
* Invoked when a new connection to the control has been established.
*
* ##### drop
*
* Invoked when a connection loss is detected between the master and a
* control. Plugins should respond to this event by throtteling
* messages it is sending to the given control connection to an absolute
* minimum.
*
* Messages sent over a dropped control connection will get queued up in
* memory on the master and sent all in one go when the connection is
* re-established again.
*
* ##### resume
*
* Invoked when a connection that had previously been dropped is
* re-established.
*
* ##### close
*
* Invoked when the connection to the control has been closed.
*
* @param {module:master/master~ControlConnection} connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume, and close.
*/
onControlConnectionEvent(connection, event) { }
/**
* Called when a slave is preparing to disconnect from the master
*
@@ -423,6 +460,133 @@ class BaseControlPlugin {
async addCommands(rootCommand) { }
}
/**
* Plugin supplied login form
* @typedef {Object} module:lib/plugin~LoginForm
* @property {string} name -
* Internal name of the login form, this should start with the
* plugin name followed by a dot.
* @property {string} title -
* Name displayed above this form in the login window.
* @property {React.Component} Component -
* React component that's rendered for this login form. This is
* supplied the setToken function via its props which should be
* called when an authentication token is aquired via this
* form.
*/
/**
* Plugin supplied pages
* @typedef {Object} module:Lib/plugin~Page
* @property {string} path - URL path to this page.
* @property {string=} sidebarPath -
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
* @property {string=} sidebarName -
* If present creates an entry in the sidebar for this page with the
* given text.
* @property {ReactNode} content -
* A react node which is rendered when this page is navigated to.
* Should render a PageLayout.
*/
/**
* Base class for web interface plugins
*
* @static
*/
class BaseWebPlugin {
constructor(container, packageData, info, logger) {
/**
* Webpack container for this plugin
*/
this.container = container;
/**
* Contents of the plugin's package.json file
* @type {Object}
*/
this.package = packageData;
/**
* The plugin's own info module
*/
this.info = info;
/**
* Control link to the master server, not available until the
* connect event in onMasterConnectionEvent is signaled.
* @type {?module:lib/link.Link}
*/
this.control = null;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
/**
* List of login forms provided by this plugin
*
* @type {Array<module:lib/plugin~LoginForm>}
*/
this.loginForms = [];
/**
* List of pages provided by this plugin
*
* @type {Array<module:lib/plugin~Page>}
*/
this.pages = [];
}
/**
* Called immediately after the class is instantiated.
*/
async init() { }
/**
* Called when an event on the master connection happens
*
* The event param may be one of connect, drop, resume and close and has
* the following meaning:
*
* ##### connect
*
* Invoked when a new connection to the master has been established.
*
* ##### drop
*
* Invoked when a connection loss is detected between the control link
* and the master. Plugins should respond to this event by throtteling
* messages it is sending to the master to an absolute minimum.
*
* Messages sent over a dropped master connection will get queued up in
* memory in the browser and sent all in one go when the connection is
* re-established again.
*
* ##### resume
*
* Invoked when the connection that had previously dropped is
* re-established.
*
* ##### close
*
* Invoked when the connection to the master has been closed. This
* typically means the master server has shut down. Plugins should not
* send any messages that goes to or via the master server after the
* connection has been closed and before a new one is established.
*
* @param {string} event - one of connect, drop, resume and close
*/
onMasterConnectionEvent(event) { }
}
/**
* Attach plugin messages
*
@@ -446,7 +610,7 @@ function attachPluginMessages(link, plugin) {
messageFormat.attach(link);
} else {
messageFormat.attach(link, async (message, format) => await plugin[handler](message, format));
messageFormat.attach(link, async (message, format) => await plugin[handler](message, format, link));
}
} catch (err) {
@@ -489,6 +653,7 @@ module.exports = {
BaseInstancePlugin,
BaseMasterPlugin,
BaseControlPlugin,
BaseWebPlugin,
attachPluginMessages,
invokeHook,
+1 -1
View File
@@ -5,7 +5,7 @@
*/
"use strict";
const libErrors = require("@clusterio/lib/errors");
const libErrors = require("./errors");
const path = require("path");
+7
View File
@@ -102,6 +102,13 @@ const serverHandshake = ajv.compile({
properties: {
"session_token": { type: "string" },
"heartbeat_interval": { type: "number" },
"account": {
additionalProperties: false,
required: ["name"],
properties: {
"name": { type: "string" },
},
},
},
},
},
+3 -3
View File
@@ -6,9 +6,9 @@
const fs = require("fs-extra");
const path = require("path");
const libConfig = require("@clusterio/lib/config");
const { logger } = require("@clusterio/lib/logging");
const libHelpers = require("@clusterio/lib/helpers");
const libConfig = require("./config");
const { logger } = require("./logging");
const libHelpers = require("./helpers");
function print(...content) {
+6 -2
View File
@@ -9,6 +9,9 @@
"use strict";
const jwt = require("jsonwebtoken");
const libErrors = require("./errors");
/**
* Represents a permission that can be granted
* @static
@@ -254,7 +257,8 @@ class User {
* "Permission denied" error is thrown.
*
* @param {string} permission - The permission to check for.
* @throws {Error} if the user does noh have the given permission.
* @throws {Error} If the given permission does not exist.
* @throws {module:lib/errors.PermissionError} if the user does noh have the given permission.
*/
checkPermission(permission) {
if (!permissions.has(permission)) {
@@ -267,7 +271,7 @@ class User {
}
}
throw new Error("Permission denied");
throw new libErrors.PermissionError("Permission denied");
}
notifyJoin(instance_id) {
+31 -52
View File
@@ -1,68 +1,55 @@
Clusterio Master Server
=======================
# Clusterio Master Server
Communication hub for Clusterio clusters. The master server forwards
data between Clusterio slaves connected to it and allows the cluster to
be remotely managed through WebSocket connections to it either by using
the included web interface or the Clusterio ctl command line utility.
Communication hub for Clusterio clusters.
The master server forwards data between Clusterio slaves connected to it and allows the cluster to be remotely managed through WebSocket connections to it either by using the included web interface or the Clusterio ctl command line utility.
Usage
-----
# Usage
npx clusteriomaster <command>
Common options:
* `--plugin-list <file>` JSON file to use for storing the list of
plugins that are available to the master server. Defaults to
`plugin-list.json` and will be created if it does not exist. See the
`plugin` command for managing this list.
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the master server.
Defaults to `plugin-list.json` and will be created if it does not exist.
See the `plugin` command for managing this list.
* `--config <file>` JSON file to use for storing configuration for the
master server. Defaults to `config-master.json` and will be created
if it does not exist. See the `config` command for inspecting and
modifying the configuration.
* `--config <file>` JSON file to use for storing configuration for the master server.
Defaults to `config-master.json` and will be created if it does not exist.
See the `config` command for inspecting and modifying the configuration.
### `plugin <command>`
Configure plugins available to be loaded by the master server. The
available plugins will be loaded unless they have been disabled in the
configuration, see the config command for disabling plugins.
Configure plugins available to be loaded by the master server.
The available plugins will be loaded unless they have been disabled in the configuration, see the config command for disabling plugins.
#### `plugin add <path>`
Add plugin either by require path or relative/absolute path to the
plugin directory. A relative path must start with ./ or ../ (or .\ and
..\ on Windows) otherwise it will be assumed to be a require path for an
installed package in node_modules.
Add plugin either by require path or relative/absolute path to the plugin directory.
A relative path must start with ./ or ../ (or .\ and ..\ on Windows) otherwise it will be assumed to be a require path for an installed package in node_modules.
For example, installing the Subspace Storage plugin:
npm install @clusterio/plugin-subspace_storage
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin remove <name>`
Remove a plugin by its name. This should be done before uninstalling
the plugin, otherwise there will be an error when Clusterio tries to
load the info from the plugin. Removing and unistalling a plugin is
usually not neccessary as the functions provided by the plugin can be
disabled in the config.
Remove a plugin by its name.
This should be done before uninstalling the plugin, otherwise there will be an error when Clusterio tries to load the info from the plugin.
Removing and unistalling a plugin is usually not neccessary as the functions provided by the plugin can be disabled in the config.
For example, uninstalling the Subspace Storage plugin:
npx clusteriomaster plugin remove subspace_storage
npm uninstall @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin list`
@@ -72,20 +59,17 @@ Lists the plugins set up to be available by name followed by path.
### `config`
Manage the master server configuration offline. This should only be
used when the master server is stopped, otherwise the config read might
be out of date and config changes will be overwritten when the master
server shuts down.
Manage the master server configuration offline.
This should only be used when the master server is stopped, otherwise the config read might be out of date and config changes will be overwritten when the master server shuts down.
#### `config set <config-entry> [value]`
Set a config entry to the given value. If value is not provided the
entry is set to null. If the config entry is of type object the value
must be a valid JSON serialization of an object.
Set a config entry to the given value.
If value is not provided the entry is set to null.
If the config entry is of type object the value must be a valid JSON serialization of an object.
See docs/configuration.md in the main repositiory for the available
configuration entries.
See docs/configuration.md in the main repositiory for the available configuration entries.
#### `config show <config-entry>`
@@ -105,22 +89,19 @@ Commands for setting up initial admin acount and access to the cluster.
#### `bootstrap create-admin <name>`
Creates a new user account with the given name if it does not already
exist in the cluster and grant it full cluster admin permission. You
should be using your own Factorio multiplayer username here.
Creates a new user account with the given name if it does not already exist in the cluster and grant it full cluster admin permission.
You should be using your own Factorio multiplayer username here.
#### `bootstrap generate-user-token <name>`
Generate an access token for logging in and possibly managing the
cluster from the given user account. This can be used both in the web
interface and as the `master_token` to connect with clusterioctl.
Generate an access token for logging in and possibly managing the cluster from the given user account.
This can be used both in the web interface and as the `master_token` to connect with clusterioctl.
#### `bootstrap create-ctl-config <name>`
Creates a clusterioctl config for the given user with url and token set
up for connecting to the cluster.
Creates a clusterioctl config for the given user with url and token set up for connecting to the cluster.
### `run`
@@ -130,6 +111,4 @@ Runs the master server.
## See Also
[The Clusterio
repository](https://github.com/clusterio/factorioClusterio)
for instructions on how to set up a cluster.
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
+109 -41
View File
@@ -14,10 +14,6 @@
* npx clusteriomaster run
*/
// Attempt updating
// const updater = require("./updater");
// updater.update().then(console.log);
"use strict";
const path = require("path");
const fs = require("fs-extra");
@@ -45,8 +41,10 @@ let loadedPlugins = {};
let devMiddleware;
let clusterLogger;
let db = {
instances: new Map(),
slaves: new Map(),
instances: null,
slaves: null,
roles: null,
users: null,
};
let slaveConnections = new Map();
let controlConnections = [];
@@ -82,18 +80,6 @@ let httpServerCloser;
let httpsServer;
let httpsServerCloser;
app.use(cookieParser());
app.use(bodyParser.json({
limit: "10mb",
}));
app.use(bodyParser.urlencoded({
parameterLimit: 100000,
limit: "10mb",
extended: true,
}));
app.use(fileUpload());
app.use(compression());
// Servers the web interface with the root path set apropriately.
function serveWeb(route) {
@@ -114,10 +100,6 @@ function serveWeb(route) {
};
}
// Set folder to serve static content from (the website)
app.use(express.static(path.join(__dirname, "dist", "web")));
app.use(express.static("static")); // Used for data export files
const slaveMappingGauge = new libPrometheus.Gauge(
"clusterio_master_slave_mapping",
"Mapping of Slave ID to name",
@@ -153,6 +135,11 @@ const instanceMappingGauge = new libPrometheus.Gauge(
}
);
const hitCounter = new libPrometheus.Counter(
"clusterio_master_http_hits_total",
"How many HTTP requests in total have been received"
);
const endpointHitCounter = new libPrometheus.Counter(
"clusterio_master_http_endpoint_hits_total",
"How many requests a particular HTTP endpoint has gotten",
@@ -185,6 +172,28 @@ const wsActiveSlavesGauge = new libPrometheus.Gauge(
"How many slaves are currently connected to the master"
);
app.use((req, res, next) => {
hitCounter.inc();
next();
});
app.use(cookieParser());
app.use(bodyParser.json({
limit: "10mb",
}));
app.use(bodyParser.urlencoded({
parameterLimit: 100000,
limit: "10mb",
extended: true,
}));
app.use(fileUpload());
app.use(compression());
// Set folder to serve static content from (the website)
app.use(express.static(path.join(__dirname, "dist", "web")));
app.use(express.static("static")); // Used for data export files
// Merges samples from sourceResult to destinationResult
function mergeSamples(destinationResult, sourceResult) {
// Merge metrics received by multiple slaves
@@ -278,8 +287,9 @@ app.get("/api/plugins", (req, res) => {
let plugins = [];
for (let pluginInfo of pluginInfos) {
let name = pluginInfo.name;
let enabled = masterPlugins.has(name) && masterConfig.group(name).get("enabled");
plugins.push({ name, enabled });
let loaded = masterPlugins.has(name);
let enabled = loaded && masterConfig.group(pluginInfo.name).get("load_plugin");
plugins.push({ name, version: pluginInfo.version, enabled, loaded });
}
res.send(plugins);
});
@@ -492,6 +502,14 @@ async function shutdown() {
logger.info("Saving configs");
await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
if (devMiddleware) {
await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
}
if (!db.slaves) {
return;
}
await saveMap(masterConfig.get("master.database_directory"), "slaves.json", db.slaves);
await saveInstances(masterConfig.get("master.database_directory"), "instances.json", db.instances);
await saveUsers(masterConfig.get("master.database_directory"), "users.json");
@@ -500,10 +518,6 @@ async function shutdown() {
stopAcceptingNewSessions = true;
if (devMiddleware) {
await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
}
let disconnectTasks = [];
for (let controlConnection of controlConnections) {
controlConnection.connector.setTimeout(masterConfig.get("master.connector_shutdown_timeout"));
@@ -682,6 +696,14 @@ class ControlConnection extends BaseConnection {
debugEvents.off("message", this.ws_dumper);
}
});
for (let event of ["connect", "drop", "resume", "close"]) {
this.connector.on(event, () => {
for (let masterPlugin of masterPlugins.values()) {
masterPlugin.onControlConnectionEvent(this, event);
}
});
}
}
async getMasterConfigRequestHandler() {
@@ -1357,8 +1379,10 @@ class WebSocketServerConnector extends libLink.WebSocketBaseConnector {
*
* @param {string} sessionToken -
* the session token to send to the client.
* @param {Object=} additionalData -
* extra properties to send along the ready message.
*/
ready(sessionToken) {
ready(sessionToken, additionalData) {
this._heartbeatInterval = masterConfig.get("master.heartbeat_interval");
this._socket.send(JSON.stringify({
seq: null,
@@ -1366,6 +1390,7 @@ class WebSocketServerConnector extends libLink.WebSocketBaseConnector {
data: {
session_token: sessionToken,
heartbeat_interval: this._heartbeatInterval,
...additionalData,
},
}));
@@ -1385,7 +1410,24 @@ class WebSocketServerConnector extends libLink.WebSocketBaseConnector {
* @param {number} lastSeq - The last message the client received.
*/
continue(socket, lastSeq) {
this._socket.terminate();
// It's possible the previous connection hasn't closed yet due to a
// 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)) {
this._socket.close(4003, "Session Hijacked");
// Correctly waiting for the connector to close and then
// handling all the edge cases like multiple continues happening
// in parallel or connections going stale is far too difficult.
// Kill the connection here and let the client reconnect later
// when the connector is not held up by an active connection.
socket.close(1013, "Session Busy");
socket.terminate();
return;
}
this._socket = socket;
if (this._timeoutId) {
@@ -1619,6 +1661,7 @@ async function handleHandshake(message, socket, req, attachHandler) {
let connector = new WebSocketServerConnector(socket, sessionId);
activeConnectors.set(sessionId, connector);
let additionalReadyData = {};
if (type === "register_slave") {
let connection = slaveConnections.get(data.id);
if (connection) {
@@ -1633,9 +1676,12 @@ async function handleHandshake(message, socket, req, attachHandler) {
} else if (type === "register_control") {
logger.verbose(`SOCKET | registered control from ${req.socket.remoteAddress}`);
controlConnections.push(new ControlConnection(data, connector, user));
additionalReadyData["account"] = {
name: user.name,
};
}
connector.ready(sessionToken);
connector.ready(sessionToken, additionalReadyData);
}
wss.on("connection", (socket, req) => {
@@ -1697,9 +1743,8 @@ ${err.stack}`
});
async function loadPlugins() {
let plugins = new Map();
for (let pluginInfo of pluginInfos) {
if (!masterConfig.group(pluginInfo.name).get("enabled")) {
if (!masterConfig.group(pluginInfo.name).get("load_plugin")) {
continue;
}
@@ -1713,10 +1758,19 @@ async function loadPlugins() {
}
let masterPlugin = new MasterPluginClass(
pluginInfo, { app, config: masterConfig, db, slaveConnections }, { endpointHitCounter }, logger
pluginInfo,
{
app,
config: masterConfig,
db,
getMasterUrl,
slaveConnections,
},
{ endpointHitCounter },
logger
);
await masterPlugin.init();
plugins.set(pluginInfo.name, masterPlugin);
masterPlugins.set(pluginInfo.name, masterPlugin);
} catch (err) {
throw new libErrors.PluginError(pluginInfo.name, err);
@@ -1724,13 +1778,12 @@ async function loadPlugins() {
logger.info(`Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
}
return plugins;
}
// handle plugins on the master
async function pluginManagement() {
let startPluginLoad = Date.now();
masterPlugins = await loadPlugins();
await loadPlugins();
logger.info(`All plugins loaded in ${Date.now() - startPluginLoad}ms`);
}
@@ -1867,6 +1920,7 @@ async function initialize() {
})
.command("run", "Run master server", yargs => {
yargs.option("dev", { hidden: true, type: "boolean", nargs: 0 });
yargs.option("dev-plugin", { hidden: true, type: "array" });
})
.demandCommand(1, "You need to specify a command to run")
.strict()
@@ -1959,15 +2013,29 @@ async function initialize() {
async function startServer(args) {
// Start webpack development server if enabled
if (args.dev) {
if (args.dev || args.devPlugin) {
logger.warn("Webpack development mode enabled");
/* eslint-disable global-require */
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackConfig = require("./webpack.config");
const webpackConfigs = [];
if (args.dev) {
webpackConfigs.push(require("./webpack.config")({}));
}
if (args.devPlugin) {
for (let name of args.devPlugin) {
let info = pluginInfos.find(i => i.name === name);
if (!info) {
throw new libErrors.StartupError(`No plugin named ${name}`);
}
let config = require(path.posix.join(info.requirePath, "webpack.config"))({});
config.output.publicPath = `/plugins/${name}/`;
webpackConfigs.push(config);
}
}
/* eslint-enable global-require */
const compiler = webpack(webpackConfig({}));
const compiler = webpack(webpackConfigs);
devMiddleware = webpackDevMiddleware(compiler, {});
app.use(devMiddleware);
}
@@ -2039,7 +2107,7 @@ async function startServer(args) {
let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
let webPath = path.join(path.dirname(pluginPackagePath), "dist", "web");
app.use(`/plugin/${pluginInfo.name}`, express.static(webPath));
app.use(`/plugins/${pluginInfo.name}`, express.static(webPath));
}
// Load plugins
+4 -10
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio master server",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"main": "master.js",
"scripts": {
"build": "webpack-cli --env.production",
@@ -19,7 +19,7 @@
"node": ">=10.16 <11 || >= 12"
},
"dependencies": {
"@clusterio/lib": "^2.0.0-alpha.4",
"@clusterio/lib": "^2.0.0-alpha.5",
"body-parser": "^1.18.2",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3",
@@ -29,25 +29,19 @@
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"node-factorio-api": "^0.3.8",
"node-forge": "^0.10.0",
"set-blocking": "^2.0.0",
"winston": "^3.3.3",
"ws": "^7.2.3",
"yargs": "^14.2.0"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.4",
"babel-loader": "^8.1.0",
"css-loader": "^4.3.0",
"@clusterio/web_ui": "^2.0.0-alpha.5",
"fast-deep-equal": "^3.1.3",
"file-loader": "^6.2.0",
"null-loader": "^4.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"style-loader": "^1.2.1",
"webpack-cli": "^3.3.12",
"webpack-dev-middleware": "^3.7.2",
"webpack-dev-middleware": "^4.1.0",
"webpack-merge": "^5.2.0"
},
"publishConfig": {
+2 -1
View File
@@ -11,5 +11,6 @@ module.exports = [
"/users/:id/view",
"/roles",
"/roles/:id/view",
"/settings",
"/plugins",
"/plugins/:name/view",
];
+2 -1
View File
@@ -3,9 +3,10 @@
<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 = "__WEB_ROOT__";
window.webRoot = new URL("__WEB_ROOT__", document.location).pathname;
</script>
</head>
<body>
+3 -1
View File
@@ -1 +1,3 @@
import("@clusterio/web_ui/src/bootstrap");
import("@clusterio/web_ui").then(webUi => {
return webUi.bootstrap();
}).catch(console.error);
+9 -2
View File
@@ -12,6 +12,7 @@ module.exports = (env = {}) => merge(common(env), {
contentBase: "./dist/web",
},
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
@@ -19,9 +20,15 @@ module.exports = (env = {}) => merge(common(env), {
new webpack.container.ModuleFederationPlugin({
name: "master",
shared: {
"@clusterio/lib": { singleton: true },
"@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/web_ui": { singleton: true },
"ajv": {},
"antd": { singleton: true },
"react": { singleton: true },
"react-dom": { singleton: true },
+27 -45
View File
@@ -1,70 +1,56 @@
Clusterio Slave
===============
# Clusterio Slave
Node hosting Factorio servers in a Clusterio cluster. Clusterio slaves
connect to the master server and waits for commands from the master
server to start up and stop instances. A cluster can have any number of
slaves in it located on different computers, and each slave can host any
number of instances each of which is a Factorio server that talks with
the rest of the cluster.
Node hosting Factorio servers in a Clusterio cluster.
Clusterio slaves connect to the master server and waits for commands from the master server to start up and stop instances.
A cluster can have any number of slaves in it located on different computers, and each slave can host any number of instances each of which is a Factorio server that talks with the rest of the cluster.
Usage
-----
## Usage
npx clusterioslave <command>
Common options:
* `--plugin-list <file>` JSON file to use for storing the list of
plugins that are available to the slave. Defaults to
`plugin-list.json` and will be created if it does not exist. See the
`plugin` command for managing this list.
* `--plugin-list <file>` JSON file to use for storing the list of plugins that are available to the slave.
Defaults to `plugin-list.json` and will be created if it does not exist.
See the `plugin` command for managing this list.
* `--config <file>` JSON file to use for storing configuration for the
slave. Defaults to `config-slave.json` and will be created if it
does not exist. See the `config` command for inspecting and
modifying the configuration.
* `--config <file>` JSON file to use for storing configuration for the slave.
Defaults to `config-slave.json` and will be created if it does not exist.
See the `config` command for inspecting and modifying the configuration.
### `plugin <command>`
Configure plugins available to be loaded by the slave. The available
plugins will be loaded unless they have been disabled in the
configuration, see the config command for disabling plugins.
Configure plugins available to be loaded by the slave.
The available plugins will be loaded unless they have been disabled in the configuration, see the config command for disabling plugins.
#### `plugin add <path>`
Add plugin either by require path or relative/absolute path to the
plugin directory. A relative path must start with ./ or ../ (or .\ and
..\ on Windows) otherwise it will be assumed to be a require path for an
installed package in node_modules.
Add plugin either by require path or relative/absolute path to the plugin directory.
A relative path must start with ./ or ../ (or .\ and ..\ on Windows) otherwise it will be assumed to be a require path for an installed package in node_modules.
For example, installing the Subspace Storage plugin:
npm install @clusterio/plugin-subspace_storage
npx clusterioslave plugin add @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin remove <name>`
Remove a plugin by its name. This should be done before uninstalling
the plugin, otherwise there will be an error when Clusterio tries to
load the info from the plugin. Removing and unistalling a plugin is
usually not neccessary as the functions provided by the plugin can be
disabled in the config.
Remove a plugin by its name.
This should be done before uninstalling the plugin, otherwise there will be an error when Clusterio tries to load the info from the plugin.
Removing and unistalling a plugin is usually not neccessary as the functions provided by the plugin can be disabled in the config.
For example, uninstalling the Subspace Storage plugin:
npx clusterioslave plugin remove subspace_storage
npm uninstall @clusterio/plugin-subspace_storage
Since the `plugin-list.json` is shared between master, slave and ctl you
usually only need to do this once per machine.
Since the `plugin-list.json` is shared between master, slave and ctl you usually only need to do this once per machine.
#### `plugin list`
@@ -74,19 +60,17 @@ Lists the plugins set up to be available by name followed by path.
### `config`
Manage the slave configuration offline. This should only be used when
the slave is stopped, otherwise the config read might be out of date and
config changes will be overwritten when the slave shuts down.
Manage the slave configuration offline.
This should only be used when the slave is stopped, otherwise the config read might be out of date and config changes will be overwritten when the slave shuts down.
#### `config set <config-entry> [value]`
Set a config entry to the given value. If value is not provided the
entry is set to null. If the config entry is of type object the value
must be a valid JSON serialization of an object.
Set a config entry to the given value.
If value is not provided the entry is set to null.
If the config entry is of type object the value must be a valid JSON serialization of an object.
See docs/configuration.md in the main repositiory for the available
configuration.
See docs/configuration.md in the main repositiory for the available configuration.
#### `config show <config-entry>`
@@ -106,6 +90,4 @@ Runs the slave.
## See Also
[The Clusterio
repository](https://github.com/clusterio/factorioClusterio)
for instructions on how to set up a cluster.
[The Clusterio repository](https://github.com/clusterio/factorioClusterio) for instructions on how to set up a cluster.
+2 -2
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio slave server",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"main": "slave.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@@ -18,7 +18,7 @@
"node": ">=10.16 <11 || >= 12"
},
"dependencies": {
"@clusterio/lib": "^2.0.0-alpha.4",
"@clusterio/lib": "^2.0.0-alpha.5",
"fs-extra": "^8.1.0",
"phin": "^3.4.1",
"pidusage": "^2.0.18",
+11 -5
View File
@@ -274,7 +274,7 @@ class Instance extends libLink.Link {
if (
!pluginInfo.instanceEntrypoint
|| !this._slave.serverPlugins.has(pluginInfo.name)
|| !this.config.group(pluginInfo.name).get("enabled")
|| !this.config.group(pluginInfo.name).get("load_plugin")
) {
continue;
}
@@ -736,7 +736,7 @@ class Instance extends libLink.Link {
let response = await phin({
url, method: "PUT",
data: content,
core: { rejectUnauthorized: false },
core: { ca: this._slave.tlsCa },
headers: {
"Content-Type": "application/zip",
"x-access-token": this._slave.config.get("slave.master_token"),
@@ -976,7 +976,7 @@ function checkFilename(name) {
class Slave extends libLink.Link {
// I don't like God classes, but the alternative of putting all this state
// into global variables is not much better.
constructor(connector, slaveConfig, pluginInfos) {
constructor(connector, slaveConfig, tlsCa, pluginInfos) {
super("slave", "master", connector);
libLink.attachAllMessages(this);
@@ -987,6 +987,12 @@ class Slave extends libLink.Link {
this.config = slaveConfig;
/**
* Certificate authority used to validate TLS connections to the master.
* @type {?string}
*/
this.tlsCa = tlsCa;
this.instanceConnections = new Map();
this.discoveredInstanceInfos = new Map();
this.instanceInfos = new Map();
@@ -1459,7 +1465,7 @@ async function symlinkMods(instance, sharedMods) {
let instanceModsEntries = new Set(await fs.readdir(instance.path("mods")));
for (let entry of await fs.readdir(sharedMods, { withFileTypes: true })) {
if (entry.isFile()) {
if ([".zip", ".dat"].includes(path.extname(entry.name))) {
if ([".zip", ".dat", ".json"].includes(path.extname(entry.name))) {
if (!instanceModsEntries.has(entry.name)) {
instance.logger.verbose(`linking ${entry.name} from ${sharedMods}`);
let target = path.join(sharedMods, entry.name);
@@ -1618,7 +1624,7 @@ async function startSlave() {
}
let slaveConnector = new SlaveConnector(slaveConfig, tlsCa, pluginInfos);
let slave = new Slave(slaveConnector, slaveConfig, pluginInfos);
let slave = new Slave(slaveConnector, slaveConfig, tlsCa, pluginInfos);
// Handle interrupts
let secondSigint = false;
+10 -2
View File
@@ -3,7 +3,8 @@
"description": "Clusterio web interface implementation",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"browser": "src/index.jsx",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -15,17 +16,24 @@
},
"dependencies": {
"@ant-design/icons": "^4.2.2",
"@clusterio/lib": "^2.0.0-alpha.4",
"@babel/core": "^7.13.8",
"@babel/preset-react": "^7.12.13",
"@clusterio/lib": "^2.0.0-alpha.5",
"antd": "^4.6.5",
"babel-loader": "^8.2.2",
"buffer": "^6.0.2",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^5.1.0",
"events": "^3.2.0",
"file-loader": "^6.2.0",
"null-loader": "^4.0.1",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"style-loader": "^2.0.0",
"util": "^0.12.3"
},
"publishConfig": {
-6
View File
@@ -1,6 +0,0 @@
// Calculate basedir of application based on current location and
// the provided web_root from master server.
const location = new URL(window.webRoot, document.location);
const basename = location.pathname !== "/" ? location.pathname : "";
export default basename;
+35 -16
View File
@@ -4,6 +4,7 @@ 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 App from "./components/App";
@@ -25,7 +26,7 @@ async function loadScript(url) {
return result;
}
async function loadPluginInfos() {
async function loadPlugins() {
let response = await fetch(`${webRoot}api/plugins`);
let pluginList;
if (response.ok) {
@@ -36,30 +37,49 @@ async function loadPluginInfos() {
pluginList = [];
}
let pluginInfos = [];
let plugins = new Map();
await __webpack_init_sharing__("default");
for (let plugin of pluginList) {
for (let meta of pluginList) {
try {
await loadScript(`${webRoot}plugin/${plugin.name}/remoteEntry.js`);
let container = window[`plugin_${plugin.name}`];
await loadScript(`${webRoot}plugins/${meta.name}/remoteEntry.js`);
let container = window[`plugin_${meta.name}`];
if (!container) {
throw new Error(`Plugin did not expose its container via plugin_${meta.name}`);
}
await container.init(__webpack_share_scopes__.default);
let info = (await container.get("./info"))();
info.container = container;
pluginInfos.push(info);
let pluginInfo = (await container.get("./info"))();
let pluginPackage = (await container.get("./package.json"))();
let WebPluginClass = libPlugin.BaseWebPlugin;
if (meta.enabled && pluginInfo.webEntrypoint) {
let webModule = (await container.get(pluginInfo.webEntrypoint))();
if (!webModule.WebPlugin) {
throw new Error("Plugin webEntrypoint does not export WebPlugin class");
}
WebPluginClass = webModule.WebPlugin;
}
let plugin = new WebPluginClass(container, pluginPackage, pluginInfo, logger);
await plugin.init();
plugins.set(pluginInfo.name, plugin);
} catch (err) {
logger.error(`Failed to load plugin info for ${plugin.name}`);
logger.error(`Failed to load plugin ${meta.name}`);
if (err.stack) {
logger.error(err.stack);
}
}
}
return pluginInfos;
return plugins;
}
async function load() {
export default async function bootstrap() {
logger.add(new ConsoleTransport({
level: "verbose",
format: new WebConsoleFormat(),
}));
let pluginInfos = await loadPluginInfos();
let plugins = await loadPlugins();
let pluginInfos = [...plugins.values()].map(p => p.info);
libConfig.registerPluginConfigGroups(pluginInfos);
libConfig.finalizeConfigs();
@@ -67,9 +87,8 @@ async function load() {
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
let controlConnector = new ControlConnector(wsUrl, 10);
let control = new Control(controlConnector, []);
controlConnector.setTimeout(15);
let control = new Control(controlConnector, plugins);
ReactDOM.render(<App control={control}/>, document.getElementById("root"));
ReactDOM.render(<App control={control} plugins={plugins}/>, document.getElementById("root"));
}
load().catch((err) => logger.fatal(err));
+38 -17
View File
@@ -4,13 +4,25 @@ import { BrowserRouter } from "react-router-dom";
import { logger } from "@clusterio/lib/logging";
import basename from "../basename";
import ErrorBoundary from "./ErrorBoundary";
import SiteLayout from "./SiteLayout";
import ControlContext from "./ControlContext";
import PluginsContext from "./PluginsContext";
import LoginForm from "./LoginForm";
import { Card, Spin } from "antd";
import { Card, Spin, Typography } from "antd";
const { Paragraph } = Typography;
function ErrorCard(props) {
return <div className="login-container">
<Card>
<h1>An unexpected error occured</h1>
<Paragraph code className="error-traceback">{props.error.stack}</Paragraph>
</Card>
</div>;
}
export default function App(props) {
let [connected, setConnected] = useState(false);
@@ -19,6 +31,7 @@ export default function App(props) {
function clearToken() {
setToken(null);
connector.token = null;
localStorage.removeItem("master_token");
}
@@ -42,17 +55,24 @@ export default function App(props) {
connector.on("close", onClose);
connector.on("error", onError);
if (token && !connected) {
connector.token = token;
connector.connect();
}
return () => {
connector.off("error", onError);
connector.off("close", onClose);
connector.off("connect", onConnect);
};
});
}, [token, props.control]);
useEffect(() => {
if (token && !connected) {
if (props.control.loggingOut) {
clearToken();
props.control.loggingOut = false;
} else {
connector.token = token;
connector.connect();
}
}
}, [token, connected]);
let page;
if (connected) {
@@ -68,18 +88,19 @@ export default function App(props) {
} else {
page = <div className="login-container">
<Card>
<h1>Clusterio</h1>
<LoginForm setToken={setToken} />
</Card>
<LoginForm setToken={setToken} />
</div>;
}
return (
<ControlContext.Provider value={props.control}>
<BrowserRouter basename={basename}>
{page}
</BrowserRouter>
</ControlContext.Provider>
<ErrorBoundary Component={ErrorCard}>
<ControlContext.Provider value={props.control}>
<PluginsContext.Provider value={props.plugins}>
<BrowserRouter basename={webRoot}>
{page}
</BrowserRouter>
</PluginsContext.Provider>
</ControlContext.Provider>
</ErrorBoundary>
);
}
+43 -9
View File
@@ -1,27 +1,61 @@
import React from "react";
import { Form, Input, Button } from "antd";
import React, { Fragment, useContext } from "react";
import { Button, Card, Divider, Form, Image, Input, List, Space, Spin, Typography } from "antd";
import LockOutlined from "@ant-design/icons/LockOutlined";
export default function LoginForm(props) {
return (
import logo from "../images/logo.png";
import PluginsContext from "./PluginsContext";
const { Paragraph, Text } = Typography;
function TokenAuth(props) {
return <>
<Paragraph>If you have generated an authentication token from the master server, use that here.</Paragraph>
<Form
name="login"
onFinish={(values) => { props.setToken(values.token); }}
layout="inline"
>
<Form.Item
name="token"
rules={[
{ required: true, message: "Please input authentication token" },
]}
rules={[{ required: true }]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
style={{ width: "14em" }}
prefix=<LockOutlined className="site-form-item-icon" />
type="password"
placeholder="Authentication Token"
/>
</Form.Item>
<Button type="primary" htmlType="submit">Log in</Button>
</Form>
);
</>;
}
export default function LoginForm(props) {
let plugins = useContext(PluginsContext);
let loginForms = [];
for (let plugin of plugins.values()) {
loginForms.push(...plugin.loginForms);
}
loginForms.push({
name: "core.token",
title: "Token",
Component: TokenAuth,
});
return <Card style={{ maxWidth: "30em" }}>
<Space style={{ width: "100%" }} direction="vertical" align="center">
<h1>Clusterio</h1>
<Image width={128} height={123} src={logo} />
Log in to the cluster using one of the methods below.
</Space>
{loginForms.map(form => <Fragment key={form.name}>
<Divider orientation="center">{form.title}</Divider>
<form.Component setToken={props.setToken} />
</Fragment>)}
</Card>;
}
@@ -0,0 +1,96 @@
import React, { useEffect, useContext, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { Alert, Descriptions, Spin } from "antd";
import PluginsContext from "./PluginsContext";
import PageLayout from "./PageLayout";
import { notifyErrorHandler } from "../util/notify";
export default function PluginViewPage() {
let params = useParams();
let plugins = useContext(PluginsContext);
let [pluginList, setPluginList] = useState(null);
let pluginName = params.name;
useEffect(() => {
(async () => {
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
setPluginList(await response.json());
} else {
notify("Failed to load plugin list");
}
})();
}, []);
let plugin = plugins.get(pluginName);
let pluginTitle = plugin ? plugin.info.title : pluginName;
let nav = [{ name: "Plugins", path: "/plugins" }, { name: pluginTitle }];
if (!pluginList) {
return <PageLayout nav={nav}>
<Descriptions borderd size="small" title={pluginTitle} />
<Spin size="large" />
</PageLayout>;
}
let pluginMeta = pluginList.find(p => p.name === pluginName);
if (!pluginMeta) {
return <PageLayout nav={nav}>
<h2>Plugin not found</h2>
<p>Plugin with name {pluginName} was not found on the master server.</p>
</PageLayout>;
}
if (!plugin) {
return <PageLayout nav={nav}>
<Descriptions bordered size="small" title={pluginTitle}>
<Descriptions.Item label="Version">{pluginMeta.version}</Descriptions.Item>
<Descriptions.Item label="Loaded">{pluginMeta.loaded ? "Yes" : "No"}</Descriptions.Item>
</Descriptions>
<Alert
style={{
marginTop: "1em",
}}
message="Error loading web module"
description={
"The web interface was unable to load the webpack module for this plugin. This is "+
"usually due to an incorrect or missing webpack build for the plugin. Due to the "+
"module not being loaded, the configs and web controls defined by this plugin will "+
"not be available in the web interface."
}
type="error"
showIcon
/>
</PageLayout>;
}
return <PageLayout nav={nav}>
<Descriptions bordered size="small" title={pluginTitle}>
<Descriptions.Item label="Version">{pluginMeta.version}</Descriptions.Item>
<Descriptions.Item label="Loaded" span={2}>{pluginMeta.loaded ? "Yes" : "No"}</Descriptions.Item>
<Descriptions.Item label="Description" span={3}>{plugin.info.description}</Descriptions.Item>
{plugin.package.homepage ? <Descriptions.Item label="Homepage" span={3}>
<a href={plugin.package.homepage}>{plugin.package.homepage}</a>
</Descriptions.Item> : null}
{plugin.package.author ? <Descriptions.Item label="Author" span={3}>
{plugin.package.author}
</Descriptions.Item> : null}
<Descriptions.Item label="License" span={3}>{plugin.package.license}</Descriptions.Item>
</Descriptions>
{pluginMeta.version !== plugin.package.version ? <Alert
style={{
marginTop: "1em",
}}
message="Version missmatch detected"
description={
`The version of the web interface module for this plugin (${plugin.package.version}) `+
`does not match the version running on the master server (${pluginMeta.version}), `+
"spurious errors may occur. This usually happens when the plugin is updated but the "+
"master server has not been restarted yet, but may also be due to an outdated build."
}
type="warning"
showIcon
/> : null}
</PageLayout>;
}
@@ -0,0 +1,3 @@
import React from "react";
export default React.createContext();
@@ -0,0 +1,81 @@
import React, { useContext, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { Table } from "antd";
import notify from "../util/notify";
import PluginsContext from "./PluginsContext";
import PageLayout from "./PageLayout";
export default function PluginsPage() {
let plugins = useContext(PluginsContext);
let history = useHistory();
let [pluginList, setPluginList] = useState([]);
useEffect(() => {
(async () => {
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
setPluginList(await response.json());
} else {
notify("Failed to load plugin list");
}
})();
}, []);
let tableContents = [];
for (let meta of pluginList) {
if (plugins.has(meta.name)) {
let plugin = plugins.get(meta.name);
tableContents.push({
meta,
info: plugin.info,
package: plugin.package,
});
} else {
tableContents.push({
meta,
});
}
}
return <PageLayout nav={[{ name: "Plugins" }]}>
<h2>Plugins</h2>
<Table
columns={[
{
title: "Name",
key: "name",
render: plugin => (plugin.info ? plugin.info.title : plugin.meta.name),
},
{
title: "Version",
key: "version",
render: plugin => {
if (!plugin.package) {
return "Error loading module";
}
if (plugin.package.version !== plugin.meta.version) {
return "Version missmatched";
}
return plugin.package.version;
},
},
{
title: "Loaded",
dataIndex: ["meta", "loaded"],
render: loaded => (loaded ? "Yes" : null),
responsive: ["sm"],
},
]}
dataSource={tableContents}
rowKey={plugin => plugin.meta.name}
pagination={false}
onRow={plugin => ({
onClick: event => {
history.push(`/plugins/${plugin.meta.name}/view`);
},
})}
/>
</PageLayout>;
}
+38 -39
View File
@@ -1,57 +1,58 @@
import React, { useEffect, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import { Switch, Route, useHistory } from "react-router-dom";
import { Layout, Menu } from "antd";
import { Dropdown, Layout, Menu } from "antd";
import UserOutlined from "@ant-design/icons/UserOutlined";
import webUiPackage from "../../package.json";
import { useAccount } from "../model/account";
import ErrorBoundary from "./ErrorBoundary";
import ErrorPage from "./ErrorPage";
import PluginsContext from "./PluginsContext";
import { pages } from "../pages";
const { Header, Sider } = Layout;
import MasterPage from "./MasterPage";
import SlavesPage from "./SlavesPage";
import InstancesPage from "./InstancesPage";
import InstanceViewPage from "./InstanceViewPage";
import UsersPage from "./UsersPage";
import UserViewPage from "./UserViewPage";
import RolesPage from "./RolesPage";
import RoleViewPage from "./RoleViewPage";
const pages = [
{ path: "/instances/:id/view", sidebarPath: "/instances", content: <InstanceViewPage /> },
{ path: "/users/:name/view", sidebarPath: "/users", content: <UserViewPage /> },
{ path: "/roles/:id/view", sidebarPath: "/roles", content: <RoleViewPage /> },
];
const sidebar = [
{ name: "Master", path: "/master", content: <MasterPage /> },
{ name: "Slaves", path: "/slaves", content: <SlavesPage /> },
{ name: "Instances", path: "/instances", content: <InstancesPage />},
{ name: "Users", path: "/users", content: <UsersPage /> },
{ name: "Roles", path: "/roles", content: <RolesPage /> },
];
export default function SiteLayout(props) {
let history = useHistory();
let [sidebarPath, setSidebarPath] = useState(null);
let [currentSidebarPath, setCurrentSidebarPath] = useState(null);
let account = useAccount();
let plugins = useContext(PluginsContext);
function SetSidebar(setSidebarProps) {
useEffect(() => {
setSidebarPath(setSidebarProps.path);
setCurrentSidebarPath(setSidebarProps.path);
});
return null;
}
let accountMenu = <Menu>
<Menu.ItemGroup title={account.name}/>
<Menu.Divider/>
<Menu.Item danger onClick={() => { account.logOut(); }}>Log out</Menu.Item>
</Menu>;
let combinedPages = [...pages];
for (let plugin of plugins.values()) {
combinedPages.push(...plugin.pages);
}
return <Layout style={{ minHeight: "100vh" }}>
<Header className="header">
<div className="site-logo" />
<span className="site-name" style={{ top: -10 }}>Clusterio</span>
<span style={{ position: "absolute", top: 10, left: 120, color: "#f00" }}>{ webUiPackage.version }</span>
<span className="site-name">Clusterio</span>
<span className="site-version">{ webUiPackage.version }</span>
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={["1"]}>
<Menu.Item key="1">Dashboard</Menu.Item>
</Menu>
<Dropdown
className="account-dropdown-header"
placement="bottomRight"
trigger="click"
overlay={accountMenu}
>
<UserOutlined/>
</Dropdown>
</Header>
<Layout className="site-layout">
<Sider
@@ -64,23 +65,21 @@ export default function SiteLayout(props) {
>
<Menu
mode="inline"
selectedKeys={[sidebarPath]}
selectedKeys={[currentSidebarPath]}
style={{ height: "100%", borderRight: 0 }}
onClick={({ key }) => history.push(key)}
>
{sidebar.map(({name, path}) => <Menu.Item key={path}>{name}</Menu.Item>)}
{combinedPages.map(({sidebarName, path}) => (
sidebarName ? <Menu.Item key={path}>{sidebarName}</Menu.Item> : null)
)}
</Menu>
</Sider>
<Layout className="site-layout-content-container">
<Switch>
{sidebar.map(({path, content}) => <Route exact path={path} key={path}>
<SetSidebar path={path} />
{content}
</Route>)}
{pages.map(page => <Route exact path={page.path} key={page.path}>
<SetSidebar path={page.sidebarPath} />
{combinedPages.map(({path, sidebarPath, content}) => <Route exact path={path} key={path}>
<SetSidebar path={sidebarPath ? sidebarPath : path} />
<ErrorBoundary Component={ErrorPage}>
{page.content}
{content}
</ErrorBoundary>
</Route>)}
<Route>
File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+59
View File
@@ -64,15 +64,24 @@ body {
.header {
padding-left: 250px;
padding-right: 64px;
}
.site-name {
position: absolute;
top: -10px;
left: 112px;
font-size: 18pt;
font-weight: 400;
}
.site-version {
position: absolute;
top: 10px;
left: 120px;
color: #f00;
}
.site-logo {
position: absolute;
left: 36px;
@@ -82,6 +91,50 @@ body {
background-size: 64px;
}
@media not all and (min-width: 576px) {
.header {
padding-left: 88px;
}
.site-logo {
left: 12px;
}
.site-name {
display: none;
}
.site-version {
display: none;
}
}
.account-dropdown-header {
position: absolute;
right: 0;
top: 0;
width: 64px;
height: 64px;
font-size: 18px;
line-height: 72px;
text-align: center;
}
.account-dropdown-header::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
transition: all 0.3s;
content: "";
}
.account-dropdown-header:hover::after {
background: rgba(255, 255, 255, 0.1);
}
.error-traceback code {
display: block;
margin: 0;
@@ -206,3 +259,9 @@ body {
.json-input {
font-family: monospace;
}
.factorio-icon.item-unknown-item {
background: url("./images/unknown-item.png");
height: 32px;
width: 32px;
}
+21
View File
@@ -0,0 +1,21 @@
export { default as bootstrap } from "./bootstrap.jsx";
export { default as notify, notifyErrorHandler } from "./util/notify.jsx";
export * from "./model/instance.jsx";
export * from "./model/slave.jsx";
export * from "./model/locale.jsx";
export * from "./model/item_metadata.jsx";
export { default as ControlContext } from "./components/ControlContext";
export { default as PluginsContext } from "./components/PluginsContext";
export { default as BaseConfigTree } from "./components/BaseConfigTree";
export { default as InstanceConfigTree } from "./components/InstanceConfigTree";
export { default as MasterConfigTree } from "./components/MasterConfigTree";
export { default as AssignInstanceModal } from "./components/AssignInstanceModal";
export { default as InstanceConsole } from "./components/InstanceConsole";
export { default as InstanceRcon } from "./components/InstanceRcon";
export { default as PageLayout } from "./components/PageLayout";
+38
View File
@@ -0,0 +1,38 @@
import { useContext, useEffect, useState } from "react";
import { notifyErrorHandler } from "../util/notify";
import ControlContext from "../components/ControlContext";
/**
* @typedef {Object} Account
* @property {?string} name - Name of the currently logged in account.
* @property {function()} logout - Logs out of the web interface.
*/
export function useAccount() {
let control = useContext(ControlContext);
let [name, setName] = useState(control.accountName);
useEffect(() => {
function onConnect(data) {
setName(data.account.name);
}
function onClose() {
setName(null);
}
control.connector.on("connect", onConnect);
control.connector.on("close", onClose);
return () => {
control.connector.off("connect", onConnect);
control.connector.off("close", onClose);
};
}, [control]);
return {
name,
logOut() {
control.loggingOut = true;
control.shutdown(notifyErrorHandler("Error logging out"));
},
};
}
@@ -0,0 +1,38 @@
import React, { useEffect, useState } from "react";
let itemMetadataCache = null;
export function useItemMetadata() {
let [itemMetadata, setItemMetadata] = useState(itemMetadataCache || new Map());
useEffect(() => {
async function load() {
let response = await fetch(`${webRoot}export/item-metadata.json`);
if (response.ok) {
let data = await response.json();
itemMetadataCache = new Map(data);
let style = document.createElement("style");
style.type = "text/css";
document.head.appendChild(style);
for (let [name, meta] of itemMetadataCache) {
style.sheet.insertRule(
`.item-${CSS.escape(name)} {
background-image: url("${webRoot}export/item-spritesheet.png");
background-repeat: no-repeat;
background-position: -${meta.x}px -${meta.y}px;
height: ${meta.size}px;
width: ${meta.size}px;
}`
);
}
setItemMetadata(itemMetadataCache);
}
}
if (!itemMetadataCache) {
load();
}
}, []);
return itemMetadata;
}
+24
View File
@@ -0,0 +1,24 @@
import React, { useEffect, useState } from "react";
let localeCache = null;
export function useLocale() {
let [locale, setLocale] = useState(localeCache || new Map());
useEffect(() => {
async function load() {
let response = await fetch(`${webRoot}export/locale.json`);
if (response.ok) {
let data = await response.json();
localeCache = new Map(data);
setLocale(localeCache);
}
}
if (!localeCache) {
load();
}
}, []);
return locale;
}
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import MasterPage from "./components/MasterPage";
import SlavesPage from "./components/SlavesPage";
import InstancesPage from "./components/InstancesPage";
import InstanceViewPage from "./components/InstanceViewPage";
import UsersPage from "./components/UsersPage";
import UserViewPage from "./components/UserViewPage";
import RolesPage from "./components/RolesPage";
import RoleViewPage from "./components/RoleViewPage";
import PluginsPage from "./components/PluginsPage";
import PluginViewPage from "./components/PluginViewPage";
export const pages = [
{ path: "/master", sidebarName: "Master", content: <MasterPage /> },
{ path: "/slaves", sidebarName: "Slaves", content: <SlavesPage /> },
{ path: "/instances", sidebarName: "Instances", content: <InstancesPage />},
{ path: "/instances/:id/view", sidebarPath: "/instances", content: <InstanceViewPage /> },
{ path: "/users", sidebarName: "Users", content: <UsersPage /> },
{ path: "/users/:name/view", sidebarPath: "/users", content: <UserViewPage /> },
{ path: "/roles", sidebarName: "Roles", content: <RolesPage /> },
{ path: "/roles/:id/view", sidebarPath: "/roles", content: <RoleViewPage /> },
{ path: "/plugins", sidebarName: "Plugins", content: <PluginsPage /> },
{ path: "/plugins/:name/view", sidebarPath: "/plugins", content: <PluginViewPage /> },
];
export const sidebarPages = [
];
+36 -8
View File
@@ -36,26 +36,52 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
* @static
*/
export class Control extends libLink.Link {
constructor(connector, controlPlugins) {
constructor(connector, plugins) {
super("control", "master", connector);
libLink.attachAllMessages(this);
/**
* Mapping of plugin names to their instance for loaded plugins.
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
* @type {Map<string, module:lib/plugin.BaseWebPlugin>}
*/
this.plugins = controlPlugins;
for (let controlPlugin of controlPlugins.values()) {
libPlugin.attachPluginMessages(this, controlPlugin.info, controlPlugin);
this.plugins = plugins;
for (let plugin of plugins.values()) {
plugin.control = this;
libPlugin.attachPluginMessages(this, plugin);
}
/**
* Flag indicating the connection is in the process of logging out.
* @type {boolean}
*/
this.loggingOut = false;
/**
* Name of the account this control link is connected as.
* @type {?string}
*/
this.accountName = null;
this.instanceLogHandlers = new Map();
this.connector.on("connect", () => {
this.connector.on("connect", data => {
this.accountName = data.account.name;
this.updateLogSubscriptions().catch(err => logger.error(
`Unexpected error updating log subscriptions:\n${err.stack}`
));
});
this.connector.on("close", () => {
this.accountName = null;
});
for (let event of ["connect", "drop", "resume", "close"]) {
this.connector.on(event, () => {
for (let plugin of this.plugins.values()) {
plugin.onMasterConnectionEvent(event);
}
});
}
}
async logMessageEventHandler(message) {
@@ -105,6 +131,10 @@ export class Control extends libLink.Link {
}
async updateLogSubscriptions() {
if (!this.connector.connected) {
return;
}
await libLink.messages.setLogSubscriptions.send(this, {
all: false,
master: false,
@@ -120,8 +150,6 @@ export class Control extends libLink.Link {
}
async shutdown() {
this.connector.setTimeout(30);
try {
await libLink.messages.prepareDisconnect.send(this);
} catch (err) {
+9 -9
View File
@@ -34,27 +34,27 @@ module.exports = (env = {}) => ({
rules: [
{
test: /node_modules.fs-extra/,
use: "null-loader",
use: require.resolve("null-loader"),
},
{
test: /node_modules.jsonwebtoken/,
use: "null-loader",
use: require.resolve("null-loader"),
},
{
test: /node_modules.winston.dist.winston.transports.(http|file)/,
use: "null-loader",
use: require.resolve("null-loader"),
},
// Colour library used by winston which doesn't work in browser.
{
test: /node_modules.colors/,
use: "null-loader",
use: require.resolve("null-loader"),
},
{
test: /\.css$/,
use: [
"style-loader",
require.resolve("style-loader"),
{
loader: "css-loader",
loader: require.resolve("css-loader"),
options: {
sourceMap: !env.production,
},
@@ -73,15 +73,15 @@ module.exports = (env = {}) => ({
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
loader: require.resolve("babel-loader"),
options: {
presets: ["@babel/preset-react"],
presets: [require.resolve("@babel/preset-react")],
},
},
},
{
test: /\.png$/,
use: "file-loader",
use: require.resolve("file-loader"),
},
],
},
+3 -6
View File
@@ -1,16 +1,13 @@
Clusterio Global Chat Plugin
============================
# Clusterio Global Chat Plugin
Plugin sharing the in-game chat between instances.
Installation
============
## Installation
Run the following commands in the folder Clusterio is installed to:
npm install @clusterio/plugin-global_chat
npx clusteriomaster plugin add @clusterio/plugin-global_chat
Substitute clusteriomaster with clusterioslave or clusterioctl if this a
dedicate slave or ctl installation respectively.
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-global_chat",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"description": "Clusterio plugin forwarding between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
@@ -17,10 +17,7 @@
"@clusterio/lib": "^2.0.0-alpha.0"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.4",
"babel-loader": "^8.1.0",
"file-loader": "^6.2.0",
"null-loader": "^4.0.0",
"@clusterio/web_ui": "^2.0.0-alpha.5",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.2.0"
},
+4 -1
View File
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
@@ -19,9 +20,11 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/web_ui": { import: false },
"ajv": { import: false },
},
+31
View File
@@ -0,0 +1,31 @@
# Clusterio Player Auth Plugin
Plugin authenticating logged in players to the web interface.
Authentication is achieved by a simple challenge response mechanism where players have to log into one of the Factorio server in the cluster and open a dialog that presents a code, this code is input into the login form on the web interface which gives back a second code.
Once the player enters the second code into the dialog in-game the web interface is authenticated.
## Installation
Run the following commands in the folder Clusterio is installed to:
npm install @clusterio/plugin-player_auth
npx clusteriomaster plugin add @clusterio/plugin-player_auth
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
## Master Configuration
#### player_auth.code_length
Length in character of the generated challenge codes that need to be input between the web interface login form and the in-game Factorio login dialog.
Defaults to `6`.
#### player_auth.code_timeout
Time in seconds the first code generated stays valid.
The login must be completed in less than this time starting from when the login dialog is opened in-game.
Defaults to `120`.
+57
View File
@@ -0,0 +1,57 @@
"use strict";
let libLink = require("@clusterio/lib/link");
let libConfig = require("@clusterio/lib/config");
class MasterConfigGroup extends libConfig.PluginConfigGroup {}
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
MasterConfigGroup.groupName = "player_auth";
MasterConfigGroup.define({
name: "code_length",
title: "Code Length",
description: "Length in characters of the generated codes.",
type: "number",
initial_value: 6,
});
MasterConfigGroup.define({
name: "code_timeout",
title: "Code Timeout",
description: "Time in seconds for the generated codes to stay valid.",
type: "number",
initial_value: 120,
});
MasterConfigGroup.finalize();
module.exports = {
name: "player_auth",
title: "Player Auth",
description: "Provides authentication to the cluster via logging into a Factorio server.",
masterEntrypoint: "master",
instanceEntrypoint: "instance",
webEntrypoint: "./web",
MasterConfigGroup,
messages: {
fetchPlayerCode: new libLink.Request({
type: "player_auth:fetch_player_code",
links: ["instance-slave", "slave-master"],
forwardTo: "master",
requestProperties: {
"player": { type: "string" },
},
responseProperties: {
"player_code": { type: "string" },
"master_url": { type: "string" },
},
}),
setVerifyCode: new libLink.Request({
type: "player_auth:set_verify_code",
links: ["instance-slave", "slave-master"],
forwardTo: "master",
requestProperties: {
"player": { type: "string" },
"verify_code": { type: "string" },
},
}),
},
};
+55
View File
@@ -0,0 +1,55 @@
/**
* @module
*/
"use strict";
const libPlugin = require("@clusterio/lib/plugin");
const libLuaTools = require("@clusterio/lib/lua_tools");
class InstancePlugin extends libPlugin.BaseInstancePlugin {
async init() {
if (!this.instance.config.get("factorio.enable_save_patching")) {
throw new Error("player_auth plugin requires save patching.");
}
this.instance.server.on("ipc-player_auth", event => this.handleEvent(event).catch(
err => this.logger.error(`Error handling event:\n${err.stack}`)
));
}
async handleEvent(event) {
if (event.type === "open_dialog") {
if (!this.slave.connector.connected) {
await this.sendRcon(`/web-login error ${event.player} login is temporarily unavailable`);
return;
}
let response;
try {
response = await this.info.messages.fetchPlayerCode.send(this.instance, { player: event.player });
} catch (err) {
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
return;
}
await this.sendRcon(`/web-login open ${event.player} ${response.master_url} ${response.player_code}`);
} else if (event.type === "set_verify_code") {
try {
await this.info.messages.setVerifyCode.send(this.instance, {
player: event.player,
verify_code: event.verify_code,
});
} catch (err) {
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
return;
}
await this.sendRcon(`/web-login code_set ${event.player}`);
}
}
}
module.exports = {
InstancePlugin,
};
+192
View File
@@ -0,0 +1,192 @@
/**
* @module
*/
"use strict";
const crypto = require("crypto");
const util = require("util");
const jwt = require("jsonwebtoken");
const libPlugin = require("@clusterio/lib/plugin");
const libErrors = require("@clusterio/lib/errors");
const { basicType } = require("@clusterio/lib/helpers");
async function generateCode(length) {
// ji1lI, 0oOQ, and 2Z are not present to ease reading.
let letters = "abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXY3456789";
let asyncRandomBytes = util.promisify(crypto.randomBytes);
let code = [];
for (let byte of await asyncRandomBytes(length)) {
// Due to the 51 characters in the letters not fitting perfectly in
// 256 there's an ever so slight bias towards a with this algorithm.
code.push(letters[byte % letters.length]);
}
return code.join("");
}
class MasterPlugin extends libPlugin.BaseMasterPlugin {
async init() {
// Store of validation attempts by players
this.players = new Map();
// Periodically remove expired entries
setInterval(() => {
let now = Date.now();
for (let [player, entry] of this.players) {
if (entry.expires < now) {
this.players.delete(player);
}
}
}, 60e3).unref();
this.master.app.get("/api/player_auth/servers", (req, res) => {
this.metrics.endpointHitCounter.labels(req.route.path).inc();
let servers = [];
for (let instance of this.master.db.instances.values()) {
if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) {
servers.push(instance.config.get("factorio.settings")["name"] || "unnamed server");
}
}
res.send(servers);
});
this.master.app.post("/api/player_auth/player_code", (req, res, next) => {
this.metrics.endpointHitCounter.labels(req.route.path).inc();
this.handlePlayerCode(req, res).catch(next);
});
this.master.app.post("/api/player_auth/verify", (req, res, next) => {
this.metrics.endpointHitCounter.labels(req.route.path).inc();
this.handleVerify(req, res).catch(next);
});
}
async handlePlayerCode(req, res) {
if (basicType(req.body) !== "object") {
res.sendStatus(400);
return;
}
let playerCode = req.body.player_code;
if (typeof playerCode !== "string") {
res.sendStatus(400);
return;
}
for (let [player, entry] of this.players) {
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
let verifyCode = await generateCode(this.master.config.get("player_auth.code_length"));
let secret = this.master.config.get("master.auth_secret");
let verifyToken = jwt.sign(
{
aud: "player_auth.verify_code",
exp: Math.floor(entry.expires / 1000),
verify_code: verifyCode,
player_code: playerCode,
},
secret
);
res.send({ verify_code: verifyCode, verify_token: verifyToken });
return;
}
}
res.send({ error: true, message: "invalid player_code" });
}
async handleVerify(req, res) {
if (basicType(req.body) !== "object") {
res.sendStatus(400);
return;
}
let playerCode = req.body.player_code;
if (typeof playerCode !== "string") {
res.sendStatus(400);
return;
}
let verifyCode = req.body.verify_code;
if (typeof verifyCode !== "string") {
res.sendStatus(400);
return;
}
let verifyToken = req.body.verify_token;
if (typeof verifyToken !== "string") {
res.sendStatus(400);
return;
}
let secret = this.master.config.get("master.auth_secret");
try {
let payload = jwt.verify(verifyToken, secret, { audience: "player_auth.verify_code" });
if (payload.verify_code !== verifyCode) {
throw new Error("invalid verify_code");
}
if (payload.player_code !== playerCode) {
throw new Error("invalid player_code");
}
} catch (err) {
res.send({ error: true, message: err.message });
return;
}
for (let [player, entry] of this.players) {
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
if (entry.verifyCode === verifyCode) {
let user = this.master.db.users.get(player);
if (!user) {
res.send({ error: true, message: "invalid user" });
return;
}
let token = user.createToken(secret);
res.send({ verified: true, token });
return;
}
res.send({ verified: false });
return;
}
}
res.send({ error: true, message: "invalid player_code" });
}
async fetchPlayerCodeRequestHandler(message) {
let playerCode = await generateCode(this.master.config.get("player_auth.code_length"));
let expires = Date.now() + this.master.config.get("player_auth.code_timeout") * 1000;
this.players.set(message.data.player, { playerCode, verifyCode: null, expires });
return { player_code: playerCode, master_url: this.master.getMasterUrl() };
}
async setVerifyCodeRequestHandler(message) {
let { player, verify_code } = message.data;
if (!this.players.has(player)) {
throw new libErrors.RequestError("invalid player");
}
let entry = this.players.get(player);
if (entry.expires < Date.now()) {
throw new libErrors.RequestError("invalid player");
}
entry.verifyCode = verify_code;
}
}
module.exports = {
MasterPlugin,
// For testing only
_generateCode: generateCode,
};
+210
View File
@@ -0,0 +1,210 @@
local clusterio_api = require("modules/clusterio/api")
local auth = {}
function open_dialog(player, url, code)
if player.gui.screen.player_auth_dialog then
player.gui.screen.player_auth_dialog.destroy()
end
local frame = player.gui.screen.add {
name = "player_auth_dialog",
type = "frame",
direction = "vertical",
}
local titlebar = frame.add {
type = "flow",
}
titlebar.drag_target = frame
local title = titlebar.add {
type = "label",
style = "frame_title",
caption = "Web Interface Login",
}
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
titlebar.add {
name = "player_auth_dialog_close_button",
type = "sprite-button",
sprite = "utility/close_white",
hovered_sprite = "utility/close_black",
clicked_sprite = "utility/close_black",
style = "frame_action_button",
}
local content = frame.add {
name = "player_auth_dialog_content",
type = "frame",
direction = "vertical",
style = "inside_shallow_frame_with_padding",
}
content.style.width = 300
local p1 = content.add {
type = "label",
caption = "Login to the web interface is a 3 step process:",
}
p1.style.single_line = false
p1.style.bottom_margin = 8
local p2 = content.add {
type = "label",
caption = "1. Open the web interface in a browser, the url to it is shown below:",
}
p2.style.single_line = false
p2.style.bottom_margin = 8
local url_textfield = content.add {
type = "textfield",
text = url,
}
url_textfield.style.bottom_margin = 8
url_textfield.style.width = 300 - 24
local p3 = content.add {
type = "label",
caption = "2. Enter the following code into step 2 of the Factorio login:",
}
p3.style.single_line = false
p3.style.bottom_margin = 8
local player_code_textfield = content.add {
type = "textfield",
text = code,
}
player_code_textfield.style.bottom_margin = 8
player_code_textfield.style.width = 180
local p4 = content.add {
type = "label",
caption = "3. Enter the code in step 3 of the Factorio login here:"
}
p4.style.single_line = false
p4.style.bottom_margin = 8
local verify_code_row = content.add {
type = "flow",
}
local verify_code_input = verify_code_row.add {
name = "player_auth_verify_code_input",
type = "textfield",
}
verify_code_input.style.bottom_margin = 8
verify_code_input.style.width = 180
local verify_code_button = verify_code_row.add {
name = "player_auth_verify_code_button",
type = "button",
style = "green_button",
sprite = "utility/check_mark",
caption = "Log in",
}
verify_code_button.style.minimal_width = 80
frame.force_auto_center()
player.opened = frame
end
function auth.add_commands()
commands.add_command("web-login", "Login to the web interface", function(event)
if event.player_index then
local player = game.players[event.player_index]
clusterio_api.send_json("player_auth", {
type = "open_dialog",
player = player.name,
})
return
end
if event.parameter then
local args = {}
for w in string.gmatch(event.parameter, "[^ ]+") do
args[#args + 1] = w
end
if #args < 1 then
rcon.print("Incorrect number of parameters")
return
end
local command = table.remove(args, 1)
if command == "open" then
if #args ~= 3 then
rcon.print("Incorrect number of parameters")
return
end
local player_name, url, code = table.unpack(args)
local player = game.players[player_name]
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
end
open_dialog(player, url, code)
elseif command == "code_set" then
local player_name = args[1]
local player = game.players[player_name]
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
end
if player.gui.screen.player_auth_dialog then
player.gui.screen.player_auth_dialog.destroy()
end
elseif command == "error" then
local player_name = table.remove(args, 1)
local player = game.players[player_name]
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
end
player.print("Error: " .. table.concat(args, " "))
end
end
end)
end
auth.events = {}
auth.events[defines.events.on_gui_click] = function(event)
if not event.element or not event.element.valid then
return
end
if event.element.name == "player_auth_dialog_close_button" then
event.element.parent.parent.destroy()
elseif event.element.name == "player_auth_verify_code_button" then
local verify_code = event.element.parent.player_auth_verify_code_input.text
local player_name = game.players[event.player_index].name
clusterio_api.send_json("player_auth", {
type = "set_verify_code",
player = player_name,
verify_code = verify_code,
})
end
end
auth.events[defines.events.on_gui_closed] = function(event)
if
event.element
and event.element.valid
and event.element.name == "player_auth_dialog"
then
event.element.destroy()
end
end
return auth
+4
View File
@@ -0,0 +1,4 @@
{
"name": "player_auth",
"load": ["auth.lua"]
}
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@clusterio/plugin-player_auth",
"version": "2.0.0-alpha.5",
"description": "Clusterio plugin authenticating logged in players to the web interface",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
"license": "MIT",
"repository": "clusterio/factorioClusterio",
"scripts": {
"build": "webpack-cli --env.production",
"test": "echo \"Error: run tests from root\" && exit 1"
},
"bugs": {
"url": "https://github.com/clusterio/factorioClusterio/issues"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.0"
},
"dependencies": {
"jsonwebtoken": "^8.2.1"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.5",
"antd": "^4.6.5",
"phin": "^3.4.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.2.0"
},
"publishConfig": {
"access": "public"
}
}
+439
View File
@@ -0,0 +1,439 @@
"use strict";
const assert = require("assert").strict;
const phin = require("phin");
const jwt = require("jsonwebtoken");
const mock = require("../../../test/mock");
const master = require("../master");
const instance = require("../instance");
const info = require("../info");
const libErrors = require("@clusterio/lib/errors");
describe("player_auth", function() {
describe("master.js", function() {
describe("generateCode()", function() {
it("should generate a code of the given length", async function() {
let code = await master._generateCode(20);
assert.equal(typeof code, "string");
assert.equal(code.length, 20, "incorrect length");
});
it("should only use the easy to recognize letters in the alphabet", async function() {
let chars = new Set();
for (let i = 0; i < 20; i++) {
let code = await master._generateCode(15);
for (let char of code) {
chars.add(char);
}
}
for (let char of chars) {
assert(/[A-Za-z0-9]/.test(char), `Contained non-alphanumeric letter ${char}`);
assert(/[^ji1lI0oOQ2Z]/.test(char), `Contained banned letter ${char}`);
}
});
});
describe("MasterPlugin", function() {
let masterPlugin;
let masterUrl;
before(async function() {
masterPlugin = await mock.createMasterPlugin(master.MasterPlugin, info);
masterPlugin.master.mockConfigEntries.set("player_auth.code_length", 10);
masterPlugin.master.mockConfigEntries.set("player_auth.code_timeout", 1);
masterUrl = await masterPlugin.master.startServer();
});
after(async function() {
if (masterPlugin) {
await masterPlugin.master.stopServer();
}
});
describe("/api/player_auth/servers", function() {
it("should return a list of running servers with player_auth loaded", async function() {
function addInstance(id, status, load, name) {
masterPlugin.master.db.instances.set(id, {
config: {
get(field) {
if (field === "player_auth.load_plugin") {
return load;
} else if (field === "factorio.settings") {
return { name };
}
throw new Error(`field ${field} not implemented`);
},
},
status,
});
}
addInstance(1, "running", true, "running loaded");
addInstance(2, "stopped", true, "stopped loaded");
addInstance(3, "running", false, "running unloaded");
addInstance(4, "stopped", false, "stopped unloaded");
addInstance(5, "running", true, undefined);
let result = await phin({
url: `${masterUrl}/api/player_auth/servers`,
parse: "json",
});
assert.deepEqual(result.body, ["running loaded", "unnamed server"]);
for (let id of [1, 2, 3, 4, 5]) {
masterPlugin.master.db.instances.delete(id);
}
});
});
describe("/api/player_auth/player_code", function() {
it("should return 400 on invalid json", async function() {
let result = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: "invalid",
});
assert.equal(result.statusCode, 400);
});
it("should return 400 on valid json which is not an object", async function() {
for (let obj of [true, false, null, "string", 123, ["an", "array"]]) {
let result = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify(obj),
});
assert.equal(result.statusCode, 400);
}
});
it("should return 400 if player_code is not a string", async function() {
for (let obj of [true, false, null, 123, [], {}]) {
let result = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify({ player_code: obj }),
});
assert.equal(result.statusCode, 400);
}
});
it("should return invalid player_code if the code is expired", async function() {
let expires = Date.now() - 1000;
masterPlugin.players.set("expired", { playerCode: "expried", verifyCode: null, expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
data: { player_code: "expired" },
parse: "json",
});
assert.deepEqual(result.body, { error: true, message: "invalid player_code" });
});
it("should return a verify code and token if code is valid", async function() {
let expires = Date.now() + 1000;
masterPlugin.players.set("valid", { playerCode: "valid", verifyCode: null, expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
data: { player_code: "valid" },
parse: "json",
});
assert.equal(typeof result.body.verify_code, "string");
assert.equal(typeof result.body.verify_token, "string");
});
});
describe("/api/player_auth/verify", function() {
it("should return 400 on invalid json", async function() {
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: "invalid",
});
assert.equal(result.statusCode, 400);
});
it("should return 400 on valid json which is not an object", async function() {
for (let obj of [true, false, null, "string", 123, ["an", "array"]]) {
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify(obj),
});
assert.equal(result.statusCode, 400);
}
});
it("should return 400 if player_code, verify_code or verify_token are not strings", async function() {
for (let obj of [true, false, null, 123, [], {}]) {
for (let field of ["player_code", "verify_code", "verify_token"]) {
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "str",
verify_code: "str",
verify_token: "str",
[field]: obj,
},
});
assert.equal(result.statusCode, 400);
}
}
});
it("should return error if verify_token is not valid", async function() {
async function verify(tokenParams, error) {
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "player",
verify_code: "verify",
verify_token: jwt.sign({
aud: "player_auth.verify_code",
player_code: "player",
verify_code: "verify",
...tokenParams,
}, tokenParams.secret || "TestSecretDoNotUse"),
},
parse: "json",
});
assert.deepEqual(result.body.error, true);
}
await verify({ secret: "InvalidSecret" });
await verify({ verify_code: "bad" });
await verify({ player_code: "bad" });
await verify({ aud: "not_player_auth" });
});
it("should return invalid player_code if the code is expired", async function() {
let expires = Date.now() - 1000;
masterPlugin.players.set("expired", { playerCode: "expried", verifyCode: "verify", expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "expired",
verify_code: "verify",
verify_token: jwt.sign({
aud: "player_auth.verify_code",
player_code: "expired",
verify_code: "verify",
}, "TestSecretDoNotUse"),
},
parse: "json",
});
assert.deepEqual(result.body, { error: true, message: "invalid player_code" });
});
it("should return verified false if verify code has not yet been set", async function() {
let expires = Date.now() + 1000;
masterPlugin.players.set("unverified", { playerCode: "unverified", verifyCode: null, expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "unverified",
verify_code: "verify",
verify_token: jwt.sign({
aud: "player_auth.verify_code",
player_code: "unverified",
verify_code: "verify",
}, "TestSecretDoNotUse"),
},
parse: "json",
});
assert.deepEqual(result.body, { verified: false });
});
it("should return error if user is missing", async function() {
let expires = Date.now() + 1000;
masterPlugin.players.set("missing", { playerCode: "missing", verifyCode: "verify", expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "missing",
verify_code: "verify",
verify_token: jwt.sign({
aud: "player_auth.verify_code",
player_code: "missing",
verify_code: "verify",
}, "TestSecretDoNotUse"),
},
parse: "json",
});
assert.deepEqual(result.body, { error: true, message: "invalid user" });
});
it("should return verified true with token if valid verification", async function() {
let expires = Date.now() + 1000;
masterPlugin.players.set("player", { playerCode: "player", verifyCode: "verify", expires });
let result = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: {
player_code: "player",
verify_code: "verify",
verify_token: jwt.sign({
aud: "player_auth.verify_code",
player_code: "player",
verify_code: "verify",
}, "TestSecretDoNotUse"),
},
parse: "json",
});
assert.equal(result.body.verified, true);
assert.deepEqual(typeof result.body.token, "string", "missing token");
});
});
describe(".fetchPlayerCodeRequestHandler()", function() {
it("should return a code", async function() {
let result = await masterPlugin.fetchPlayerCodeRequestHandler({ data: { player: "test" }});
assert(typeof result.player_code === "string", "no code returned");
assert(result.player_code.length === 10, "incorrect code length returned");
let expires = masterPlugin.players.get("test").expires;
let msFromExpected = Math.abs(expires - Date.now() - 1000);
assert(msFromExpected < 100, `expiry time expected outside window (${msFromExpected}ms)`);
});
});
describe(".setVerifyCodeRequestHandler()", function() {
it("should throw if player does not exist", async function() {
await assert.rejects(
masterPlugin.setVerifyCodeRequestHandler(
{ data: { player: "invalid", verify_code: "invalid" }}
),
new libErrors.RequestError("invalid player")
);
});
it("should throw if player code has expired", async function() {
let expires = Date.now() - 1000;
masterPlugin.players.set("expired", { playerCode: "expried", verifyCode: null, expires });
await assert.rejects(
masterPlugin.setVerifyCodeRequestHandler(
{ data: { player: "expired", verify_code: "expired" }}
),
new libErrors.RequestError("invalid player")
);
});
});
describe("integration", function() {
it("should verify a full login flow", async function() {
let app = masterPlugin.master.app;
let { player_code } = await masterPlugin.fetchPlayerCodeRequestHandler({ data: { player: "test" }});
let playerCodeResult = await phin({
url: `${masterUrl}/api/player_auth/player_code`,
method: "POST",
data: { player_code },
parse: "json",
});
let { verify_code, verify_token } = playerCodeResult.body;
await masterPlugin.setVerifyCodeRequestHandler({ data: { player: "test", verify_code }});
let verifyResult = await phin({
url: `${masterUrl}/api/player_auth/verify`,
method: "POST",
data: { player_code, verify_code, verify_token },
parse: "json",
});
assert.equal(verifyResult.body.verified, true);
});
});
});
});
describe("instance.js", function() {
describe("class InstancePlugin", function() {
let instancePlugin;
before(async function() {
instancePlugin = await mock.createInstancePlugin(instance.InstancePlugin, info);
});
describe(".handleEvent()", async function() {
describe("open_dialog", async function() {
it("should call /web-login error if not connected to master", async function() {
instancePlugin.instance.server.reset();
instancePlugin.slave.connector.connected = false;
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
let command = instancePlugin.instance.server.rconCommands[0];
instancePlugin.slave.connector.connected = true;
assert.equal(command, "/web-login error test login is temporarily unavailable");
});
it("should call /web-login error after error from the master", async function() {
instancePlugin.instance.server.reset();
instancePlugin.instance.connector.once("send", message => {
instancePlugin.instance.connector.emit("message", {
seq: 1,
type: "player_auth:fetch_player_code_response",
data: {
seq: message.seq,
error: "master error",
},
});
});
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
let command = instancePlugin.instance.server.rconCommands[0];
assert.equal(command, "/web-login error test master error");
});
it("should call /web-login open after a valid response from the master", async function() {
instancePlugin.instance.server.reset();
instancePlugin.instance.connector.once("send", message => {
instancePlugin.instance.connector.emit("message", {
seq: 1,
type: "player_auth:fetch_player_code_response",
data: {
seq: message.seq,
player_code: "code",
master_url: "master-url",
},
});
});
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
let command = instancePlugin.instance.server.rconCommands[0];
assert.equal(command, "/web-login open test master-url code");
});
});
describe("open_dialog", async function() {
it("should call /web-login code_set after a valid response from the master", async function() {
instancePlugin.instance.server.reset();
instancePlugin.instance.connector.once("send", message => {
instancePlugin.instance.connector.emit("message", {
seq: 1,
type: "player_auth:set_verify_code_response",
data: { seq: message.seq },
});
});
await instancePlugin.handleEvent(
{ type: "set_verify_code", player: "test", verify_code: "verify" }
);
let command = instancePlugin.instance.server.rconCommands[0];
assert.equal(command, "/web-login code_set test");
});
it("should call /web-login error after error from the master", async function() {
instancePlugin.instance.server.reset();
instancePlugin.instance.connector.once("send", message => {
instancePlugin.instance.connector.emit("message", {
seq: 1,
type: "player_auth:set_verify_code_response",
data: { seq: message.seq, error: "master error" },
});
});
await instancePlugin.handleEvent(
{ type: "set_verify_code", player: "test", verify_code: "verify" }
);
let command = instancePlugin.instance.server.rconCommands[0];
assert.equal(command, "/web-login error test master error");
});
});
});
});
});
});
+9
View File
@@ -0,0 +1,9 @@
.factorio-login-steps {
padding-left: 1em;
}
.factorio-login-steps ul {
padding-left: 1em;
margin-bottom: 1em;
list-style-type: disc;
}
+158
View File
@@ -0,0 +1,158 @@
import React, { useEffect, useState } from "react";
import libPlugin from "@clusterio/lib/plugin";
import { notify, notifyErrorHandler } from "@clusterio/web_ui";
import { Button, Form, Input, Spin, Typography } from "antd";
import "./index.css";
const { Paragraph, Text } = Typography;
function LoginForm(props) {
let [servers, setServers] = useState(null);
let [playerCode, setPlayerCode] = useState(null);
let [playerCodeError, setPlayerCodeError] = useState(null);
let [verifyCode, setVerifyCode] = useState(null);
let [verifyToken, setVerifyToken] = useState(null);
useEffect(() => {
(async () => {
let response = await fetch(`${webRoot}api/player_auth/servers`);
if (response.ok) {
setServers(await response.json());
} else {
setServers([]);
}
})();
}, []);
useEffect(() => {
if (verifyToken) {
let checkLoop = setInterval(() => {
(async () => {
let response = await fetch(`${webRoot}api/player_auth/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
verify_token: verifyToken,
verify_code: verifyCode,
player_code: playerCode,
}),
});
if (!response.ok) {
throw new Error(`Bad server response: ${response.status}`);
}
let json = await response.json();
if (json.error) {
// While there are other possibilites the most likely
// cause of an error response here is that the code expired.
setPlayerCodeError("Code expired");
setVerifyToken(null);
clearInterval(checkLoop);
return;
}
if (json.verified) {
props.setToken(json.token);
}
})().catch(err => {
clearInterval(checkLoop);
notifyErrorHandler("Error verifying code")(err);
});
}, 2000);
return () => { clearInterval(checkLoop); };
}
return undefined;
}, [verifyToken]);
if (!servers) {
return <Spin size="large" />;
}
if (servers.length === 0) {
return <Paragraph>
There are no servers in the cluster currently running that can complete
the login via Factorio. Please try again later.
</Paragraph>;
}
return <>
<Paragraph>Login using your Factorio account is a 3 step process:</Paragraph>
<style>
</style>
<ol className="factorio-login-steps">
<li>
<Paragraph>
Start Factorio, join one of the following multiplayer servers
and type <Text keyboard>/web-login</Text> into the chat:
</Paragraph>
<ul>
{servers.map(text => <li key={text}>{text}</li>)}
</ul>
</li>
<li>
<Paragraph>Enter the code displayed in the in-game dialog here:</Paragraph>
<Paragraph>
<Form
layout="inline"
onFinish={values => {
(async () => {
let response = await fetch(`${webRoot}api/player_auth/player_code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
player_code: values.player_code,
}),
});
if (!response.ok) {
throw new Error(`Bad server response: ${response.status}`);
}
let json = await response.json();
if (json.error) {
setPlayerCodeError("Invalid Code");
setVerifyToken(null);
return;
}
setPlayerCodeError(null);
setPlayerCode(values.player_code);
setVerifyCode(json.verify_code);
setVerifyToken(json.verify_token);
})().catch(notifyErrorHandler("Error verifying code"));
}}
>
<Form.Item
name="player_code"
rules={[{ required: true, message: "Field is required" }]}
help={playerCodeError}
validateStatus={playerCodeError ? "error" : undefined}
>
<Input style={{ width: "10em" }} />
</Form.Item>
<Button type="primary" htmlType="submit">Verify</Button>
</Form>
</Paragraph>
</li>
<li>
<Paragraph>Enter this code into the in-game dialog:</Paragraph>
<Paragraph>
<Input disabled={verifyCode === null} style={{ width: "10em" }} value={verifyCode} />
</Paragraph>
</li>
</ol>
</>;
}
export class WebPlugin extends libPlugin.BaseWebPlugin {
async init() {
this.logger.info("Player Auth init");
this.loginForms = [{
name: "player_auth.factorio",
title: "Factorio",
Component: LoginForm,
}];
}
}
+37
View File
@@ -0,0 +1,37 @@
"use strict";
const path = require("path");
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
plugins: [
new webpack.container.ModuleFederationPlugin({
name: "player_auth",
library: { type: "var", name: "plugin_player_auth" },
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
},
shared: {
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/lib/plugin": { import: false },
"@clusterio/web_ui": { import: false },
"antd": { import: false },
"react": { import: false },
"react-dom": { import: false },
},
}),
],
});
+4 -8
View File
@@ -1,17 +1,13 @@
Clusterio Research Sync Plugin
==============================
# Clusterio Research Sync Plugin
Plugin synchronizing research progress between instances and the
cluster.
Plugin synchronizing research progress between instances and the cluster.
Installation
============
## Installation
Run the following commands in the folder Clusterio is installed to:
npm install @clusterio/plugin-research_sync
npx clusteriomaster plugin add @clusterio/plugin-research_sync
Substitute clusteriomaster with clusterioslave or clusterioctl if this a
dedicate slave or ctl installation respectively.
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-research_sync",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"description": "Clusterio plugin for synchronizing research between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
@@ -20,10 +20,7 @@
"fs-extra": "^8.1.0"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.4",
"babel-loader": "^8.1.0",
"file-loader": "^6.2.0",
"null-loader": "^4.0.0",
"@clusterio/web_ui": "^2.0.0-alpha.5",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.2.0"
},
+4 -1
View File
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
@@ -19,9 +20,11 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/web_ui": { import: false },
"ajv": { import: false },
},
+8 -17
View File
@@ -1,33 +1,24 @@
Clusterio Statistics Exporter Plugin
====================================
# Clusterio Statistics Exporter Plugin
Plugin exporting item production, fluid production, kill count, entity
build and pollution statistics to the Prometheus integration to
Clusterio.
Plugin exporting item production, fluid production, kill count, entity build and pollution statistics to the Prometheus integration to Clusterio.
Installation
------------
## Installation
Run the following commands in the folder Clusterio is installed to:
npm install @clusterio/plugin-statistics_exporter
npx clusteriomaster plugin add @clusterio/plugin-statistics_exporter
Substitute clusteriomaster with clusterioslave or clusterioctl if this a
dedicate slave or ctl installation respectively.
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
Instance Configuration
----------------------
## Instance Configuration
### statistics_exporter.command_timeout
Timeout in seconds to wait for the statitics gathering command to the
Factorio server return the result. If the timeout is exceeded the
previous collected results are returned instead. Normally the command
returns immediatly with the data, but if the RCON interface is
overloaded then the command may take too long to timely answer the
metrics collection request.
Timeout in seconds to wait for the statitics gathering command to the Factorio server return the result.
If the timeout is exceeded the previous collected results are returned instead.
Normally the command returns immediatly with the data, but if the RCON interface is overloaded then the command may take too long to timely answer the metrics collection request.
Defaults to `1`.
+4 -2
View File
@@ -15,8 +15,10 @@ InstanceConfigGroup.finalize();
module.exports = {
name: "statistics_exporter",
title: "Prometheus Statistics Export",
description: "Exports statistics to Prometheus",
title: "Statistics Exporter",
description:
"Provides in-game item/fluid production, builds, kills, and pollution "+
"statistics to the cluster's Prometheus endpoint.",
instanceEntrypoint: "instance",
InstanceConfigGroup,
};
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-statistics_exporter",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"description": "Clusterio plugin for gathering statistics from Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
@@ -17,10 +17,7 @@
"@clusterio/lib": "^2.0.0-alpha.0"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.4",
"babel-loader": "^8.1.0",
"file-loader": "^6.2.0",
"null-loader": "^4.0.0",
"@clusterio/web_ui": "^2.0.0-alpha.5",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.2.0"
},
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
@@ -19,9 +20,11 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/web_ui": { import: false },
"ajv": { import: false },
},
+3 -6
View File
@@ -1,16 +1,13 @@
Clusterio Subspace Storage Plugin
=================================
# Clusterio Subspace Storage Plugin
Plugin for the Subpsace Storage mod.
Installation
============
## Installation
Run the following commands in the folder Clusterio is installed to:
npm install @clusterio/plugin-subspace_storage
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
Substitute clusteriomaster with clusterioslave or clusterioctl if this a
dedicate slave or ctl installation respectively.
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
+23 -2
View File
@@ -1,6 +1,7 @@
"use strict";
const libLink = require("@clusterio/lib/link");
const libConfig = require("@clusterio/lib/config");
const libUsers = require("@clusterio/lib/users");
class MasterConfigGroup extends libConfig.PluginConfigGroup {}
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
@@ -41,6 +42,13 @@ InstanceConfigGroup.define({
});
InstanceConfigGroup.finalize();
libUsers.definePermission({
name: "subspace_storage.storage.view",
title: "View Subspace Storage",
description: "View the items and fluids stored in the shared subspace.",
grantByDefault: true,
});
// JSON schema for subspace storage items
const items = {
type: "array",
@@ -65,6 +73,9 @@ module.exports = {
masterEntrypoint: "master",
MasterConfigGroup,
webEntrypoint: "./web",
routes: ["/storage"],
messages: {
// XXX this should be a request to be reliable
place: new libLink.Event({
@@ -90,7 +101,8 @@ module.exports = {
}),
getStorage: new libLink.Request({
type: "subspace_storage:get_storage",
links: ["instance-slave", "slave-master"],
links: ["instance-slave", "slave-master", "control-master"],
permission: "subspace_storage.storage.view",
forwardTo: "master",
responseProperties: {
"items": items,
@@ -98,11 +110,20 @@ module.exports = {
}),
updateStorage: new libLink.Event({
type: "subspace_storage:update_storage",
links: ["master-slave", "slave-instance"],
links: ["master-slave", "slave-instance", "master-control"],
broadcastTo: "instance",
eventProperties: {
"items": items,
},
}),
setStorageSubscription: new libLink.Request({
type: "subspace_storage:set_storage_subscription",
links: ["control-master"],
permission: "subspace_storage.storage.view",
requestProperties: {
"storage": { type: "boolean" },
},
}),
},
};
+19
View File
@@ -72,6 +72,8 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
}
}, 1000);
this.subscribedControlLinks = new Set();
routes.addApiRoutes(this.master.app, this.items, this.metrics.endpointHitCounter);
}
@@ -90,6 +92,9 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
itemsToUpdate = [...itemsToUpdate.entries()];
this.broadcastEventToSlaves(this.info.messages.updateStorage, { items: itemsToUpdate });
for (let link of this.subscribedControlLinks) {
this.info.messages.updateStorage.send(link, { items: itemsToUpdate });
}
this.itemsLastUpdate = new Map(this.items._items.entries());
}
@@ -181,6 +186,20 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin {
};
}
async setStorageSubscriptionRequestHandler(message, request, link) {
if (message.data.storage) {
this.subscribedControlLinks.add(link);
} else {
this.subscribedControlLinks.delete(link);
}
}
onControlConnectionEvent(connection, event) {
if (event === "close") {
this.subscribedControlLinks.delete(connection);
}
}
onMetrics() {
if (this.items) {
for (let [key, count] of this.items._items) {
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-subspace_storage",
"version": "2.0.0-alpha.4",
"version": "2.0.0-alpha.5",
"description": "Clusterio plugin for sharing storage between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -20,11 +20,8 @@
"fs-extra": "^8.1.0"
},
"devDependencies": {
"@clusterio/web_ui": "^2.0.0-alpha.4",
"@clusterio/web_ui": "^2.0.0-alpha.5",
"antd": "^4.6.5",
"babel-loader": "^8.1.0",
"file-loader": "^6.2.0",
"null-loader": "^4.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack-cli": "^3.3.12",
+5
View File
@@ -0,0 +1,5 @@
.factorio-icon {
display: inline-block;
vertical-align: middle;
margin-right: 0.5em;
}
+184
View File
@@ -0,0 +1,184 @@
import React, { useContext, useEffect, useState } from "react";
import { Input, Table, Typography } from "antd";
import libPlugin from "@clusterio/lib/plugin";
import { notifyErrorHandler, useItemMetadata, useLocale, PageLayout, ControlContext } from "@clusterio/web_ui";
import info from "../info";
import "./index.css";
const { Paragraph } = Typography;
function useStorage(control) {
let plugin = control.plugins.get("subspace_storage");
let [storage, setStorage] = useState([...plugin.storage]);
useEffect(() => {
function update() {
setStorage([...plugin.storage]);
}
plugin.onUpdate(update);
return () => {
plugin.offUpdate(update);
};
}, []);
return storage;
}
function StoragePage() {
let control = useContext(ControlContext);
let locale = useLocale();
let itemMetadata = useItemMetadata();
let storage = useStorage(control);
let [filter, setFilter] = useState(null);
function getLocaleName(itemName) {
let localeName = itemName;
let meta = itemMetadata.get(itemName);
if (meta && meta.localized_name) {
// TODO: implement the locale to name conversion.
localeName = locale.get(meta.localized_name[0]);
} else {
for (let section of ["item-name", "entity-name", "fluid-name", "equipment-name"]) {
let name = locale.get(`${section}.${itemName}`);
if (name) {
localeName = name;
break;
}
}
}
return localeName;
}
let numberFormat = new Intl.NumberFormat("en-US");
return <PageLayout nav={[{ name: "Storage" }]}>
<h2>Storage</h2>
<Paragraph>
<Input
placeholder="Search"
onChange={(event) => {
let search = event.target.value.trim();
if (!search) {
setFilter(null);
return;
}
search = search.replace(/(^| )(\w)/g, "$1\\b$2");
search = search.replace(/ +/g, ".*");
let filterExpr = new RegExp(search, "i");
setFilter(() => (item => {
let name = getLocaleName(item[0]);
return filterExpr.test(name) || filterExpr.test(item[0]);
}));
}}
/>
</Paragraph>
<Table
columns={[
{
title: "Resource",
key: "resource",
sorter: (a, b) => {
let aName = getLocaleName(a[0]);
let bName = getLocaleName(b[0]);
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
render: item => {
let localeName = getLocaleName(item[0]);
let hasMeta = itemMetadata.get(item[0]);
return <>
<span className={`factorio-icon item-${hasMeta ? item[0] : "unknown-item"}`}/>
{localeName}
</>;
},
},
{
title: "Quantity",
key: "quantity",
align: "right",
defaultSortOrder: "descend",
sorter: (a, b) => a[1] - b[1],
render: item => numberFormat.format(item[1]),
},
]}
dataSource={filter ? storage.filter(filter) : storage}
rowKey={item => item[0]}
pagination={false}
/>
</PageLayout>;
}
export class WebPlugin extends libPlugin.BaseWebPlugin {
async init() {
this.pages = [
{ path: "/storage", sidebarName: "Storage", content: <StoragePage/> },
];
this.storage = new Map();
this.callbacks = [];
}
onMasterConnectionEvent(event) {
if (event === "connect") {
this.updateSubscription();
}
}
async updateStorageEventHandler(message) {
this.updateStorage(message.data.items);
}
onUpdate(callback) {
this.callbacks.push(callback);
if (this.callbacks.length) {
this.updateSubscription();
}
}
offUpdate(callback) {
let index = this.callbacks.lastIndexOf(callback);
if (index === -1) {
throw new Error("callback is not registered");
}
this.callbacks.splice(index, 1);
if (!this.callbacks.length) {
this.updateSubscription();
}
}
updateStorage(items) {
for (let [name, amount] of items) {
this.storage.set(name, amount);
}
for (let callback of this.callbacks) {
callback();
}
}
updateSubscription() {
if (!this.control.connector.connected) {
return;
}
info.messages.setStorageSubscription.send(
this.control, { storage: Boolean(this.callbacks.length) }
).catch(notifyErrorHandler("Error subscribing to storage"));
if (this.callbacks.length) {
info.messages.getStorage.send(this.control).then(
result => {
this.updateStorage(result.items);
}
).catch(notifyErrorHandler("Error updating storage"));
} else {
this.storage.clear();
}
}
}
+7 -2
View File
@@ -9,6 +9,7 @@ module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
output: {
publicPath: "auto",
filename: "bundle.js",
path: path.resolve(__dirname, "dist", "web"),
},
@@ -19,11 +20,15 @@ module.exports = (env = {}) => merge(common(env), {
filename: "remoteEntry.js",
exposes: {
"./info": "./info.js",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/lib/config": { import: false },
"@clusterio/lib/link": { import: false },
"@clusterio/lib/plugin": { import: false },
"@clusterio/lib/users": { import: false },
"@clusterio/web_ui": { import: false },
"ajv": { import: false },
"antd": { import: false },
"react": { import: false },
"react-dom": { import: false },
+34 -1
View File
@@ -2,11 +2,15 @@
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const events = require("events");
const libLink = require("@clusterio/lib/link");
const libUsers = require("@clusterio/lib/users");
const { slowTest, get, exec, execCtl, sendRcon, getControl, instancesDir } = require("./index");
const {
TestControl, TestControlConnector, url, controlToken, slowTest,
get, exec, execCtl, sendRcon, getControl, instancesDir,
} = require("./index");
async function getInstances() {
@@ -32,6 +36,29 @@ describe("Integration of Clusterio", function() {
);
});
});
describe("run", function() {
it("should handle resume of an active connection", async function() {
slowTest(this);
let tlsCa = await fs.readFile("test/file/tls/cert.pem");
let connectorA = new TestControlConnector(url, 2, tlsCa);
connectorA.token = controlToken;
let controlA = new TestControl(connectorA);
await connectorA.connect();
connectorA._state = "closing";
connectorA.stopHeartbeat();
connectorA.on("error", () => {});
let connectorB = new TestControlConnector(url, 2, tlsCa);
connectorB.token = controlToken;
let controlB = new TestControl(connectorB);
connectorB._sessionToken = connectorA._sessionToken;
connectorB._state = "handshake";
await connectorB._doConnect();
await events.once(connectorB, "resume");
await connectorB.close();
});
});
});
describe("clusterioctl", function() {
@@ -71,6 +98,12 @@ describe("Integration of Clusterio", function() {
});
});
describe("master plugin list", function() {
it("runs", async function() {
await execCtl("master plugin list");
});
});
describe("slave list", function() {
it("runs", async function() {
await execCtl("slave list");
+10
View File
@@ -13,6 +13,13 @@ const server = require("@clusterio/lib/factorio/server");
const { ConsoleTransport, logger } = require("@clusterio/lib/logging");
const libLoggingUtils = require("@clusterio/lib/logging_utils");
// Make sure permissions from plugins are loaded
require("../../plugins/global_chat/info");
require("../../plugins/player_auth/info");
require("../../plugins/research_sync/info");
require("../../plugins/statistics_exporter/info");
require("../../plugins/subspace_storage/info");
class TestControl extends libLink.Link {
constructor(connector) {
@@ -143,6 +150,7 @@ before(async function() {
await exec("node ../../packages/ctl plugin add ../../plugins/research_sync");
await exec("node ../../packages/ctl plugin add ../../plugins/statistics_exporter");
await exec("node ../../packages/ctl plugin add ../../plugins/subspace_storage");
await exec("node ../../packages/ctl plugin add ../../plugins/player_auth");
await exec("node ../../packages/master bootstrap create-admin test");
await exec("node ../../packages/master bootstrap create-ctl-config test");
@@ -175,6 +183,7 @@ after(async function() {
masterProcess.kill("SIGINT");
await events.once(masterProcess, "exit");
}
await control.connector.close();
});
// Ensure the test processes are stopped.
@@ -195,6 +204,7 @@ module.exports = {
getControl,
url,
controlToken,
instancesDir,
databaseDir,
masterConfigPath,
+8
View File
@@ -53,4 +53,12 @@ describe("Integration of lib/link", function() {
await events.once(controlConnector, "invalidate");
await events.once(controlConnector, "connect");
});
it("should properly close connector if close is called during reconnect wait", async function() {
controlConnector._socket.close(1008, "Test");
controlConnector.once("drop", () => {
controlConnector.close();
});
await events.once(controlConnector, "close");
});
});
+91 -1
View File
@@ -1,7 +1,13 @@
"use strict";
const events = require("events");
const http = require("http");
const express = require("express");
const bodyParser = require("body-parser");
const libLink = require("@clusterio/lib/link");
const libPlugin = require("@clusterio/lib/plugin");
const libUsers = require("@clusterio/lib/users");
const libPrometheus = require("@clusterio/lib/prometheus");
class MockLogger {
@@ -85,8 +91,13 @@ class MockConnector extends events.EventEmitter {
}
}
class MockServer {
class MockServer extends events.EventEmitter {
constructor() {
super();
this.reset();
}
reset() {
this.rconCommands = [];
this.rconCommandResults = new Map();
}
@@ -135,6 +146,82 @@ class MockControl extends libLink.Link {
}
}
class MockMaster {
constructor() {
this.app = express();
this.app.use(bodyParser.json({ limit: "10mb" }));
this.mockConfigEntries = new Map([
["master.external_address", "test"],
["master.auth_secret", "TestSecretDoNotUse"],
]);
this.config = {
get: (name) => {
if (this.mockConfigEntries.has(name)) {
return this.mockConfigEntries.get(name);
}
throw Error(`mock for field ${name} is not implemented`);
},
};
let roles = new Map([
[0, new libUsers.Role({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })],
[1, new libUsers.Role({ id: 1, name: "Player", description: "player", permissions: [] })],
]);
roles.get(1).grantDefaultPermissions();
let users = new Map([
["test", new libUsers.User({ name: "test", roles: [0, 1] }, roles)],
["player", new libUsers.User({ name: "player", roles: [1] }, roles)],
]);
this.db = {
instances: new Map(),
slaves: new Map(),
roles,
users,
};
}
getMasterUrl() {
return "http://master.example/";
}
async startServer() {
this.server = http.createServer(this.app);
await new Promise(resolve => {
this.server.listen(0, "127.0.0.1", resolve);
});
let address = this.server.address();
return `http://127.0.0.1:${address.port}`;
}
async stopServer() {
if (this.server) {
this.server.close();
this.server.unref();
}
}
}
async function createMasterPlugin(MasterPluginClass, info) {
let master = new MockMaster();
let metrics = {
endpointHitCounter: new libPrometheus.Counter("hit_counter", "Hit Counter", { labels: ["route"] }),
};
let logger = new MockLogger();
let plugin = new MasterPluginClass(info, master, metrics, logger);
await plugin.init();
return plugin;
}
async function createInstancePlugin(InstancePluginClass, info) {
let instance = new MockInstance();
let slave = new MockSlave();
let plugin = new InstancePluginClass(info, instance, slave);
libPlugin.attachPluginMessages(instance, plugin);
await plugin.init();
return plugin;
}
module.exports = {
MockLogger,
@@ -143,4 +230,7 @@ module.exports = {
MockInstance,
MockSlave,
MockControl,
createMasterPlugin,
createInstancePlugin,
};
+3
View File
@@ -95,6 +95,7 @@ describe("Slave testing", function() {
await fs.outputFile(path.join(testDir, "shared", "mod_a.zip"), "a");
await fs.outputFile(path.join(testDir, "shared", "mod_b.zip"), "b");
await fs.outputFile(path.join(testDir, "shared", "mod.dat"), "c");
await fs.outputFile(path.join(testDir, "shared", "mod-list.json"), "d");
let instanceConfig = new libConfig.InstanceConfig("slave");
await instanceConfig.init();
@@ -111,6 +112,7 @@ describe("Slave testing", function() {
assert.equal(await fs.readFile(instance.path("mods", "mod_a.zip"), "utf-8"), "a");
assert.equal(await fs.readFile(instance.path("mods", "mod_b.zip"), "utf-8"), "b");
assert.equal(await fs.readFile(instance.path("mods", "mod.dat"), "utf-8"), "c");
assert.equal(await fs.readFile(instance.path("mods", "mod-list.json"), "utf-8"), "d");
assert.equal(await fs.readFile(instance.path("mods", "mod_i.zip"), "utf-8"), "i");
});
@@ -140,6 +142,7 @@ describe("Slave testing", function() {
await assert.rejects(fs.lstat(instance.path("mods", "mod_a.zip")), { code: "ENOENT" });
assert.equal(await fs.readFile(instance.path("mods", "mod_b.zip"), "utf-8"), "b");
assert.equal(await fs.readFile(instance.path("mods", "mod.dat"), "utf-8"), "c");
assert.equal(await fs.readFile(instance.path("mods", "mod-list.json"), "utf-8"), "d");
assert.equal(await fs.readFile(instance.path("mods", "mod_i.zip"), "utf-8"), "i");
});
});