Compare commits

...
1130 Commits
Author SHA1 Message Date
Hornwitser 6da7b84757 v2.0.0-alpha.10.c hotfix for installer 2021-12-09 00:14:19 +01:00
Arvids Godjuks 072e95046a Merge pull request #436 from jake-87/patch-1
Wrong capitalisation on `process.env`
2021-12-09 01:04:43 +02:00
Hornwitser 5aa4d99ea3 Fix installer ignoring --plugins argument (#440)
Fix issue passing --plugins to installer which was discovered in #438.
2021-12-09 00:03:45 +01:00
Hornwitser f1ac22881c Use strict parsing in installer (#439)
Bail out if arguments that aren't understood are passed to the
installer script instead of silently ignoring them.
2021-12-08 23:59:40 +01:00
J87 fb85869d09 Fix spelling mistake 2021-12-07 22:13:28 +11:00
J87 c69672ec60 Variable misspelling 2021-12-07 21:42:58 +11:00
J87 3e1a41393a Fix incorrrent spelling of variable
Honestly not sure how this one slipped through
2021-12-07 21:42:10 +11:00
J87 e9225a4b2c Wrong capitalisation on process.env 2021-12-07 21:38:46 +11:00
Arvids Godjuks df80effbdf Merge pull request #434 from Danielv123/small-changes-for-plugins
Small changes for plugins
2021-11-25 01:37:28 +02:00
Arvids Godjuks 35a60e798e Merge pull request #431 from jake-87/user-editable-config
Implementing user editable config for CLI editors
2021-11-25 01:35:14 +02:00
J87 dfd0cb2d36 Fix linter issue 2021-11-25 07:45:56 +11:00
danielv da941a299c Start all and stop all instance buttons 2021-11-24 21:33:55 +01:00
J87 81e3c9e19b Removed unnecessary double negation 2021-11-24 14:31:42 +11:00
J87 b9e8ffa3d1 Fix linting error 2021-11-24 09:33:08 +11:00
J87 62efc63830 Fixed variable definition 2021-11-23 22:03:36 +11:00
J87 e6e5805a49 Refactored serializedConfigToString into function
I'm unable to test right now, so this is currently not yet tested. Will test asap.
2021-11-23 13:12:49 +11:00
Danielv123 a3d98595e2 Fix linter errors 2021-11-22 13:28:29 +01:00
Danielv123 6af34dc3a6 Update BaseConfigTree when props.id changes 2021-11-22 13:17:39 +01:00
Danielv123 1cf3e39d72 Avoid savesProvider throwing errors on missing instanceId 2021-11-22 12:44:48 +01:00
Danielv123 b192c82dcc Export induvidual components from the web interface for plugin use 2021-11-22 10:20:44 +01:00
Danielv123 9138efca6f Clear log console when changing props 2021-11-22 10:20:06 +01:00
Danielv123 52238de9b3 Expose react-router-dom to allow plugins to use useHistory()
See https://githubmemory.com/repo/module-federation/module-federation-examples/issues/716
2021-11-22 10:19:43 +01:00
J87 cc064c2fd1 Fix mistake with Instance descriptions
Also added warning for description not found in master.
2021-11-22 09:36:20 +11:00
J87 baf7dbfe0d Fixed linter issue 2021-11-21 20:26:17 +11:00
J87 0bc6cf1178 Minified getEditor function. 2021-11-21 20:25:33 +11:00
Daniel 8638632600 Merge pull request #425 from Danielv123/fix-invokeHook
Add timeout to invokeHook on plugins
2021-11-21 10:03:30 +01:00
jake87 6d5d9ced47 Refactored editor selection to function 2021-11-21 13:39:23 +11:00
jake87 e825f643e2 Refactored string processing to function
Previously string processing was done the exact same way for both master
and instance; this has now been refactored into a function.
2021-11-21 13:31:12 +11:00
jake87 c4f656996e Fixed linting issues 2021-11-21 12:24:00 +11:00
J87 103c0e3820 Fix missing brace from conflict merge 2021-11-21 12:15:46 +11:00
J87 6da349c888 Fix commented out catch bracket 2021-11-21 12:14:49 +11:00
J87 15c3bb81a6 Fix "emitter" spelling & rename to be more descriptive 2021-11-21 12:12:54 +11:00
J87 4a628df108 Remove unneeded backslash 2021-11-21 12:11:18 +11:00
J87 25ae9b9a0d Add example to "no editor" error 2021-11-21 12:10:24 +11:00
J87 6b10f2ab7d Resolve function style 2021-11-21 12:07:30 +11:00
danielv 770e6a41d4 Add timeout to invokeHook on plugins 2021-11-20 12:44:46 +01:00
J87 375f591fe5 Remove outdated comments 2021-11-20 22:28:21 +11:00
Daniel fcdbf59f7a Merge pull request #433 from Danielv123/fix-node12-support
Support node12 by removing optional chaining
2021-11-20 12:17:34 +01:00
Danielv123 6c80227103 Support node12 by removing optional chaining 2021-11-20 12:03:46 +01:00
Daniel 0016c6271a Merge pull request #427 from Danielv123/gh-actions
Gh actions
2021-11-20 11:50:24 +01:00
Daniel c557b7d6b2 Merge pull request #428 from Hornwitser/fix-localhost-resolve
Fix error when localhost resolves to ::1
2021-11-20 11:49:56 +01:00
J87 64c328c53d Merge branch 'master' into user-editable-config 2021-11-20 16:37:49 +11:00
J87 e5994287fb Remove outdated comments 2021-11-20 16:33:47 +11:00
jake87 7126014619 Added editing for instances
Instances require a number of modifications leading to it not being
able to be converted into a function. The main one is that there is a
list of config elements that cannot be set via the edit command and
either have to be set via different means or cannot be set. To stop
these fields constantly sending error messages, we disregard adding them
to the file in the first place.
2021-11-20 16:23:21 +11:00
Arvids Godjuks 3b8e89ea00 Merge pull request #424 from bbassie/slave-instance-ui-updates
Slave instance UI updates
2021-11-19 21:48:52 +02:00
Danielv123 e72ee3ecaf Added linter to CI action 2021-11-19 17:58:54 +01:00
jake87 31cfc45e58 Fix null processing ( again ) 2021-11-17 21:16:22 +11:00
jake87 834fc8cf98 Fixed null value parsing 2021-11-17 18:16:24 +11:00
jake87 eb3b7df390 Fix to be more consistant with the rest of clustorio
Also made sure "null" is entered as empty as to be consistant with the
rest of clustorio.
2021-11-17 17:45:54 +11:00
jake87 76882ad1c4 Fix styling to be more consistant with the rest of clustorio 2021-11-17 17:42:39 +11:00
bbassie 15381ac260 Final running status color update 2021-11-16 23:50:13 +01:00
bbassie b7e6acb24e Added instance deleted alert and updated not found 2021-11-16 23:44:07 +01:00
bbassie b86305db8c Updated colors of the instance tag 2021-11-16 23:42:02 +01:00
bbassie fe803a99ca Instance status tag color changes 2021-11-16 21:50:21 +01:00
jake87 4612826664 spelling mistake 2021-11-16 21:43:59 +11:00
Hornwitser a5a99e2ab1 Fix error when localhost resolves to ::1
Some systems may resolve localhost to ::1 while Factorio doesn't respond
to RCON connection over IPv6 loopback, this causes instances startup to
fail.  Fix by using 127.0.0.1 instead of localhost.
2021-11-15 21:29:51 +01:00
Danielv123 56584a25c8 Remove old pipeline 2021-11-15 10:26:32 +01:00
Danielv123 03ba7cb68c Added new testing pipeline 2021-11-15 10:20:18 +01:00
jake87 cc605250db removed final debugging helpers 2021-11-14 20:55:16 +11:00
jake87 ae34a86cbf Removed debugging prints, and added removing of tmpFile 2021-11-14 20:54:27 +11:00
jake87 86de5ae151 Fixed config editing, fixed child spawning, fixed config setting
Using an event emmiter, the fields can now be set correctly in the
master config. Null values are now handled correctly, and numbers are also set
correctly.
2021-11-14 20:51:06 +11:00
jake87 bcc25047f2 Readded config file editing, working on config file value setting 2021-11-13 22:35:15 +11:00
bbassie f5ce0a8a8c Missing import 2021-11-07 22:00:27 +01:00
bbassie 55bbc10986 Added slave status indicator tag to slave details page
Co-authored-by: Danielv123 <danielv@live.no>
2021-11-07 21:54:10 +01:00
bbassie 4fef2d8143 Fix message null issues 2021-11-07 21:35:48 +01:00
bbassie 4432a689bb Fix instance list 2021-11-07 21:31:56 +01:00
jake87 6013fcd528 Started rework of file loading system 2021-11-06 17:04:34 +11:00
jake87 71fb8541d5 Added more comments, added quotes around strings 2021-11-06 14:32:13 +11:00
jake87 4398d33fea Fixed definitions to be consistant with bullet points. 2021-11-05 23:32:18 +11:00
jake87 92814aa9b3 Reworked to use config list's grabber system
Also now using the system described by Hornwitser, in using
libConfig.MasterConfig.groups to get definitions. Now all that's needed
is to re-read the updated data back into clustorio.
2021-11-05 23:24:13 +11:00
bbassie 69d4a883ad Add copy public address button 2021-11-04 22:25:53 +01:00
bbassie 2e5617c4b5 Fix instance port and status update
Master was not recieving instance status after restart. Instead only a status from the slave but the slave doesn't store the full instance status.
2021-11-04 22:25:36 +01:00
bbassie f930ed36a0 Changed to using the real factorio port 2021-11-04 21:29:53 +01:00
bbassie 83b720eade Added public_address field to lists
- InstanceList
- SlavesPage
Added public_address field to messages
- slaveProperties
- instanceProperties
2021-11-04 01:39:28 +01:00
bbassie c3a1b420ea Added InstanceStatusTag react component
- Modified InstanceList & InstanceViewPage to use it
2021-11-04 01:36:56 +01:00
Hornwitser c889d8f325 Ignore SessionLost from updateInstances (#422)
Don't shutdown the slave if the connection session is lost during
updateInstances.  Resolves #418.
2021-11-03 16:27:21 +01:00
Hornwitser e23d07e965 Use safe file writing pattern (#421)
Write new file contents to a temporary file and then rename over the
target file to avoid data loss in the unlikely event there's an error
writing the new file contents or the process is interrupted.

Resolves #416.
2021-11-03 16:14:18 +01:00
jake87 08a8182173 Reworked to use throw error, added system for getting config groups
Now using `throw error` to exit out, instead of exit(1). Reworked config
get system to use the map of configs in libConfig.MasterConfig.groups .
2021-11-01 20:40:44 +11:00
Hornwitser 9e7a7a602f Merge pull request #420 from Hornwitser/slave-page
Slave page
2021-10-31 16:53:34 +01:00
Hornwitser 6e5e5f0aeb Show log console for master and slaves in web UI
Rename InstanceConsole to LogConsole and use it to show logs for the
master server and slaves as well.
2021-10-31 10:41:25 +01:00
Hornwitser 7769c1f89e Add SlaveViewPage
Add slave overview page displaying instances assigned and some basic
properties.
2021-10-31 10:41:25 +01:00
Hornwitser a2a5cf6d4c Warn on instances with duplicated ids
It's possible that someone copies an instance folder and don't change
the instance id in it.  Emit a warning in case this happens.
2021-10-31 07:26:56 +01:00
jake87 77e9b929dc Testing why config not being returned correctly 2021-10-30 12:31:20 +11:00
jake87 b6306031dc Reworked some of editor logic to work with async functions
Now async functions can be used after the editor returns, as before they
couldnt be used due to .on being non-async.
2021-10-29 22:21:46 +11:00
jake87 0564eee400 Experementing with the master config
no important work, commit for my own sake
2021-10-29 22:09:01 +11:00
jake87 f548ac2ecc Added more logic into ctl.js
Now prints out a basic approximation of the final look of the config
file; with comments on the function of each part.
2021-10-29 20:42:32 +11:00
jake87 c9fd99589b Added more selection options for the edit
ctl master config edit [editor]
is the new structure; it can take an optional editor, and will prio this
editor over any other editor, such as one set by env vars. Generally,
the prio goes like this: argv supplied editor > EDITOR env var > VISUAL
env var. Things needing to be done are marked with a TODO.
2021-10-27 20:16:37 +11:00
jake87 23d8a819af Added basic framework for edit command
Implimented framework for edit command, including ability to get a temp
file, open an editor on the file, then resume code execution once the
editor exits.
2021-10-26 21:23:23 +11:00
jake87 14b3331c8e Added basic logic for config edit command
Added groundwork for the ctl master config edit command,
including temporary file grabbing and JSON config fetching.
2021-10-26 20:46:24 +11:00
jake87 f266c441db Fixed async logic within lib/file_ops.js 2021-10-26 20:46:24 +11:00
jake87 ed73e3ad1e Fixed variable naming issue with file_ops.getTempFile 2021-10-26 20:46:05 +11:00
jake87 4f9a8fb45c Rewrote temp function to be safer, using findUnusedName 2021-10-26 17:50:39 +11:00
jake87 6910737374 Added function in lib/file_ops.js to return a temp filename with salt 2021-10-25 21:58:56 +11:00
Hornwitser bdfe8e630e v2.0.0-alpha.10.b hotfix for installer 2021-10-05 20:19:58 +02:00
Hornwitser 66719375af Fix installer not setting address and ctl config (#417)
Somehow the step of automatically obtaining the right address to apply
to slave.public_address, and asking for the master url and token for ctl
installations was implemented but the code to actually set it in the
relevant config was forgotten.
2021-10-05 20:05:44 +02:00
Hornwitser ef3d5209eb v2.0.0-alpha.10 2021-10-01 17:59:16 +02:00
Hornwitser e4eac3f9ad Merge pull request #415 from Hornwitser/small-things-6
Small things 6
2021-10-01 17:55:07 +02:00
Hornwitser 107284e796 Fix &gt; and &lt; being present in docs
< and > must apparently not be escaped inside backticks.
2021-10-01 01:52:18 +02:00
Hornwitser bad86a5ff2 Fix inventory_sync uploading editor mode
The serialization and deserialization of editor mode is not supported by
the current code, and while the download correctly checked for the
player being in editor mode, the upload did not check for the player
being in editor mode causing players in editor mode to break. Fix by
adding check in upload_inventory.
2021-10-01 01:52:18 +02:00
Hornwitser 034ca4c250 Fix research_sync sending commands before instance start
Fix commands syncing researces being sent before the instance is started
up, and potential desync of research progress as a result of out of
order execution of RCON commands sent.
2021-10-01 01:52:18 +02:00
Hornwitser 2b101d594a Fix statistics_exporter sending commands before start
Fix statistics_exporter potentially sending commands before an instance
is started when metrics are collected.
2021-10-01 01:52:18 +02:00
Hornwitser c40362fa7f Add sendOrderedRcon for plugins
Add BaseInstancePlugin method for sending RCON commands in order to
simplify logic in plugins that depend on commands being delivered in
order.
2021-10-01 01:52:18 +02:00
Hornwitser e8b515c733 Update missing docs from connector refactor
Fix documentation of max_reconnect_delay, heartbeat_interval and
session_timeout which where changed with the connector refactor.
2021-09-28 23:04:30 +02:00
Hornwitser e5582b5558 Skip gatherng metrics from dropped slaves
Avoid stacking up huge amounts of metrics requests while a slave
connection is in the dropped state.
2021-09-28 21:36:28 +02:00
Hornwitser 3b6ebeb653 Handle errors gathering metrics from slaves
If the session to a slave is lost while gathering metrics the error
would propagate out to the metrics route handler and break collection of
statistics.  Fix by handling errors thrown from sending the getMetrics
request to slaves.
2021-09-28 21:32:29 +02:00
Hornwitser bfec83996a Expect close in WsServer connection disconnect
Don't bother doing the prepareDisconnect dance if the connection is
already broken. This avoids having to wait until the connection times
out when a slave gives up on a current connection and starts a new one.
2021-09-28 21:28:38 +02:00
Hornwitser 4a72c14b74 Terminate connection on timeout
If a connection has timed out the most likely cause is the connection
going stale, in which case calling close could take up to 30 seconds
before the ws lib gives up on waiting for the close message.  Terminate
the connection instead on the Node.js side.  Note that Firefox does not
permit applications from closing connections with a code other than
1000, or in the range 3000-4999.
2021-09-28 21:24:42 +02:00
Hornwitser f529457f96 Replace ini packages with homebrew module
The INI format Factorio use differs from what the ini package parse and
produce.  Most notably no escape mechanism exists in Factorio's parser
which causes instance paths containing among other thing the # character
to completely break.  Fix by replacing the ini package with a Factorio
compatible implementation of INI parsing and serialization.

Resolves the problem in #414.
2021-09-27 03:45:52 +02:00
Hornwitser 642504874b Fix error on slave reconnect of stale connection
Remove call to removed function setTimeout on connector that was left
over from the connector refactor.
2021-09-27 03:45:52 +02:00
Hornwitser d99e160a27 Remove unused node-factorio-api dependency 2021-09-27 03:45:52 +02:00
Hornwitser 714e590496 Add sorting to web interface tables
Add sort options to the slave, instance, user, role and plugins tables.
2021-09-27 03:45:52 +02:00
Hornwitser bfcb57002d Merge pull request #413 from Hornwitser/hide-unauthorized-controls
Hide unauthorized controls
2021-09-22 22:14:15 +02:00
Hornwitser 8b59c89322 Link account menu name to user account
Take the user to the user view page for their own account when clicking
on the name in the top right account menu.
2021-09-20 00:57:38 +02:00
Hornwitser 31e9f59d8d Move save list subscription permission
Move the save list subscription permission to be after the save list
permission as these two permissions are logically related to each other
and both need to be granted to view the save list in the web interface.
2021-09-20 00:56:47 +02:00
Hornwitser 0c74aef68e Hide unauthorized web interface controls
Hide or disable controls in the web interface that the user for the
control connection it's connected as don't have the requisite
permissions to use.
2021-09-20 00:54:43 +02:00
Hornwitser 18e9fc8e7c Provide roles and permission for the control user
Add the roles and their permissions for the user the control connection
is connected as along with updates when these change.  This enables the
web interface to know which permissions it has available to it.
2021-09-20 00:52:59 +02:00
Hornwitser 1035389c9e Hotfix new player sync logic
Fix has_data not being checked by sync_player, dialog being stuck open if
no data is received by the master and master server refusing to server
empty data.
2021-09-12 00:27:46 +02:00
Hornwitser 8c62213a52 v2.0.0-alpha.9 2021-09-11 23:37:28 +02:00
Hornwitser e7248bfd98 Merge pull request #412 from Hornwitser/small-things-5
Small things 5
2021-09-11 21:50:07 +02:00
Hornwitser 8c0e883b23 Add Inventory Sync to the installer plugins 2021-09-11 19:23:23 +02:00
Hornwitser e3d8aa1a9b Add limit and order to query log request
Add optional limit to the number of log records to return in the
log query request to avoid having the web interface instance console
download the entire log for only to show a few hundred lines from it.

To make it possible to query the end of the log instead of just the
start add an order option as well.
2021-09-11 19:11:30 +02:00
Hornwitser 9aebeeac02 Fix offSlaveUpdate and offSaveListUpdate
Fix unreachable code and use of slice instead splice turning
offSlaveUpdate and offSaveListUpdate into no-ops.
2021-09-11 17:42:26 +02:00
Hornwitser 049ccee94e Disable save buttons when slave is offline
With the instance status unknown now meaning assigned slave is offline
the save list buttons can now easily be disabled using this status.
Disable the upload save, download save and delete save buttons when the
slave is offline or the instance is not assigned to a slave.
2021-09-11 17:40:38 +02:00
Hornwitser c59267d5ee Set status to unknown on slave disconnect
Change the instance status to unknown if the slave it's connected to
is disconnected rather than leaving them at their previous state
indefinitely.
2021-09-11 17:12:06 +02:00
Hornwitser c9ae964eee Correct filetype of headless server
The Factorio headless archive is actually an xz compressed tar, not gz.
2021-09-11 04:19:51 +02:00
Hornwitser 89cf89670f Add rate limiting to storage updates
Limit the rate of storage updates broadcasted by subspace_storage to at
most one per second.
2021-09-11 04:18:17 +02:00
Hornwitser 3b659116db Don't ping subspace_storage mod if master connection is down
The system for preventing items being sent without a connection to the
master and (and thus voiding the items) is to only do so if a ping from
the plugin has been received in the last 5 seconds of in-game time.
Don't send pings without being fully connected to the master to make
sure this system actually activates during connection downtime.
2021-09-11 04:18:17 +02:00
Hornwitser ddd1bde1b4 Fix bad command and request sending in subspace_storage
Ignore inventory updates and orders while the instance is not in the
running state to avoid sending commands before the server has started
up, and also ignore orders if the connection to the master server isn't
isn't online to avoid piling up requests in the buffer during session
resume attempts.
2021-09-11 04:18:17 +02:00
Hornwitser 715ab343bf Explicitly drop events events to the master if no session exists
Since the connector refactor the action of sending a message on a
connector that is in the process of connecting changed from queuing up
the message to send it to throwing an error.  To avoid events forwarded
by the slave to to the master when the connection is down from being
logged as an error forwardEventToMaster has to explicitly drop them.
2021-09-11 04:18:17 +02:00
Hornwitser 7430212ac8 Send save list explictitly on stop
Prevent ordering issues with save notifications on stop by making the
save list update part of the stop action instead of some unspecificed
amount of time later.

This also fixes the issue of the connection being closed before the last
instance sends it save list on slave shutdown.
2021-09-11 01:02:24 +02:00
Hornwitser 33efb32aa9 Add connected property to Slave and BaseConnection
Provide a convenience method for checking if the Master-Slave link is
fully online via a .connected property.  This is true if the link has
been establised, is not in the dropped state, and not in the process of
disconnecting.
2021-09-11 01:02:24 +02:00
Hornwitser 679eec05bc Fix copy-paste mistake 2021-09-11 01:02:24 +02:00
Hornwitser b019d44d0b Fix active connectors leaking after refactor
The activeConnectors mapping is from session id to connector, but the
master refactor in 67219ed accidentally changed it to try deleting the
entry by the connector reference instead.  Possibly mistaking it for a
Set instead of a Map.
2021-09-11 01:02:24 +02:00
Hornwitser efc71580ea Fix Lua global leaks
Fix unintended leaked globals in the clusterio and player_auth modules.
2021-09-11 01:02:24 +02:00
Hornwitser 90dacd98b2 Rework inventory_sync (#411)
Complete rework of the inventory synchronization to reduce the number of
times the inventory has to be downloaded and the possibilities for item
loss/duplication.

The basic concept is to acquire exclusive access to the player data
before downloading the data.  This prevents the player from logging in
to two servers at the same time and getting the synced inventory twice.
The acquisition will in most cases execute faster than the player can
provide input to the game on login, and if the player data on the master
isn't what was previously uploaded then the player inventory is deleted
and the player is turned into a spectator while the updated inventory is
being transferred into the game.  This model allows for the downloading
of the synced inventory to be skipped in the case the player joins the
same server they left from.

The synced data is expanded to include more than just the character and
inventory to properly handle sync of dead players.  If the player leaves
dead on one server this state will be saved and when the player joins
another server they continue off as dead.  The ghost, god, and spectator
controllers are supported, while the cutscene and editor controllers are
not.  For players in a cutscene when joining the acquisition will be
attempted immediately, but the scenario waits until the player is out of
the cutscene before acting upon the response to the acquisition.  For
players in the editor mode the scenario toggles them out of the mode
before syncing data.

Since uploads to the master can fail handling of this is also added.
The scenario will retry uploading the player data at exponentially
increasing intervals until it gets the message back that the upload
succeeded.  Since it's possible for servers to go down and stay down for
a long time the exclusive access held by these servers are released after
a timeout.  This means that a player could be playing and or have
uploaded newer data by the time the instance comes back online and tries
to upload the inventory, in this case the master will report the upload
succeeded but discard the data.  Instances may also upload old data if
they load back to an earlier save, which will also be discarded by the
master.

Other small improvements made by this rework:
- Add a retry button to the failed download dialog
- Update the existing progress dialog instead of destroying and
  recreating it every time.
- Automatically stuff the items from the temporary inventory into the
  synced inventory if they fit.
- Move player back to the position they started the download from if
  they were turned into a spectator for the download.
- Ensure the player has a character if the stored data is wiped.
- Add error checking of the RCON commands sent by the instance.
2021-09-07 15:49:53 +02:00
justchen1369 f9a1e28074 Add option to download and unpack latest Linux headless in installer (#409) 2021-09-07 03:12:53 +02:00
Hornwitser de3f5da9d4 Merge pull request #407 from Hornwitser/refactor-connectors
Refactor connectors
2021-08-31 20:18:14 +02:00
antt1995 abe2acd0dc Add description for running via Systemd (#408) 2021-08-31 20:14:35 +02:00
Hornwitser de48d807f1 Refactor connector logic
Refine the state flow used in the connector abstractions to better
handle close while connecting and resuming.  The new logic is to close
the connection immediately in these two states instead of continuing
trying to connect and then giving up some point later.

For the connectors the handshake state is split into connecting
and resuming, new is replaced by closed, and the closing state is turned
into a boolean flag as it's possible that closing is desired while
connecting, connected or resuming.  The connecting state is not present
in the server connector because there's nothing for the server to do in
that connector state.

The timeout logic is completely removed, once a connection attempt is
started for the client it will continue to attempt to connect, attempt
to resume dropped connections and attempt reconnects until either an
authentication error occurs or the client explicitly requests to close
the connection.

A new session timeout is added to control how long to attempt to resume
the connection until it's considered dead and invalidated.  This allows
stale connections to give up earlier, and swallow up less data while in
the dropped but not yet invalidated state.

For the client side sending messages while it's connecting has been
changed to throw an error instead of queuing up the message and
(possibly) sending it after the connection is established.  Code
shouldn't try to send messages without knowing if the connection it's
trying to send them over is active or not.
2021-08-30 20:20:49 +02:00
Hornwitser 22032ae4f8 Describe how to set up Subspace Storage (#404)
Add descriptions to the README detailing how to set up mods, and how to
set up Subspace Storage which requires manually setting up mods to work.
2021-08-19 19:10:51 +02:00
Hornwitser 3e617a81cf Add exponential backoff helper
Add helper class for implementing exponential backoff delay.
2021-08-17 05:07:46 +02:00
Hornwitser 9005b6cef9 Merge pull request #403 from Hornwitser/single-web-lib-entrypoint
Single web lib entrypoint
2021-08-17 03:27:54 +02:00
Hornwitser f4635a79f9 Remove ajv from plugin webpack config
The ajv module was made shared to reduce package size, but it turned out
the package size was large because of a missunderstanding in how imports
to sub-paths works with module federation.  Since that was fixed the ajv
module was no longer shared, but plugins incorrectly still specified it.
2021-08-17 01:42:45 +02:00
Hornwitser 8a0c1e8a4b Use single entrypoint for lib in web interface
In order to import a path inside packages/lib/ in the webpack shared
module environment it has to be declared as shared in both the web_ui
webpack config and the plugin's webpack config.  This is rather error
prone and easy to miss.  Move the burden of specifying which entrypoints
are valid in lib to lib itself by changing all imports in the web
interface code to import from @clusterio/lib instead sub paths.

For web code this means imports is done like this from now on:

import { libConfig, libLink } from "@clusterio/lib";

For info.js which has to be loadable by both Node.js and web code the
imports are done as follows:

const { libConfig, libLink } = require("@clusterio/lib");

While the Node.js only code can either continue to import sub paths
directly or use the info.js style.
2021-08-17 01:42:45 +02:00
Hornwitser e3b662717b Merge pull request #391 from Danielv123/inventory_sync
Inventory sync
2021-08-14 19:23:08 +02:00
Hornwitser cbfc0adf3d Remove extra whitespace in inventory_sync plugin 2021-08-13 06:22:45 +02:00
Hornwitser 4a29c13acb Fix bad reference in inventory_sync stat request 2021-08-13 06:21:32 +02:00
Hornwitser 60e434e4ac Fix inventory_sync player created/removed
Create entries for players that are present in the save on server
startup to preserving their inventory and fix attempting to reference the
player object after it had been removed in on_player_removed.
2021-08-13 06:17:59 +02:00
Hornwitser d126e7f25a Validate GUI element in on_gui_click
Make sure the GUI element in the on_gui_click event is valid before
attempting to access this, as another module may have destroyed it
during its event handler.
2021-08-10 18:19:18 +02:00
Hornwitser ecbce16c99 Be more defensive with global table initialization
When upgrading or downgrading the inventory_sync plugin it's possible
an different structure of the global.inventory_sync table is left behind
which could cause the code to crash when certain sub tables are not
present.

Be more defensive in the initialization of the table to make sure all
sub-tables are present on startup.
2021-08-10 17:41:40 +02:00
Hornwitser 0576308187 Fix unable to clear number fields in web UI (#402)
Fix setting a nullable numeric field to blank in the web UI return error
that the value cannot be a string due to the field returning null and
this being converted to the string "null".
2021-08-03 16:09:17 +02:00
Hornwitser f2c9cd0c62 Don't try stopping if instance is already stopped (#401)
Ignore stop if instance status is already stopped. Fixes instance status
getting stuck at stopping if Factorio exits during startup due to the
stopped status having already been sent at the time stop is invoked.

Fixes #400
2021-08-03 16:08:16 +02:00
Daniel f72995ec8b Remove control plugin 2021-08-01 21:54:46 +02:00
Daniel 2ffb848fe6 Replace spaces with tabs 2021-07-29 23:59:37 +02:00
Daniel e63f1246da Offer the player a temporary inventory while waiting for master 2021-07-29 23:55:48 +02:00
Hornwitser dbe623c9a7 Replace spaces with tabs for Lua, and more (#399)
Convert code indents to tabs to be consistent with the rest of the
JavaScript code.  This commits covers all of the lua code, some of the
hand written JSON code and the lone HTML and CSS files that were not
using tabs.
2021-07-13 17:18:44 +02:00
Hornwitser dbe01c6c23 Decode master secret from base64 (#398)
The jsonwebtoken accept strings as input for secrets which is
nonsensical from a cryptographic standpoint as the algorithms all
operate on byte sequences.  Worse still, strings are encoded into bytes
using UTF-8, which disallows certain sequences of bytes from appearing
and potentially weakening the cryptography.

For legacy reasons the master secret was encoded into base64 _and passed
as a string to jsonwebtoken_.  This further weakened the crypto as the
bytes that can appear in base 64 is only 1/4 of the 256 possible values
for bytes.  This could significantly weaken the cryptographic strength
of the tokens.

I have known of this issue for quite some time but has put off actually
fixing it for too long now.  Since the master secret is already
documented as being in base 64, decode it as base 64 and pass it as a
byte Buffer to jsonwebtoken.  This will ensure every byte value is
possible in the byte sequence passed to the jsonwebtoken crypto.

The impact of this change is unfortunately that all tokens will become
invalid and will have to be reissued.
2021-07-13 17:16:10 +02:00
Daniel 29917fb85a Maintain nodejs v12 support 2021-07-12 23:30:41 +02:00
Daniel 1a1b4fb0d2 Fix invincible ghost character creation on first join
Inventory sync will now wait intil the player has a character.
This means sync won't happen ex during respawn animations
or freeplay intro cutscenes.
2021-07-12 23:28:16 +02:00
danielv 4027a4f310 Cleanup, sync more player properties 2021-07-12 22:05:54 +02:00
danielv 37052e1f68 Changed inventory storage to use Map 2021-07-12 22:05:54 +02:00
danielv 98f801e244 Changed usage of globals, worked around edge cases 2021-07-12 22:05:54 +02:00
Daniel f9ef2ebbe1 Microoptimizations after code review 2021-07-12 22:05:54 +02:00
Daniel a939e11641 Fixed bug with import of logistics slots
Slots were shifted 1 slot to the right every time due to off by one issue in the deserializer
2021-07-12 22:05:54 +02:00
Daniel a4a667ef06 Reimplement core method for crafting_queue sync
* New method avoids all dataloss and prefectly replicates the queue
* Made proper description in readme
* Added packages to make lerna build happy
* Added reference to main readme
2021-07-12 22:05:54 +02:00
Daniel 87218e4803 Support for transferring crafting queues 2021-07-12 22:05:54 +02:00
Daniel 40301f59b7 Transfer all misc properties as well 2021-07-12 22:05:54 +02:00
Daniel 5db9cad7e5 Removed unused settings 2021-07-12 22:05:54 +02:00
Daniel e4bdf996f7 Serialize hotbar&logistics&data, show download progess 2021-07-12 22:05:54 +02:00
Daniel c1d0404f23 Made inventories download in chunks to prevent blocking rcon 2021-07-12 22:05:54 +02:00
Daniel 6bb2c50e5c Implemented changes suggested by linter 2021-07-12 22:05:54 +02:00
Daniel 8a85cfc8da Added small web interface with the most vital stats 2021-07-12 22:05:54 +02:00
Daniel 86bd07666f First working draft of inventory sync 2021-07-12 22:05:53 +02:00
Hornwitser a578719fc5 v2.0.0-alpha.8 2021-06-28 16:29:34 +02:00
Hornwitser 72e0b6a3b1 Merge pull request #397 from Hornwitser/fix-export
Fix export
2021-06-28 16:17:50 +02:00
Hornwitser 90a400b782 Handle mod not found in data export
Handle the case of the mod file not being found from the mod version.
This could happen if the naming scheme is weird or the mod is unpacked.
2021-06-26 17:59:48 +02:00
Hornwitser 80eae94fe1 Handle leading zeros in mod filenames
It's possible to put leading zeroes in the names of mod zip files which
cause the existing algorithm to find the mod zip from the mod version to
fail.  Fix by listing the mods directory and fixing the version numbers
for any of the mods with leading zeros in them.
2021-06-26 17:59:48 +02:00
Hornwitser 5daa492f68 Emitt more useful error on parse error
Rethrow parse errors with the JSON content that failed to parse for JSON
content sent to Clusterio from Factorio.
2021-06-26 17:59:48 +02:00
Hornwitser b43a181bf4 Fix JSON serialization in export mod
Fix incorrect serialization of control characters to \xHH escapes since
JSON only support \uHHHH escapes, and serialization of infinite and NaN
numbers.
2021-06-26 17:59:48 +02:00
Hornwitser a31ba31567 Fix Docker build referencing the old mod branch 2021-06-26 17:42:34 +02:00
Hornwitser b03b201c20 Merge pull request #395 from Hornwitser/rename-repository
Rename repository
2021-06-22 18:46:24 +02:00
Hornwitser af995428bc Rename clusterio/factorioClusterioMod repository
The mod was rebranded to subspace_storage quite some time ago.
2021-06-22 18:36:02 +02:00
Hornwitser d79e30f9be Rename clusterio/factorioClusterio repository
Drop the factorio part of the Clusterio repository name.
2021-06-22 18:36:02 +02:00
Hornwitser f5228e6c2e Merge pull request #396 from Hornwitser/fix-upload
Fix upload
2021-06-21 17:44:57 +02:00
Hornwitser ea77e09058 Show upload progress
Add progress bar for file uploads in progress and notify user if the
upload fails.
2021-06-21 16:59:36 +02:00
Hornwitser 5ab4b95e3c Support non-standard zip mime types in save upload
Browser like Chrome doesn't use the application/zip mime type when
uploading .zip files.  Add support for application/x-zip-compressed to
the save upload endpoint.
2021-06-21 16:59:36 +02:00
Hornwitser 8dc1415404 Skip slaveUpdateEventHandler test on Windows
Due to the lack of a simple way to automate graceful shutdown of a slave
on Windows causing the test to be unreliable, skip it on Windows.
2021-06-21 16:59:04 +02:00
Hornwitser 122bab303d Remove suggestion to use the 1.2.x branch (#394)
Development on 1.2.x has effectively been abandoned.  Discourage
installing or working on 1.2.x.
2021-06-18 14:00:21 +02:00
Hornwitser 160051fc3c Merge pull request #392 from Hornwitser/installer
Add Nodej.s based installer
2021-06-17 15:13:11 +02:00
Hornwitser 500baffb2b Use installer in installation guide
Switch over to referencing the installer as the method for installing
Clusterio in the readme and contributing docs.
2021-06-16 21:04:58 +02:00
Hornwitser 4c77f5ac31 Add installation script
Provide a create package for the clusterio npm org which gathers the
necessary details to install a working cluster either as standalone or
as separate nodes.
2021-06-16 18:56:37 +02:00
Hornwitser 8ad518e7b6 v2.0.0-alpha.7 2021-06-16 13:08:58 +02:00
Hornwitser 1faf6df5a3 Merge pull request #390 from Hornwitser/alpha-7
Alpha 7 preparations
2021-06-16 13:06:30 +02:00
Hornwitser fc08438164 Add generate slave token button
Add a generate slave token button to the web interface and modify the
generateSlaveToken message and corresponding ctl command to have the
slave id be optional.  The random id is the desired behavior when adding
a slave with the installer, as it'll ask for the token for the new slave
and deduce the id from the token.
2021-06-15 14:46:44 +02:00
Hornwitser cc562a451e Fix create instance modal
Fix copy and paste bug in the name check for the create instance moddal.
2021-06-15 14:25:45 +02:00
Hornwitser 782e93f4a1 Add changelog entry for #380
Add entry in CHANGELOG.md for the save management feature.
2021-06-15 14:24:35 +02:00
Hornwitser cf1e333191 Add bootstrap command to generate slave token (#389)
To simplify the standalone installation add command to create a slave
token without having to start up the master server.
2021-06-14 12:08:24 +02:00
Hornwitser 31f1099dc0 Expect error loading configs (#388)
Rethrow error loading configs as StartupError as these are most likely a
result of user error.
2021-06-14 12:07:59 +02:00
Hornwitser ddaaf15b2e Merge pull request #383 from Danielv123/documentation-updates
Documentation updates
2021-06-12 15:15:19 +02:00
Hornwitser a6cafeb5c6 Merge pull request #386 from Hornwitser/instances-page
Instances page rewrite
2021-06-12 15:15:04 +02:00
Hornwitser 9b34b949a6 Accept invalid IDs in useSlave and useInstance
Gracefully handle invalid IDs in useSlave and useInstance as this is
necessary when when the ID isn't known by the component using the hook,
since it can't conditionally call the hook.
2021-06-10 10:26:40 +02:00
Hornwitser 82c3d27bff Rewrite instances page
Render the table in the instances page using an antd Table component
directly and feed it the data via an automatically updating list of
instances.
2021-06-10 10:26:40 +02:00
Hornwitser 0e9893fcb6 Fix create save modal
Fix the wrong variable being passed as mapSettings and clear the form
after successfully creating the save.
2021-06-10 10:26:40 +02:00
Hornwitser f37c387d52 Fix instance deleted status not being sent out 2021-06-10 10:26:40 +02:00
Daniel 7a49e8aa15 Merge pull request #385 from Hornwitser/fix-command-after-stop
Fix subspace storage sending commands during stop
2021-06-10 06:58:20 +02:00
Daniel c80be802d1 Merge pull request #384 from Hornwitser/test-fixes
Fix tests on Windows and remove internet requirement to pass
2021-06-10 06:54:52 +02:00
Daniel ff9b05fc85 Merge pull request #382 from Hornwitser/slave-updates
Add slave update subscriptions
2021-06-10 06:50:03 +02:00
Hornwitser 55f3dac9b6 Fix integration tests requiring internet to work
When factorio starts up with with the require_user_verification setting
turn on it tries to obtain a server padlock from the matchmaking server
and will fail if this does not succeed.  Turn off user verification and
public visibility which require user verification both of which are on
by default for the integration tests so that they work without internet.
2021-06-09 14:53:33 +02:00
Hornwitser 1b082c6a25 Fix subspace storage sending commands during stop
Add logic to stop sending commands and wait for any pending commands to
complete during instance stop in subspace_storage.  This should fix the
RCON connection lost errors printed in the console during stop.
2021-06-09 14:47:52 +02:00
Hornwitser 5b78a86166 Fix HttpCloser test on Windows
Double timeout for the refuse connnection test as it's taking 2 seconds
on Windows for some reason.
2021-06-09 13:28:18 +02:00
Daniel 7d40b086e4 Fix norwegian text in web interface 2021-06-09 09:48:47 +02:00
Daniel 59b2338891 Updated guide with commands to start from source 2021-06-08 08:20:40 +02:00
Daniel af525c01ad Added syntax highlighting to docs/writing-plugins 2021-06-07 14:00:21 +02:00
Hornwitser 2be91cbd3a Add slave update subscriptions
Allow the web interface to listen for updates to the slave list and
apply it live to the places where slaves are shown in the web interface.
2021-06-02 16:19:18 +02:00
Hornwitser cc438574ff Merge pull request #380 from Hornwitser/save-management
Save management
2021-06-01 20:38:50 +02:00
Hornwitser 09e4256c18 Fix hang on missing/wrong Factorio version
Fix the start command hanging if the configured Factorio version for the
instance started is not available on the slave.
2021-06-01 16:38:58 +02:00
Hornwitser a0f5864324 Copy save if the save to start is an autosave
Prevent potential data loss when starting an autosave by copying it
first.  This avoids overwriting a random autosave slot when the server
shuts down after having loaded an autosave.
2021-06-01 16:38:58 +02:00
Hornwitser 12db0d5a84 Add download save feature
Add command and web interface button to download saves from instances.
2021-06-01 16:38:56 +02:00
Hornwitser 35f54a0ee8 Add delete save feature
Add command and web interface button to delete saves.
2021-06-01 16:32:58 +02:00
Hornwitser 2e314ff098 Add upload save feature
Add /api/upload-save endpoint that streams the data via the slave
assigned to the target instance to the instance saves directory, along
with a ctl command and web interface button to do the upload.
2021-06-01 16:27:02 +02:00
Hornwitser 0066a06107 Add findUnusedName to file_ops.js
Add function to find an unused name in a directory to lib/file_ops and
use it for finding an unused instance directory.
2021-06-01 11:22:22 +02:00
Hornwitser 544d8deb81 Add load save button to web interface 2021-06-01 11:22:22 +02:00
Hornwitser e730718671 Update antd 2021-06-01 11:22:22 +02:00
Hornwitser a96fae5ffd Add save listing and subscriptions
Add link messages for listing saves and listening for changes to the
list of saves for instances along with a table in the instance view page
showing the save list for the instance and a list-saves command to ctl.
2021-06-01 11:22:22 +02:00
Hornwitser b53ed7f6ca Add Export data button to instance page
Add missing button to do the export data action on an instance.
2021-05-21 18:39:24 +02:00
Hornwitser dd216318b0 Add option to pass map settings to Factorio
Add options to pass map seed, map exchange string, map gen settings
and/or map settings when creating a save either through the create save
request or the load scenario request.
2021-05-21 18:39:19 +02:00
Hornwitser 4a8a4c8227 Check if test control exists before closing it
Fix after action in integration test throwing in certain error cases.
2021-05-21 17:30:06 +02:00
Hornwitser 177278fee3 Ignore instance status changes that didn't change
Check if the instance status in the instanceStatusChanged event actually
changed and ignore the event if it didn't.  This prevents bogus updates
from being pushed to control clients and plugins.
2021-05-21 17:27:25 +02:00
Hornwitser 6ffcb8c451 Add instance update subscription
Provide the control link with the ability to subscribe to updates when
instance status and properties changes.  This allows the web interface
to respond in real time to instance starts and stops that are triggered
from somewhere else.
2021-05-21 17:27:01 +02:00
Hornwitser bda103d7a0 Fix --dev mode broken by master refactor 2021-05-20 21:51:51 +02:00
Hornwitser 2eadc3ea10 Add Map Exchange String parser
Add readMapExchangeString to lib/factorio which parses and decodes the
data in Map Exchange into a format suitable for giving to the Factorio
server command line.
2021-05-20 21:51:51 +02:00
Hornwitser b352e89b48 Merge pull request #379 from Hornwitser/small-things-4
Small things 4
2021-05-17 22:02:40 +02:00
Hornwitser 19ebee4697 Remove PluginsContext
The plugins are available as a property on the Control provided by the
ControlContext which makes the PluginsContext redundant.  Remove it and
use ControlContext to retreive the plugin list in the app.
2021-05-16 20:36:09 +02:00
Hornwitser 4014eb4ec2 Fix log query broken after refactor
Add missing levels import from the logging library in
ControlConnection.js.
2021-05-15 23:22:37 +02:00
Hornwitser e94662350a Support symbolic links to modules
Ignore files instead checking if the entry is a directory looking for
modules to patch in.  This is to support symbolic link to modules.
2021-05-15 23:22:37 +02:00
Hornwitser 0ae4fa0d2e Create equipment grid when deserializing it
Create equipment grid for items that support it and doesn't currently
have a grid.  Fixes deserialization of spidertron equipment grids.
2021-05-15 23:22:37 +02:00
Hornwitser b0067eccc6 Use node plugin for Eslint
The Node.js specific rules for Eslint were deprecated in favour of the
node plugin for Eslint.  Add node plugin for eslint and switch over to
using its rules, fixing any issues detected.
2021-05-15 23:22:37 +02:00
Hornwitser 95b2262cc3 Remove log rotation
Remove log rotation as Factorio implemented its own log rotation an
eternety ago and the cluster log already contains all the log entries.
2021-05-15 23:22:32 +02:00
Hornwitser 7cef5528b3 Check for 0.16 startup string
Adjust the regex for detecting startup to also work with 0.16 as
updateTick was called mapTick before 0.17.  Note: This commit does not
imply 0.16 is going to be supported by Cluestio.
2021-05-15 20:53:16 +02:00
Hornwitser a7d728c3ac v2.0.0-alpha.6 2021-05-12 23:50:26 +02:00
Hornwitser 4e61c62965 Add changelog entry for previous commit 2021-05-12 22:06:22 +02:00
Hornwitser 1c4010a744 Drop support for Node.js v10
Version 10 of Node.js is no longer supported upstream, so stop
supporting running Clusterio with it.
2021-05-12 21:43:40 +02:00
Hornwitser cbfe6fbc11 Merge pull request #376 from Hornwitser/master-refactor
Refactor master
2021-05-12 21:42:16 +02:00
Hornwitser e9c3c38a53 Add SlaveConnection.id
Add .id property to access the id of the slave this SlaveConnection is
for.
2021-05-12 20:42:52 +02:00
Hornwitser 322570f298 Add instance config event for master plugins
Add onInstanceConfigFieldChanged hook for master plugin code which
triggers when instance configs are changed on the master server.
2021-05-12 20:42:52 +02:00
Hornwitser 8b857224dc Move app routes into separate function
Reduce the _startupInternal complexity by moving out the express app
route logic into a separate function.
2021-05-12 20:42:52 +02:00
Hornwitser d3ea85d5ac Add stopping instance status
Notify when an instance is in the process of stopping with a separate
status for it.  This makes handling shutdown gracefully a bit simpler in
plugin code.
2021-05-12 20:42:52 +02:00
Hornwitser f6ab825692 Add status events for create/delete instance
Notify plugins of status changes when instances are created and deleted.
2021-05-12 20:42:52 +02:00
Hornwitser 67219ede6b Refactor master code
Move implementation of master systems into dedicated classes for better
organization.
2021-05-12 20:42:40 +02:00
Hornwitser 8b32267858 Merge pull request #377 from Hornwitser/test-package
Move test dependencies into separate package
2021-05-12 20:39:51 +02:00
Hornwitser 0a469a0524 Add missing test dependencies
The usage of lerna bootstrap --hoist meant these dependencies being
missing went unnoticed.
2021-05-12 19:57:19 +02:00
Hornwitser 65c6fd592c Log stderr instead of writing to test log
During Travis integration tests a BrokenPipeError is emitted on stderr
after the Factorio server has quit.  I have no idea what's going on here
so for now just log it and ignore it.
2021-05-07 20:07:40 +02:00
Hornwitser a6ce755743 Update development tools 2021-05-07 18:04:13 +02:00
Hornwitser c2e7a02e5a Use --hoist
Save time installing dependencies by recommending the usage of --hoist.
2021-05-07 17:39:49 +02:00
Hornwitser ef0218ddfe Move test dependencies to separate package
Move the depenedncies needed to run the tests into test/package.json and
manage it using lerna.  This should make it easier to selectively
install only what's needed to run the software.
2021-05-07 17:39:21 +02:00
Hornwitser ea645da3cb Fix chalk dependency being in the wrong package 2021-05-07 17:39:21 +02:00
Alyssa Smith 9a42f784ff Make community plugin list clearer (#374)
Include npm package names to be copied by users.
2021-03-29 00:45:42 +02:00
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
Hornwitser 9869d92ead v2.0.0-alpha.4 2021-02-26 15:52:18 +01:00
Hornwitser a248a67d10 Merge pull request #360 from Hornwitser/edit-master-config
Remote master config management
2021-02-26 15:40:24 +01:00
Hornwitser bec30c9927 Add remote editing of master config
Add commands and web UI for editing the master configuration remotely.
2021-02-24 22:16:03 +01:00
Hornwitser 38b7d0f600 Fix web config trying to set the new prop value as field
Fix the BaseConfigTree treating the nex property name and value boxes as
fields to be applied to the config when pressing Apply.
2021-02-24 20:18:41 +01:00
Hornwitser 0366334e7c Generalize InstanceConfigTree
Pull out the instance specific logic from InstanceConfigTree and turn it
into a general BaseConfigTree component that can be used for master and
slave configs.
2021-02-24 20:17:29 +01:00
Hornwitser b538960592 Add ErrorBoundary to page content
Prevent the page from turning into a black screen when page rendering
fails by adding an error boundary to the display of the page content in
SiteLayout.jsx.
2021-02-24 19:48:43 +01:00
Hornwitser ac17b8497a Split InstanceViewPage into separate components
Pull out the components in InstanceViewPage.jsx into separate source
files.
2021-02-24 18:47:03 +01:00
Hornwitser 2c74a67738 Add location access control to config class
Add a per field access parameter to the config class which specifies
which of master, slave and control can access the given field.  A
location is added to the config class to specify which party owns it and
fields which do not have an access parameter are not present in the
config and will throw if attempted to be read from or writen to.

The serialize, load, update, get, set and setProp are ammended with a
location parameter which specifiy where the data is from or destined to
and the access checks are performed to ensure remote access to the
config is still limited by the access parameter of field.

With this system in place the master config can be shared with control
and slave connections without compromising secrets that should stay
private.  This is primarily the master authentication secret, but other
plugins may define secrets it does not want to have shared with slave
and/or control connections.
2021-02-24 15:46:19 +01:00
Hornwitser 77cf4947b6 Merge pull request #359 from Hornwitser/small-things-3
Small things 3
2021-02-20 18:33:21 +01:00
Hornwitser c6a0435751 Fix missing space 2021-02-20 18:10:46 +01:00
Hornwitser 7e9f1562e4 Add Instance.id shortcut
Add shortcut for instance.config.get("instance.id") similar to the
already existing shortcut for its name.
2021-02-19 14:57:27 +01:00
Hornwitser 1de8b76303 Emit resume instead of connect on resumed connections
Raise "resume" instead of "connect" when a connection is re-established
after a connection drop.  This prevents logic meant to resynchronize
state after a full reconnection from being invoked whenever the
connection was temporarily dropped.
2021-02-19 14:28:19 +01:00
Hornwitser b4cbe2355d Add add/remove perms option to ctl role edit
Implement --add-perms and --remove-perms to add and remove permissions
from the role without having to list up all of the permissions in the
role again.

Rename --permissions to --set-perms to be consistent with the two new
options added.
2021-02-17 21:21:18 +01:00
Hornwitser 09c3f82226 Fix test failures on Windows
Fix broken regex, too tight time check, and lack of shell expansion on
Windows causing test cases to fail or not run at all.
2021-02-17 21:21:18 +01:00
Hornwitser 01e442c42e Mention WebSocket interface in docs
Replace old paragraph in the developing for clusterio document about the
removed HTTP interface with a notice of the new protocol running over
WebSocket.
2021-02-17 21:21:18 +01:00
Hornwitser 75dfc66e9a Document event_handler requiremenet for modules
Add note in the docs for modules that the event_handler interface has to
be used.
2021-02-17 21:21:18 +01:00
Hornwitser f8153bd784 Remove redundant parameter from attachPluginMessages
Remove the pluginInfo parameter from lib/plugin attachPluginMessages as
the plugin info object is available in the `.info` property of the
plugin.  While a plugin instance doesn't exist for the slave side and
was the reason for these to be separate arguments, I expect slave
plugins to be implemented eventually.
2021-02-17 21:21:18 +01:00
Hornwitser b7c7e79a86 Move distributed static/ folder to dist/web/
Move the target folder for the built web interface to dist/web/ instead
of static/.  This better follows the norm for npm packages to include
built files in the dist/ folder and also allows putting other built
files into that folder in the future.
2021-02-17 21:21:18 +01:00
Hornwitser 207a28cb44 Merge pull request #358 from Hornwitser/shutdown-logic
Shutdown logic changes.
2021-02-17 21:18:11 +01:00
Hornwitser 9660fa0801 Handle SIGHUP gracefully
Invoke shutdown instead of exiting immediately when SIGHUP is received,
and do the same on both slave and master for signals received.
2021-02-17 17:49:54 +01:00
Hornwitser 4ca092f351 Prevent shutdown running more than once
Exit immediately if shutdown has already been called.  This is due to
the possibility of it being called multiple times from combinations of
signals and fatal errors.
2021-02-17 17:48:05 +01:00
Hornwitser 99fa6b4b12 Set blocking on logging before process.exit
On windows the terminal output is asynchronous by default which leads to
log lines being lost if text is written to stdout before calling
process.exit.  Fix by using setBlocking before doing the log call.
2021-02-17 17:45:43 +01:00
Hornwitser fb30499d80 Fix Factorio config test failure
Fix response for allow-commands config having changed in the latest
experimental of Factorio.
2021-02-17 17:12:04 +01:00
Hornwitser 2a2c188aec Merge pull request #357 from furgin/handle-term-signal
Add SIGTERM handler to master and slave.
2021-02-17 16:55:39 +01:00
Matthew Jensen c101fa7a32 Added term signal handling to slave process 2021-02-03 10:22:31 +11:00
Matthew Jensen 40bd3edb32 Added TERM signal handler 2021-02-03 10:00:40 +11:00
Hornwitser e944364fc2 Merge pull request #354 from Hornwitser/improve-config
Improve config
2021-01-26 12:31:22 +01:00
Hornwitser a2c72b7fd1 Implement add and remove properties in web config
Implement removing properties for object config fields by setting them
to an empty string and adding new properties with a corresponding value.
2021-01-24 20:41:37 +01:00
Hornwitser 2117271992 Add option to read config value from stdin
Add --stdin option to the config set commands which reads the value to
set via stdin instead of on the command line.  This allows a more secure
way of entering values and avoids having to deal with multiple layers of
quoting and escapes for special characters.
2021-01-23 13:40:17 +01:00
Hornwitser 0e21060ea3 Allow removing properties with set-prop
Implement removal of properties in ConfigGroup.setProp,
set_instance_config_prop, and the set-prop clusterioctl command.
2021-01-22 12:53:53 +01:00
Hornwitser dea3a30cae Allow optional top level properties in messages
Add optional arguments to Request and Event to control which properties
are required to be present in the payload.  This allows gracefully
extending the protocol, reducing overhead for optional data, and leaving
properties out for additional behaviour.
2021-01-22 12:20:27 +01:00
Hornwitser b84c5b4293 Add common settings for Factorio servers
Add settings of interest to server administrators that appear in
server-settings.json to the factorio.settings config.  This makes it
easier to see which settings can be set there.
2021-01-18 08:17:14 +01:00
Hornwitser 201eb89dcd Assume bad JSON is string in set-prop
Assume value is supposed to be a string if it fails to parse as JSON and
it does not start and end in a [], {}, or "" pair.
2021-01-16 11:01:15 +01:00
Hornwitser 1b901f5d1c Fix the remaning linter warnings (#353)
Refactor the code base to remove all of the remaining warnings from
ESLint and convert them into errors.
2021-01-05 22:28:08 +01:00
Hornwitser 9243731646 Merge pull request #352 from Hornwitser/rework-tls
Remove automatic creation of TLS certificate
2021-01-04 21:27:15 +01:00
Hornwitser 06a0255547 Increase integration test set up timeout
Double time alloted for the setting up phase of the integration tests
from 20 seconds to 40 seconds after a suspected timeout during the
travis build.
2021-01-04 09:04:04 +01:00
Hornwitser b89688597a Fix ctl control-config not being recognized
When parsing using the .argv interface to yargs the parser state is not
restored back to its pre-parsing state after the operation completes.
This causes a susequent reparse with argv to forget its commands and
respond with an unknown arguments error when trying to use the
control-config command to clusterioctl.  Fix by switching to use the
parse() interface which does restore the parser state after completing.
2021-01-04 08:22:05 +01:00
Hornwitser 3592ff3269 Remove automatic creation of TLS certificate
The automatic creation of a self-signed certificate while nice presents
several problems.  To make connections not reject the self-signed
certificate the validation of TLS certificates are wholesale disabled,
decreasing the security even when properly set up and trusted
certificates are used.

To make the created self-signed certificate pass validation it is
necessary for it to list the host names it's valid for and to copy it to
each machine that intends to connect to the server.  These two steps
complicate automating the process in a reliable way and will likely lead
to confused users seeking to disable TLS certificate validation as the
easy way out to problems.

For these reasons, remove the automating creation of a broke self-signed
certificate, fix validation of certificates and document how to set up
TLS properly.  This makes HTTPS recommended when accessing over the
internet, but still something which must be explicitly configured.
2021-01-04 08:22:05 +01:00
Hornwitser a094a06cd1 Merge pull request #351 from Hornwitser/alpha-bugfixes-3
Alpha bugfixes 3
2020-12-29 17:57:48 +01:00
Hornwitser aa3dfdbc0a Fix crash on race condition stopping Factorio server
If the server decides to exit on its own during the stop operation to
the FactorioServer class after the operation has started but before the
signal is sent to the process then the exit handler will have wiped the
server child process and cause the kill call to fail.  This in turn
would let the hang timer running which would bring down the Node.js
process due to an unhandled error for the same reason.

Fix by making sure the stop operation watches for the server exiting and
returning early if the server already quit before getting to the send
stop signal part.
2020-12-29 03:34:38 +01:00
Hornwitser 16525ae07c Add graceful closure of HTTP connections
When closing down the HTTP and HTTPS servers existing connections are
not closed by Node.js, and may live on forever if they are actively in
use.  A common case where this happens is if Prometheus is set up with a
short interval for collections.

To close down the active connections they need to be tracked and closed
when the server is closed, add the HttpCloser class which encapsulates
the tracking and closure process behind a simple interface and use this
to close down the HTTP and HTTPS server on the master.
2020-12-28 07:05:04 +01:00
Hornwitser e8405cbf77 Fix message validation for request responses
Fix the validation loging for sending responses to requests not
functioning due to the message data property being passed incorrectly to
the validator.
2020-12-28 07:03:32 +01:00
Hornwitser 6eef4bd300 Make metrics gathering timeout configurable
When reducing the collection interval in Prometheus it may be necessary
to to reduce the metrics gathering timeout to ensure it's less than the
interval.  Add config options to adjust the timeouts used for metrics
gathering so that these can be set accordingly.
2020-12-28 07:03:32 +01:00
Hornwitser 699e3994df Add missing plugin error printing in slave.js
Special case PluginError printing during startup in slave.js in the same
way it's done in master.js.  This makes it so that the stack of the
error thrown by a plugin is printed instead of the stack trace of the
place it was wrapped into a PluginError.
2020-12-28 07:03:32 +01:00
Hornwitser b681ea7243 Add RateLimiter helper
Add helper class for implementing rate limited operations.
2020-12-28 07:03:32 +01:00
Hornwitser 29a3c26b44 Fix usage of logger in FactorioServer
Fix incorrect usage of the global logger, and typo'd use of the _logger
property in FactorioServer.
2020-12-28 07:03:32 +01:00
tovernaar123 35cb9f8698 Add bind_address option (#349)
Add option to set the IP address the master server binds its HTTP and
HTTPS ports to.
2020-12-28 03:40:28 +01:00
Daniel 6204c73ef2 Merge pull request #346 from Danielv123/github-actions
Added automated build and test infrastructure
2020-12-23 17:15:16 +01:00
Danielv123 95e1b3e349 Run actions for all branches 2020-12-23 14:26:56 +00:00
Danielv123 30f345be55 Added automated build and test infrastructure
Based on docker and github actions
Builds a docker image for each commit as well as the lua lib mod
Builds the master branch of subspace_storage for use with the tests
Tests are based on latest stable factorio branch
2020-12-23 14:13:02 +00:00
Arvids Godjuks 231505c6fe Merge pull request #345 from BlizzardBlue/patch-1
Fix typo in README.md
2020-12-22 12:08:21 +02:00
BlizzardBlue d87252d52f Fix typo in README.md 2020-12-22 12:07:45 +09:00
Hornwitser b3e518d570 v2.0.0-alpha.3 2020-12-14 07:11:34 +01:00
Hornwitser 3b13dfdcb5 Merge pull request #344 from Hornwitser/alpha-3
Alpha 3 preparations
2020-12-07 21:04:24 +01:00
Hornwitser 2e5333af29 Fix banlist sync being broken
Fix the logic for determining which entries in the banlist changed being
incorrect and causing the banlist sync to be broken.  Add unit tests for
the syncUserListsEventHandler to ensure it functions as exxpected.
2020-12-07 18:59:32 +01:00
Hornwitser 450e27e001 Add metric mapping slave and instance id to name
Add exported metrics that map instance id and slave id to their
respective names.  This allows getting the instance name of a metric by
doing a left join using query of the form 'metric * on(instance_id)
group_left(instance_name) clusterio_master_instance_mapping'.
2020-12-07 17:22:17 +01:00
Hornwitser 71164faa14 Fix docs and usage of Collector callback
Fix callback typo in Collector constructor jsdocs, add notice that
the collector is passed as the first argument, and use this first
arugument in the built-in collector implementations.
2020-12-07 17:15:25 +01:00
Hornwitser a89106f4a3 Add changelog entry for logging system 2020-12-07 17:12:29 +01:00
Hornwitser d768019e1b Merge pull request #343 from Hornwitser/factorio-1.1
Add support for Factorio 1.1
2020-12-07 17:05:49 +01:00
Hornwitser 30b7fd8767 Add support for Factorio 1.1
Add variant to clusterio_lib supporting 1.1 and update travis config to
use the 1.1 compatible version of Subspace Storage.
2020-12-06 22:48:47 +01:00
Hornwitser faae6ccf97 Rework mod build system to be multi-version capable
Add a variants array which can specify overriden properties of the
info.json for building multiple versions of the mod at the same time and
fix the additonal_files to allow specifying under what path the file is
put.

Together these two changes allow setting up the build to do all
supported versions at the same time and add specialized code for each
version of Factorio supported.
2020-12-06 22:46:32 +01:00
Hornwitser 6c11cefb99 Remove resource count limiting
Remove the limiting of the resource counts sent to the subspace_storage
mod as this has been implemented on the mod side with version 1.99.6.
2020-12-06 17:09:46 +01:00
Hornwitser dd8f9dbfd5 Merge pull request #341 from Hornwitser/log-system
Implement logging system
2020-12-06 16:58:38 +01:00
Hornwitser 93752e671e Fix plugin errors show incorrect error stack
Fix master server showing the stack of the PluginError instance on
startup instead of the original error that the plugin threw.
2020-11-30 02:49:06 +01:00
Hornwitser 7b75d4f0bb Fetch log in web interface instance view
Use the queryLog request to add past entries to the log fix the console
view to scroll to the bottom and stick there upon being loaded up.
2020-11-30 02:49:06 +01:00
Hornwitser 1c8a2fef32 Add log inspection commands
Add log query and log follow commands which allows inspection of the
combined cluster log.  Past entries can be queried via the new queryLog
request.  The log is stored into a single flat file for now, though the
performance of this may good enough.
2020-11-30 02:49:06 +01:00
Hornwitser 69221a310c Add startTimer to Gauge
Add startTimer interface to Gauge similar to how the startTimer
interface for Histogram works.
2020-11-30 02:49:06 +01:00
Hornwitser 49b5b4412f Fix require paths shown in contributing doc
Change the example code in the contributing doc to use the clusterio org
when referencing the lib package.
2020-11-27 18:21:26 +01:00
Hornwitser fe86041fc4 Document server settings applied live
Add a list of the server settings that are automatically applied to a
running instance when they are changed.
2020-11-27 18:20:13 +01:00
Hornwitser 957983d8a5 Add logging system based on winston
Add winston based logger for master, slave, ctl, lib, and web_ui code.
Logging done from slaves and instances are sent to the master server and
tagged with the instance and/or slave it originated from.  This allows
for example error messages to propagate back to the source that iniated
the action.  The master server also stores the log to allow it to be
queried at a later point.  For slaves a copy of the slave's log is also
stored locally.
2020-11-27 17:51:07 +01:00
Hornwitser 8f176c969b Merge pull request #340 from Hornwitser/rcon-performance
Improvements from investigating RCON performance
2020-11-20 22:27:19 +01:00
Hornwitser 79d20260c5 Document instance.auto_start config
Add entry to docs/configuration.md for the auto_start option implemented
in 87da492.
2020-11-20 04:55:01 +01:00
Hornwitser 0079d8634a Optimize labelsToKey for single label metrics
Avoid doing an Array.join when there's only one label in labelsToKey.
2020-11-20 01:36:21 +01:00
Hornwitser a08087c5e8 Optimize escapeLabelValue for the common case
Avoid an expensive string copy for the common case of there being no
characters to escape in the label value.
2020-11-20 01:36:07 +01:00
Hornwitser cdb048741c Rework RCON statistics gathering
Add a sendRcon method to BaseInstancePlugin and the slave's Instance
class.  This servers two purposes, one is to reduce the repetition when
send RCON commands particularly from plugins, and the other is to
instrument the RCON commands sent with metrics about duration and size.
2020-11-20 01:36:07 +01:00
Hornwitser 4d0720ade8 Add documentation for lib/prometheus
Document the basic usage of lib/prometheus for instrumenting code, and
the interfaces it exposes.
2020-11-20 01:36:07 +01:00
Hornwitser e5e1851852 Add Histogram collector to prometheus client
Move common functionality between labeled collectors out of
ValueCollector and into a new LabeledCollector class, and use this as
the basis for implementing a Histogram collector.
2020-11-18 21:27:18 +01:00
Daniel 0b249b6018 Merge pull request #339 from justchen1369/master
change some occurrences of var
2020-11-18 09:40:51 +01:00
justchen1369 c65494f093 change some occurrences of var 2020-11-17 23:53:35 -05:00
Hornwitser e82f849118 Fix web instance log following lost on reconnect
Fix the console view of the instance web interface losing the output
subscription after a reconnect to the master server due to not
re-subscribing to the outputs.
2020-11-17 04:34:49 +01:00
Hornwitser 87da492ef8 Add auto_start config option to instances
Add option to automatically start the instance on slave startup.
2020-11-17 04:34:49 +01:00
Hornwitser 9d2ac2b81f Rework instance assigment process
Under the current system there's no distinction made on the slave side
between instance config files that are present in the filesystem and
instances actually assigned to the slave.  Which could lead to problems
with determing if an instance is actually assigned to the slave.

Resolve this issue by separating discovered instances with assigned
instances.  Upon startup discovered instances are put into the
discoveredInstanceInfos mapping on the Slave object and all of these are
sent to the master server.  The master server in turn sends assignment
requests for all the instances which are assigned to the slave.  These
are put into the instanceInfos mapping of the Slave object which can
then be relied up to include only assigned instances.
2020-11-17 04:34:49 +01:00
Hornwitser c78bf02ce4 Fix event forwarding to master
Fix trying to use non-existent method on Slave class for forwading
events to the master.  Also update request forward to be consistent with
event forwarding.
2020-11-17 04:22:08 +01:00
Hornwitser 2f7419cf3b Add max_concurrent_commands option
Add option to set the the maximum amount of commands transmitted in
parallel to the game.  Increasing this value allows a greater rate of
commands to be executed.
2020-11-17 04:22:07 +01:00
Hornwitser 10303f979d Add timeout to statistics_exporter metrics
When statistics_exporter handles getMetrics it may take a long time for
the result of command it sends to be returned if the instance's command
stream is overloaded, this in turn would cause the slave to miss its
window to reply on the getMetrics request and no metrics for the slave
would be available.  Ensure statistics_exporter does not use more than 5
seconds to respond by adding a timeout to the metrics gathering.
2020-11-17 04:19:05 +01:00
Hornwitser b742291131 Add timeout to slave metric request
When the slave metrics request takes more than 10 seconds the response
to the /metrics endpoint ends up being discarded by Prometheus which
defaults to a timeout of 10 seconds.  Fix by ignoring slaves that take
longer than 8 seconds to respond to the metrics request.
2020-11-17 04:19:05 +01:00
Hornwitser a06781ebc3 Merge pull request #338 from Hornwitser/assorted-improvements
Assorted improvements
2020-11-15 20:03:35 +01:00
Hornwitser f5e28f6815 Fix errors detected by linter 2020-11-12 04:35:22 +01:00
Hornwitser 770a6664d4 Add missing instance request forwarding
Add missing forwardRequestToInstance default handler for Instance
connections for handling request forwarding from one instance to
another.
2020-11-12 04:22:13 +01:00
Hornwitser f89e66e3cb Rename getMetrics request type
Remove statistics_exporter: prefix to the getMetrics request which was
left over from when this event was a part of statistics exporter.
2020-11-12 04:19:57 +01:00
Hornwitser 65a25e2510 Add more context to missing handler error
Amend the error message shown when a message handler is missing from a
plugin with the expected name of the handler and the name of the plugin
it's missing from.
2020-11-12 04:17:52 +01:00
Hornwitser 5b74432ee3 Improve web interface console performance
Calculate the coloring for instance console messages once instead of
every time the instance console is updated.
2020-11-12 04:07:08 +01:00
Hornwitser 49eb966315 Respond to unhandled requests
Give an error response back to unhandled requests, this fixes requests
hanging forever when the target does not support the given request.
2020-11-12 04:05:24 +01:00
Hornwitser 42e680f09d Fix typos in documentation
Fix typos spotted by T-A-R#8121 in the documentation.
2020-11-12 03:53:40 +01:00
Hornwitser 31c0925354 Fix error on dash in instance name
Fix stripPaths logic breaking when the instance name contains RegExp
meta characters due to a lack of escaping.
2020-11-10 22:13:07 +01:00
Hornwitser 4c9b91ab57 Lower default heartbeat interval
Use 45 seconds as the default heartbeat interval instead of 60.  Using
60 causes connections to drop when proxied through Nginx as the default
timeout there is also 60 seconds.
2020-11-09 20:19:22 +01:00
Hornwitser a92ae95ab1 Fix updating config breaking instance status
Fix broken assign instance code in slave sending of the wrong instance
status to the master server.
2020-11-09 18:36:03 +01:00
Hornwitser fc8edeba5f Reduce log spam on connection error
Print only the error message in the console when a connection error is
encountered on the link connector.  This reduces log spam on reconnect
for slaves.
2020-11-09 17:26:56 +01:00
Hornwitser db1f239a0f Add formatting of Script and Verbose levels
Detect and colour the Script and Verbose levels.  The format of the
source file for the Script level is slightly different and needs an
update to the output parsing.
2020-11-09 15:57:46 +01:00
Hornwitser c8c65d2e92 Add verbose logging option
Add option to pass the --verbose flag to the Factorio server.
2020-11-09 15:57:46 +01:00
Hornwitser 67654fdf85 Add stripping of instance paths in Factorio log
Add option defaulting to true to strip instance directory paths printed
into the Factorio log down to be relative to the instance.
2020-11-09 15:57:46 +01:00
Hornwitser 63a656cd61 Rename default Admin role to Cluster Admin
Clarify the purpose and distinquesh the Admin role a bit better from the
in-game admin status by renaming the role to Cluster Admin.
2020-11-09 15:57:46 +01:00
Hornwitser 36865cfcf9 v2.0.0-alpha.2 2020-11-09 12:50:24 +01:00
Hornwitser d493c2d671 Merge pull request #337 from Hornwitser/alpha-2
Alpha 2 preparations
2020-11-09 12:44:59 +01:00
Hornwitser 2a0ae12829 Improve the responsiveness of the web interface
Set breakpoint for the sidebar so that it hides by default when the page
width is too small to show it with the content and keep the width of the
content constant on small screens while sliding the sidebar out to
prevent it getting crushed.
2020-11-08 20:28:48 +01:00
Hornwitser 6a4875c5ec Add bootstrap generate-user-token command
Add command to generate a user token for connecting to the master server
web interface.  This servers as an alternative to generating a control
config for use with clusterioctl.
2020-11-08 18:27:24 +01:00
Hornwitser 325c08389e Fix typo in README.md
Replace does with doesn't as the latter was intended.
2020-11-07 20:24:25 +01:00
Hornwitser bcd1290301 Move config command to lib/shared_commands
Move the definition and implementation of the config command to
lib/shared_comhmands.js, this removes the need for lib config to depend
on the fs module, which is not accessible in web build.
2020-11-07 20:23:06 +01:00
Hornwitser bd1414dd1c Remove CI failure debug code
Remove the echoing out of the test log from test/integration/server.js
used for debugging weird hang on shutdown immediately after startup that
would only happen on Travis.
2020-11-07 18:42:11 +01:00
Hornwitser d6623f89aa Run build scripts on Travis
Run the build scripts of all the packages when testing integrations on
Travis.  This should help catch errors caused by bad web interface code.
2020-11-07 18:39:56 +01:00
Hornwitser 0494858886 Move loadPluginInfos out of lib/plugin.js
Move the code for loading plugins infos from Node.js packages into a
separate module in order to break it out from lib/plugin.js which is
also used on the web interface side.  This resolves a warning of using
an expression with require on the webpack build.
2020-11-07 18:37:51 +01:00
Hornwitser df94ff1d8d Remove leftover file from web refactor
Remove wslink.jsx which became obsolete after f67f56e reimplemented the
logic.
2020-11-07 18:24:17 +01:00
Hornwitser 85111e233e Move linebreak to be after + operator 2020-11-07 18:19:50 +01:00
Hornwitser 5337280eec Remove usage of ++ outside of for loops update
The usage of postfix ++ is somewhat obscure in its meaning.  It's better
to be explicit here.
2020-11-07 18:16:24 +01:00
Hornwitser 3ce32ba870 Fix ctl slave generate-token command
Fix missing link -> libLink rename in clusterioctl slave generate-token
command as well as missing this reference.
2020-11-07 17:53:57 +01:00
Hornwitser 6cb33a8b63 Fix typo in subspace_storage config 2020-11-07 17:48:12 +01:00
Hornwitser 10cebd9e09 Fix typo in request description
Unlike HTTP the lib/link Requests can be initiated by both sides of the
connection.
2020-11-07 17:46:48 +01:00
Hornwitser 0c554cc754 Use double quotes in documentation code
Be consistent with the project code style which requires the use of
double quotes in code.
2020-11-07 17:45:24 +01:00
Hornwitser f3b918e2c2 Merge pull request #335 from Hornwitser/web-refactoring
Web Interface Rework
2020-11-07 17:42:51 +01:00
Hornwitser f67f56e6de Refactor web interface
Reimplement the pages of the web interface in order to make them
prettier and more easily amended by plugins.
2020-11-06 23:08:43 +01:00
Hornwitser 0c0626564a Add login flow
Present user with a login form to input token when connection fails or
the token is not present in local storage.  Replaces the settings page.
2020-10-29 19:11:10 +01:00
Hornwitser 019ffc25f0 Add context to webpack configs
Fix the issue with paths resolving incorrectly for the webpack config
when running the master server with the --dev option from the project
root folder through either npx clusteriomaster or node packages/master
2020-10-28 19:22:20 +01:00
Hornwitser 690e566556 Move web interface to its own package
Create clusterio/web_ui package and move all of the web interface files
to it in order to be able to share the interface components with
plugins.
2020-10-28 19:22:14 +01:00
Hornwitser eee4c69575 v2.0.0-alpha.1 2020-10-28 15:02:13 +01:00
Hornwitser adbbdef1a8 Merge pull request #327 from Hornwitser/add-package-readmes
Add package readmes and update project
readme file.
2020-10-28 13:32:04 +01:00
Hornwitser 36e5606e25 Add section on configuring the master server
Add detailed instructions for setting the https port the master server
runs on, setting the external_address option and how to set up a reverse
proxy to the master server.
2020-10-28 01:48:59 +01:00
Hornwitser 5fb7a33199 Link slave and ctl docs in README
Link to the slave package README and the managing-a-cluster document in
the sections for slave and clusterctl.
2020-10-28 01:39:43 +01:00
Hornwitser db0a3fb85e Drop support for Node.js v11
Drop claims of supporting the EOL v11 branch of Node.js.
2020-10-28 00:58:01 +01:00
Hornwitser 1215bbf3ab Rewrite introduction to Clusterio
Rewrite the woefully out of date description of Clusterio in the
readme to give a better sense of what Clusterio is and the available
plugins.
2020-10-28 00:58:01 +01:00
Hornwitser 64ac5a181a Fix typo in managing-a-cluster.md filename. 2020-10-28 00:58:01 +01:00
Hornwitser 4834c289ff Add README.md files to packages
Add README with basic descriptions of each package to all of the
packages in this repository.
2020-10-28 00:58:01 +01:00
Hornwitser 5fffc282fe Merge pull request #332 from Hornwitser/web-plugins
Implement plugin loading in web interface
2020-10-27 21:57:42 +01:00
Hornwitser 6dbf29153e Implement plugin loading in web interface
Add loading of plugin info to the web interface via webpack Module
Federation.
2020-10-27 18:24:45 +01:00
Hornwitser 542d8a798f Merge pull request #326 from Hornwitser/publish-fixes
Publish fixes
2020-10-26 15:50:53 +01:00
Hornwitser 7d8b55adb6 Use less strict matching for lib dependency
Stop requiring exact version match for the clusterio/lib dependency in
master, slave and ctl to make doing alpha releases simpler.
2020-10-26 15:24:29 +01:00
Hornwitser 1eca2ec6e6 Add missing ajv dependency to clusterio/lib
The lib/schema module uses ajv for compiling JSON Schema validators but
for some reason ajv was not part of the dependency list in its
package.json.
2020-10-26 15:17:01 +01:00
Hornwitser 9ee3fe6c37 Move gitignore for web build out of package
Mave the ignore entry for packages/master/static out of
packages/master/.gitignore to avoid npm using it and causing the web
build to be excluded from the package.
2020-10-24 16:46:02 +02:00
Hornwitser d96fb75703 Mark packages public
Add the required publishConfig.access: public for publishing public
packages in an ord on npm.
2020-10-24 14:43:42 +02:00
Hornwitser 20c7b613f2 Use lerna's pre-publish version format
Use -alpha.0 instead of -alpha0 as the pre-publish version as this is
the format lerna uses with the --canary option to publish.
2020-10-24 14:31:49 +02:00
Hornwitser 59436e7f10 Add notice in changelog about uploading to npm 2020-10-24 14:18:17 +02:00
Hornwitser a614ef8405 Merge pull request #325 from Hornwitser/lint-2
Lint fixes 2
2020-10-24 14:00:41 +02:00
Hornwitser 6d005505ce Fix brace style in lib/prometheus 2020-10-23 21:22:45 +02:00
Hornwitser ceade431ef Convert dole code to follow project style 2020-10-23 21:22:45 +02:00
Hornwitser 8586dfb95f Fix capitalization of constructors
Enforce new-cap rule and fix capitalization and mistakes related to
constructors.
2020-10-23 21:22:45 +02:00
Hornwitser 00a1f6aea7 Remove variable shadowing
Refactor to remove all no-shadow warnings.
2020-10-23 21:22:45 +02:00
Hornwitser e5e82ba2fb Remove inventoryImports plugin
Remove defunct and coding horror inventoryImports.  The functionality
provided in it should be added as an optional extra to subspace_storage.
2020-10-23 21:22:45 +02:00
Hornwitser 24397b75cc Prefix clusterio/lib imports with lib
Use lib* prefix for the modules impored from the lib package to help
distinguish them more from ordinary variables.  Fix up the naming
convention for files to be consistently lowercase_underscore for
JavaScript files.
2020-10-23 21:22:43 +02:00
Hornwitser fbbaf3dd3a Alter no-mixed-operators rule
Change the rule for mixed operators to allow basic arithmetic and
boolean logic.
2020-10-23 19:05:12 +02:00
Hornwitser a84f2d5eb6 Use strict equality checking
Remove usage of == and != in favour of === and !==.
2020-10-23 19:05:12 +02:00
Hornwitser ee81fbb7f6 Merge pull request #323 from Hornwitser/plugin-restructure
Convert plugins to node packages
2020-10-23 19:04:17 +02:00
Hornwitser 65f0845398 Fix plugin modules not loading
Fix the path to plugin modules using the old plugins dir instead of
resolving to the actual path of the plugin, which in turn caused no
plugin modules to be loaded.
2020-10-23 18:13:13 +02:00
Hornwitser 7968d89345 Use peerDependency for plugins
Avoid any potential for accidentally installing multiple copies of
clusterio/lib by specifiying it as a peer dependency instead of a
regular dependency.
2020-10-23 17:01:43 +02:00
Hornwitser 0ae520b5cc Fix plugin add command incorrectly resolving path
Fix the plugin add command not storing the resolved path for relative
paths passed to it into plugin-list.json and incorrectly resolving any
path containing ./ or ../ instead of only paths starting with these.
2020-10-23 16:59:20 +02:00
Hornwitser 1b0b245484 Fix plugin version not being loaded
Load the version of the plugin from its package.json file and document
this requirement in the writing plugins document.
2020-10-23 16:57:49 +02:00
Hornwitser c5a14db8ea Fix path for export test
Fix the path tested for the export-data command test incorrectly using
the project root instead of temp/test.
2020-10-22 16:38:46 +02:00
Hornwitser 7bba6cbde6 Add basic error check in build_mod.js
Check if the additional file to add exists before trying to pack it.
Note that there may be other ways reading the files could break, but
this should suffice for most cases of getting it wrong.
2020-10-21 21:38:24 +02:00
Hornwitser b2408d54ec Convert plugins to node packages
Setup package.json for the plugins and add them back to tests now that
they are compatible again.
2020-10-21 21:37:26 +02:00
Hornwitser f06d3509e6 Move lua code to slave package and fix paths
Move the export and clusterio mod code to the slave package and fix the
path used for them.  Refactor tests to use temp/tests/ as the cwd when
running integration tests in order to ensure paths to lua code are
correctly handled.
2020-10-21 21:26:44 +02:00
Hornwitser 18e609f3fe Allow plugins to be installed as npm packages
Add a separate plugin-list.json file for storing which plugins are
available and the path to pass to require in order to load it.  This
allows plugins to be published and installed as npm packages.  And also
replaces the old method of installing plugins by putting them in the
plugins folder.
2020-10-21 19:35:24 +02:00
Hornwitser 2a7bc75929 Move generation of auth secret to master startup
Move the code generating the master authentication secret from the
inital value function of the master.auth_secret config entry to the
master startup.  Having the authentication secret generated in the
config library poses issues with using the config library in browser.
2020-10-21 17:58:30 +02:00
Hornwitser 34a0ce4dc3 Merge pull request #321 from Hornwitser/instance-status
Rework instance status
2020-09-29 11:34:55 +02:00
Hornwitser 003b035abd Send instance status in listInstances request
Send the status of the instance in the repsonse to the listInstances
request and use this status in the integration tests to check if the
instance was started/stopped.
2020-09-29 01:28:00 +02:00
Hornwitser b82de23876 Extend instance statuses
Add the statuses unknown and unassigned for when the status has not yet
been set by a slave and replace the initialized status with starting,
creating_save and exporting_data.
2020-09-29 01:01:19 +02:00
Hornwitser 363bb54902 Merge pull request #320 from Hornwitser/web-interface
Import new web interface
2020-09-28 18:09:41 +02:00
Hornwitser dfaf74aac5 Supress webpack asset size warning
Set threshold for webpack's asset size warning to 2 MB due to Ant Design
being large.
2020-09-28 05:57:10 +02:00
Hornwitser 86b8775df0 Fix modals stuck after web interface import
Fix the modals for adding new entries to instances, users, and roles not
being closable due to a missing bind of the close method.
2020-09-28 05:52:31 +02:00
Hornwitser bccc89fa0b Finish linting web interface
Fix remaining linter errors detected in web interface code.
2020-09-28 05:52:31 +02:00
Hornwitser 03b7d22ba6 Use consistent semi-colon placment
Add semicolons to web interface code to be consistent with project
standard.
2020-09-28 05:52:29 +02:00
Hornwitser 8dce1dd0fd Fix web interface indents
Use project standard of tabs for indents.
2020-09-28 05:51:15 +02:00
Hornwitser 94200433cc Serve interface on all routes for the interface
Add all routes the web interface is served on to the express app and
serve the index.html file on.  Due to the index html not knowing which
path it's hosted under, a path to the root of the web interface is
replaced using string substitution from the master server.
2020-09-28 05:51:11 +02:00
Hornwitser 392a7655c4 Use property for calculating handler name
Replace usage of .constructor.name with a separate property to avoid
issue with JavaScript terser mangling the class name during minification
for the web build.
2020-09-28 04:23:45 +02:00
Hornwitser f1f9b4d020 Close webpack middleware on shutdown
Call the .close method on the webpack-dev-middleware in order to
properly shut it down.
2020-09-28 04:23:45 +02:00
Hornwitser 701ff87bcd Implement webpack build for web interface
Set up webpack for the master server's web interface and fix the issues
from the imported interface source files.  The .jsx files in
package/master/web are bundled together into package/master/static and
served by the master server via express.
2020-09-28 04:23:45 +02:00
Danielv123 2382f4bbdc Import web interface from clusterio/clusterioWeb
Import the source files in use for the web interface, excluding the
copied and modified lib/link files.
2020-09-28 04:19:00 +02:00
Hornwitser f318f8826b Adapt config and link library to web
Make the config and link libraries compatible with packing for web with
webpack.  This would fail due to ws not being web compatible and config
using a node function not available in the web shim for util.
2020-09-28 04:19:00 +02:00
Hornwitser fbf2843f5b Remove old web interface 2020-09-22 22:06:03 +02:00
Daniel bca949f7be Merge pull request #318 from Hornwitser/restructure-project
Restructure project
2020-09-21 11:57:43 -07:00
Hornwitser 5e410a6ac3 Use full path when referencing files in documentation
Include the packages/ component in paths referenced in the documentation
to make it clear where the files are located in the repository.

[skip travis]
2020-09-21 19:57:52 +02:00
Hornwitser 0cca9ed22c Mark user creation tests as slow
Mark the "should not create the user if not instructed to" and "should
create the user if instructed to" tests as slow as these take more than
2 seconds to execute on travis.
2020-09-21 19:27:24 +02:00
Hornwitser 6621e37e23 Do not use ci in travis environment
Disable ci mode for lerna in travis environment as this requires
package-lock.json files to be commited to the repository and we do not
for the time being do this.
2020-09-21 18:51:21 +02:00
Hornwitser bd8f685a18 Fix JSDoc module references
Fix module names, which are incorrectly deduced by JSDoc, and references
to master, slave and ctl modules that are now in separate packages.
2020-09-21 18:00:07 +02:00
Hornwitser 3485c9ac68 Rename clusterctl to clusterioctl
Rename the clusterctl command line utility to clusterioctl in order to
be consistent with the naming of the commands for master and slave.
2020-09-21 18:00:07 +02:00
Hornwitser bf5a4a8a9c Split up into multiple packages with Lerna
Split up codebase into the packages @clsterio/lib, @clusterio/master,
@clusterio/slave, and @clusterio/ctl using lerna to manage them.  This
will allow deployment to use only the parts needed reducing install
size, and better separate the code into shared library, master, slave
and control.
2020-09-21 18:00:07 +02:00
Hornwitser 198d5c344b Remove unused dependencies from dead code
Remove dependencies used by inventoryImports, external plugins and the
removed modManager code.
2020-09-16 15:09:23 +02:00
Hornwitser efa3eff0d5 Delete old mod manager code 2020-09-16 15:09:23 +02:00
Hornwitser 6fde461da5 Merge pull request #313 from Hornwitser/list-management
List management
2020-09-16 15:08:21 +02:00
dependabot[bot] 14ddb76a0b Bump node-forge from 0.7.6 to 0.10.0 (#315)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 0.7.6 to 0.10.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/0.7.6...0.10.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-16 14:59:35 +02:00
Hornwitser 3753c303dd Add changelog entry for list synchronization
[skip travis]
2020-09-01 20:33:31 +02:00
Hornwitser 08ec542907 Move integration tests for lists sync
Move the integration tests for the admin, user, and ban list sync to be
done while an instance is running in order to hit more lines in the
coverage with them.
2020-09-01 20:16:17 +02:00
Hornwitser a8b8232d73 Merge pull request #314 from RMuskovets/readme-typo-factorio-version
Fix a typo in README.md
2020-09-01 19:17:56 +02:00
RMuskovets 4f86be1b7f Fix a typo in README.md:
Line 174, "Factorio_0.x.y" -> "Factorio.x.y.z"
Since 1.0.0 release Factorio versions no longer start from 0
2020-09-01 19:56:08 +03:00
Hornwitser 84450b83b4 Implement adminlist, whitelist and banlist sync
Add integration of the server adminlist, whitelist and banlist into
clusterio.  Membership is stored as booleans in the user acconuts and
changes are synchronized live to slaves which pass it on to instances
which in turn will update the running server's lists with RCON commands.
2020-09-01 18:17:40 +02:00
Hornwitser f08774bd4a Add execCtl shortcut for clusterctl tests
Add execCtl which is a shortcut to execute node clusterctl with the test
config set.
2020-09-01 18:17:40 +02:00
Hornwitser 5e5a47ce02 Leave out roles array in serialized user
Leave out the roles property containing the array of role ids if the
user does not have any roles.
2020-09-01 17:59:19 +02:00
Hornwitser 9761fbb71c Fix test names for clusterctl
Rename the test cases named after the old clusterctl commands for the
integrations to the new clusterctl commands.
2020-09-01 17:59:19 +02:00
Hornwitser 3c55188a87 Merge pull request #311 from Hornwitser/fix-logger-warning
Fix logger.warning is not a function

Fixes #303
2020-08-27 11:41:43 +02:00
Hornwitser a40c78d85d Fix logger.warning is not a function
Replace logger.warning with logger.warn as I incorrectly assumed the
warning method of console was called console.warning instead of
console.warn.
2020-08-27 11:31:36 +02:00
Arvids Godjuks 0b790357f9 Merge pull request #310 from Hornwitser/remove-bcrypt
Remove bcrypt dependency
2020-08-27 11:28:11 +02:00
Hornwitser 2dcc5c9d47 Remove bcrypt dependency
Remove the unused dependencies to bcrypt and bcrypt-promise.  These were
used by the playerManager plugin to implement password based
authentication, but this plugin is likely not going to be ported to 2.0.
2020-08-27 11:17:35 +02:00
dependabot[bot] 6d8d926268 Bump express-fileupload from 1.1.5 to 1.2.0 (#308)
Bumps [express-fileupload](https://github.com/richardgirges/express-fileupload) from 1.1.5 to 1.2.0.
- [Release notes](https://github.com/richardgirges/express-fileupload/releases)
- [Commits](https://github.com/richardgirges/express-fileupload/compare/v1.1.5...1.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-27 11:07:35 +02:00
Hornwitser c59c30b840 Merge pull request #304 from Hornwitser/clusterctl-restructure
Restructre clusterctl cli interface
2020-08-26 13:23:33 +02:00
Hornwitser fd307e58c2 Add global-chat shout command
Add command to broadcast a message to all instances from clusterctl.
2020-08-18 18:52:11 +02:00
Hornwitser da1cbc16f7 Restructre clusterctl cli interface
Turn the flat list of commands into a hierarchy of commands, converting
most required parameters into positional parameters and add plugin
loading support for clusterctl.
2020-08-18 18:52:11 +02:00
ToxicFrazzles 9b870fde64 Fix syntax error in plugins/subspace_storage/static/storage.js (#302)
Fix _lastData being used as a global causing the script to fail to load
after strict mode was added.
2020-07-08 12:51:16 +02:00
Hornwitser 0802c5b29d Merge pull request #301 from Hornwitser/custom-scenario
Add support for running custom scenarios
2020-07-08 12:46:49 +02:00
Hornwitser 781e40428f Add enable_save_patching to mock instance config
Handle the factorio.enable_save_patching config option in the mock
instance as these are queried by instances after 9458155.
2020-07-07 14:24:47 +02:00
Hornwitser 999386c088 Add --keep-open option to clusterctl
Add option to keep the console log open after starting an instance
instead of exiting as soon as it has finished starting up.
2020-07-06 13:16:57 +02:00
Hornwitser 8c6130adba Add load scenario command
Add alternative method of starting an instance by loading a scenario.
Since there's no save when starting a scenario it can only be used if
the save patching is disabled.
2020-07-06 13:10:26 +02:00
Hornwitser 9458155a1b Add option to disable save patching
Add factorio.enable_save_patching config entry to allow disabling the
save patching done on startup.  This is for running vannila games or
scenarios that are not compatible with Clusterio.
2020-07-04 16:45:42 +02:00
Hornwitser 0969e07d79 Merge pull request #300 from Hornwitser/lint
Lint codebase
2020-06-17 22:12:27 +02:00
Hornwitser 9c9b3ac625 Document running tests and linting
Add section to contributing docs detailing how to run tests and linting
on the project.
2020-06-17 21:08:45 +02:00
Hornwitser 7814656241 Add ESLint check to travis integration
Invoke the lint package.json script during travis integration check.
2020-06-17 20:59:37 +02:00
Hornwitser e9f5cae4f0 Fix misc problems detected by ESLint
Reformat and clean up issues detected by ESLint.
2020-06-17 20:59:37 +02:00
Hornwitser 8cb21c5fa8 Add dangling commas
Ensure multiline object and array litterals are consistenly typed out
with dangling commas for the last element.
2020-06-17 20:59:37 +02:00
Hornwitser 3e4f0b6b5e Fix indents
Fix incorrectly indented code as detected by ESLint.
2020-06-17 20:59:33 +02:00
Hornwitser 703fda43ad Fix JSDoc issues
Fix missing documentation for parameters and return types, incorect
config and errors in type definitions.
2020-06-17 20:53:18 +02:00
Hornwitser 3dba92a03d Use strict mode
Add strict mode directives to all js files.
2020-06-17 20:53:18 +02:00
Hornwitser b254e4189d Use more consistent spacing in objects literals 2020-06-17 20:53:18 +02:00
Hornwitser 29f066a191 Fix missing semicolons
Ensure all statements end with a semicolon.
2020-06-17 20:53:18 +02:00
Hornwitser 34aaeee354 Use double quotes
Convert existing mix of double quotes and single quotes to only use
double quotes unless the string contains double quotes.
2020-06-17 20:53:18 +02:00
Hornwitser efd759bec5 Add code linting using ESLint
Add ESLint as a dev dependency and configure it to match the preferred
style for the project.  The config options set to warn are tentative and
may not be sensible as they haven't been fully investigated yet.
2020-06-17 20:53:18 +02:00
Hornwitser 922c6af632 Merge pull request #299 from Hornwitser/user-management
User management and authentication rework
2020-06-17 20:47:52 +02:00
Hornwitser cf6f2cb70a Add user and permission system
Add users, roles and permissions and rework the authentication to use
separate user and slave tokens for control and slave connections
respectively.

Add commands for listing permissions, roles and users, and creating,
editing and deleting roles and users.
2020-06-15 16:47:15 +02:00
Hornwitser 2acf57d23d Fix erroneous delete of local variable
The delete operator only works on object properties, and causes the code
coverage to fail for the file in question if used on a local variable.
2020-06-15 00:13:25 +02:00
Hornwitser 07cff0ab9a Fix error during slave authentication failure
Treat sending messages over a link that is not connected as a
SessionLost error and handle AuthenticationFailed error in the
startup logic of slave.
2020-06-15 00:13:25 +02:00
Hornwitser 3dfed4d2ad Remove master.factorio_directory config
Remove old config entry for the old locale API that was removed with the
introduction of the new export system in 260f976.
2020-06-15 00:13:25 +02:00
Hornwitser c3179f0423 Move ControlConfig to lib/config/definitions.js
Move the definition of the control config to lib/config so that the
master code can use it.
2020-06-15 00:13:25 +02:00
Hornwitser 37bff10382 Merge pull request #298 from Hornwitser/config-event
Config events
2020-06-11 22:07:23 +02:00
Hornwitser 931ad4816f Update config of running Factorio instances
Monitor the factorio.settings config for changes and apply them to
running instances via the /config set command.
2020-05-31 22:28:51 +02:00
Hornwitser 5b043524b4 Implement setProp in configs
Add setProp method for selectively changing one property of an object
field in a config group.
2020-05-31 16:08:21 +02:00
Hornwitser 8c9daff0bb Add name shortcut for ConfigGroup instances
Add .name shortcut to get the groupName class property of config group
instances.
2020-05-31 14:16:06 +02:00
Hornwitser 67ca8dcc4d Implement config notification event
Add fieldChanged event to config objects that is triggered when the
value of a field in a group belonging to it is changed.
2020-05-30 14:27:59 +02:00
Hornwitser fc3686b865 Invoke plugin hooks via common function
Use a common function to iterate through and invoke the given hook in
every plugin, and ignore errors thrown by plugins from these hooks.
2020-05-30 14:27:59 +02:00
Hornwitser 01ea301a8c Merge pull request #297 from Hornwitser/automate-export
Automate export process
2020-05-26 18:13:17 +02:00
Hornwitser 36cf14074e Automatically upload export to master server
Upload the created export data zip file to a new /api/upload-export
endpoint on the master server which unpacks it into the appropriate
directory on the master.
2020-05-26 15:31:42 +02:00
Hornwitser f3d4cfdcf5 Add reference to Slave in Instance
Add a reference to the Slave that's running the given instance in order
for it to be able to access the slave config.
2020-05-26 14:04:25 +02:00
Hornwitser f8597f500c Remove license note for angels icons
Remove notice about license for the now removed modded angels icons.
2020-05-26 13:17:58 +02:00
Hornwitser 2b20c943bb Merge pull request #296 from Hornwitser/item-export
Implement icon and locale export

Closes #290
2020-05-26 13:16:11 +02:00
Hornwitser 1bddea8cec Remove Factorio icons
Remove no longer used Factorio icons.
2020-05-26 01:12:05 +02:00
Hornwitser e2255ddb7c Remove inclusion of 3625 Angels item icons
Remove obsolete inclusion of the 3625 Angels item icons added by
09f476e.
2020-05-26 01:12:05 +02:00
Hornwitser 453226f246 Implement locale trenslation of resource names
Translate the names of the resources using the exported Factorio locale.
2020-05-26 01:12:05 +02:00
Hornwitser 260f976fa3 Implement export of item icons and locale
Add mod and command for exporting the Factorio locale including mods,
and item icons assemled into a sprite sheet including mods.
2020-05-26 01:11:49 +02:00
Hornwitser d1b851d890 Move clusterio_lib to lua/clusterio_lib
Move the library mod to lua/clusterio_lib and restructure build_mod.js
to handle additonal files as a property to info.js instead of being hard
coded.
2020-05-25 20:45:09 +02:00
Hornwitser 2e24d6c8ae Merge pull request #295 from Hornwitser/fix-windows
Fix Windows stop logic
2020-05-25 17:58:54 +02:00
Hornwitser 203f2d2a14 Fix Windows stop logic
Handle the Factorio server not exiting when encountering an error on
Windows and fix the hang detection for .stop().
2020-05-21 18:06:20 +02:00
Hornwitser 87d14252fc Merge pull request #294 from Hornwitser/plugin-track
Integrate plugins closer with connections
2020-05-12 14:23:04 +02:00
Hornwitser 3211fbe985 Notify master about instance status update
Send updates to the master server when the running status of instances
change and add onInstanceStatusChanged hook to master plugins.
2020-05-11 18:10:11 +02:00
Hornwitser a8e29fdc56 Separate instance from config in master
Separate the instance object and the config that belongs to the instance
object in the instances db held in memory on the master.
2020-05-11 18:07:41 +02:00
Hornwitser a194508f52 Filter plugin messages forwarded to links
Drop events forwarded to a slave or instance that does not have the
plugin the event is defined in.  Reject requests for the same reason.
2020-05-11 18:04:03 +02:00
Hornwitser ca2839594e Timeout quikly when disconnecting existing slave
If a slave that is already connected attempts connecting the existing
connection for that slave is disconnected.  The most common scenario for
this is that the existing connection is stale and the slave is
attempting to connect agan.  However if the connection is stale then the
disconnection will take 15 minutes to time out and the slave will appear
unresponsive during this time.  Fix this problem by setting the timeout
to 15 seconds before disconnecting the existing connection.
2020-05-09 16:36:33 +02:00
Hornwitser 1b427af5f0 Fix client send buffer not being sent on ready
Fix WebSocketClientConnector not sending the send buffer queued up
between starting the connection and the master server sending the ready
signal for it.
2020-05-09 16:34:46 +02:00
Hornwitser 04ee3d3f96 Fix WebSocketClientConnector lifecycle events
Fix drop being repeatedly emitted while reconnecting and close not being
emitted on connection timeout.
2020-05-09 16:28:07 +02:00
Hornwitser c87538ca58 Handle connection being closed in slave
Listen for the close event and connect again if the connection was
closed from proper disconnect or initiate a shutdown if the connection
was unexpectedly closed.  Unexpected closure happens from authentication
failure and if a connection fails to reconnect before the timeout.
2020-05-09 16:24:02 +02:00
Hornwitser c35b6de70e Notify on connection events
Implement notification to plugins for disconnect request, connect, drop,
and close.  This allows plugins to properly handle slaves and the master
server shutting down without sending messages that will get lost.
2020-05-08 23:49:28 +02:00
Hornwitser b8b8c44083 Fix onExit not being invoked if plugin load fails
Fix instance not properly exiting if an error occurs during the loading
of a plugin.
2020-05-08 18:15:17 +02:00
Hornwitser 1beffb1bfd Fix crash on hartbeat on closing connection
Fix heartbeat incorrectly asserting it cannot be sent on a connection in
the closing state.
2020-05-08 18:09:48 +02:00
Hornwitser b151dd8469 Fix documentation
Fix missing module declarations, broken lings, broken formating and
incorrect scopes in the documentation.  For modules split over several
files it appears the only viable option is to use the memberof directive
on each documented member and only declare the index file as a module.
2020-05-08 17:28:06 +02:00
Hornwitser 7a9b6592e6 Handle disconnect in link integration test
Handle the prepareDisconnect request properly in the TestControl in
test/integration/link.js.
2020-05-07 13:23:11 +02:00
Hornwitser 5c1214ad38 Rename connected and dropped events
Rename the connector events connected and dropped to connect and drop
respectively to be consistent with common Node.js names for events like
close.
2020-05-07 12:36:43 +02:00
Hornwitser 1629eaf225 Track plugins loaded by master and slaves
Send a mapping of the plugins the master server has loaded to slaves
vice versa.  Limit the plugins that may be loaded by instances to only
those that the master server has loaded.
2020-05-06 20:14:40 +02:00
Hornwitser 4060c75142 Merge pull request #293 from Hornwitser/alpha-bugfixes-2
Alpha bugfixes 2
2020-05-06 20:13:22 +02:00
Hornwitser f3c13eab21 Simplify level logic
Simplify tech level logic according to TheAiBot's suggestion.
2020-05-06 19:49:33 +02:00
Hornwitser e9fab3cc10 Fix level > max_level crash in research_sync
Fix crash when technologiy level stored on the master is greater than
the max_level for the technology in Factorio.  This can happen with mods
that for example reduce the max_level from inifity to a finite level.
2020-04-24 15:27:30 +02:00
Hornwitser 446333f4aa Fix stylesheet loaded from wrong location
Fix stylesheet path not taking into account the web_root config.
2020-04-24 15:26:52 +02:00
Hornwitser f8817ecf0c Merge pull request #292 from Hornwitser/alpha-bugfixes
Alpha bugfixes
2020-04-19 23:32:10 +02:00
Hornwitser c601c53044 Fix typo in clusterctl.js
Fix SessionLost being refered to as a global instead of from the errors
module in clusterctl.js.
2020-04-19 18:24:43 +02:00
Hornwitser 3603b16901 Fix missing space in researched notification
Add space between the technology name and the level in the notification
for researched technologies.
2020-04-19 18:23:37 +02:00
Hornwitser f4ca8366e2 Handle contributions to the next level of a technology
Fix research_sync sending out contribution events for the previous level
of a researched technology and sending finished events for the wrong
level when synchronizing technologies.  Both of these issues are related
to failing to accunt for the unlocked level being one less than the
level stored when the researched property is false.
2020-04-19 18:20:28 +02:00
Hornwitser b6585e6364 Fix reconnect giving up too early
Fix the reconnect logic giving immediately if a previous reconnection
had been made long prior to the connection being dropped again.
2020-04-18 23:37:50 +02:00
Hornwitser d2fb33f8b9 Fix log spam by subspace_storage plugin
Fix log spam when the dole distribution method is used on
subspace_storage plugin.  This was caused by a bogus if condition being
translated into a sensible but wrong one.
2020-04-18 23:37:50 +02:00
MyNameIsTrez 568cf81204 Edit docs/writing-plugins.md (#287)
Fix grammar, punctuation and spelling mistakes in
docs/writing-plugins.md.
2020-04-18 23:34:26 +02:00
Hornwitser 15ad64e356 Fix web icons ignoring web_root (#291)
Fix the URLs for the item icons in the web interface ignoring the
web_root config option.

[travis skip]
2020-04-17 17:46:17 +02:00
MyNameIsTrez ef6441ca46 Edit docs/developing-for-clusterio.md (#285)
Fix grammar, punctuation and spelling mistakes in
docs/developing-for-clusterio.md.
2020-04-17 15:46:35 +02:00
Hornwitser 534408856c Merge pull request #284 from Hornwitser/small-things-2
Small things 2
2020-04-11 02:04:59 +02:00
Hornwitser 430aa6d207 Disable heap and memory size tests
Disable the tests for the memory and heap size metrics as the travis
environment is apparently using 3x as much memory as locally.
2020-04-09 20:44:06 +02:00
Hornwitser 00d1d304d2 Fix integration test certificate path
Fix integration tests using the default certificate paths instead of
having them located in the dedicated test database directory.  Also
increment key size to the minimum allowed of 1024.
2020-04-09 20:35:51 +02:00
Hornwitser 38c392ff6d Fix kill logic of integration test
Test if the process variable exists before trying to call kill on it, as
in certain failure situations it is not set.
2020-04-09 20:08:04 +02:00
Hornwitser 4f463a8180 Reduce number of bits in generated test cert
Add config entry for the number of bits to use in the auto generate TLS
certificate for the master server and reduce it to 512 for the
integration test.  This reduces the time needed to generate the
certificate in the integration test.
2020-04-09 20:02:29 +02:00
Hornwitser 2c855a5846 Add debug-dump-ws command
Add debug command to dump the WebSocket messages being sent and received
by the master server to aid in finding chatty events.
2020-04-09 19:50:01 +02:00
Hornwitser 61d92a6910 Rename master plugin onExit hook to onShutdown
Rename the onExit hook for master plugins to onShutdown to better
reflect when it is invoked.
2020-04-09 19:50:01 +02:00
Hornwitser 9507608e59 Rework disconnect logic for connectors
Rework the way disconnection at shutdown is done to properly close
the connections when the master server is shut down.
2020-04-09 19:50:01 +02:00
Hornwitser 47ea8f5477 Rename connection shutdown to prepare disconnect
Rename the shutdownConnection request to prepareDisconnect to better
reflect its actual purpose.
2020-04-09 16:17:38 +02:00
Hornwitser e7d1039942 Link to writings plugins document in docs
Replace notice about in development plugin interface with a link to the
writing plugins document in the developing for clusterio document.
2020-04-09 16:17:38 +02:00
Hornwitser fc04fd9ebc Remove master branch warning
Remove warning of master branch being broken now that it's no longer
completely broken.
2020-04-09 16:17:38 +02:00
Hornwitser 3dc4bdaab6 Remove debug log
Remove debug log statement annoing events have been broadcast.
2020-04-09 16:17:38 +02:00
Hornwitser 59111cc844 Disable replay in patched save
Ensure that the replay is disabled after the game is patched by calling
game.disable_replay().  It's not possible to replay a patched save, and
it also stores every command sent into save, causing it to quickly grow
into an unplayably large size.
2020-04-09 16:17:38 +02:00
Hornwitser d537e5cf6b Fix typo in default cpu metric
Fix proces_cpu_seconds_total being mislabled as process_cpu_seconds.
2020-04-09 16:17:38 +02:00
Hornwitser 47c221f6f3 Fix typo in test names
Fix the labels function being refered to as the label function in the
test descriptions.
2020-04-09 16:17:38 +02:00
Hornwitser 56488b2c2d Add resident memory and heap size metrics
Implement the default resident memory and heap size metrics.
2020-04-09 16:17:38 +02:00
Hornwitser e908c5069b Add autosave tracking and statistics
Detect when autosaves are made and track the size in a Prometheus gague.
2020-04-09 16:17:38 +02:00
Hornwitser 139585f8c6 Prevent metrics being called on createSave
Gate the onMetrics plugin hook behind a boolean that's set only after
the Factorio server has fully started, and reset when it stops.  This
prevents errors due to plugins trying to get metrics while the server
is creating a new save or stopped.
2020-04-09 16:17:38 +02:00
Hornwitser 6bc07ad1b3 Add Factorio CPU and Memory usage statistics
Add per instance metrics for CPU and memory usage with the pidusage
library.
2020-04-09 16:17:38 +02:00
Hornwitser 56ee1fe955 Rework save logic with regard to start instance
Ensure that notifyExit is called if errors occur during startInstance
and createSave request, and rework startInstance save logic to create
a new savegame if no save is present and allow specifying the save to
load.
2020-04-09 16:17:38 +02:00
Hornwitser 62e38199bd Rework exit notification for Instances
Pull the exit handler into its own method that's invoked if the startup
fails before the Factorio server is created.  Also move the metrics
clearing to the exit notification method to ensure it's also invoked.
2020-04-09 16:17:38 +02:00
Hornwitser 407e3d2aed Merge pull request #283 from clusterio/TheAIBot-patch-1
Fix overflow check in subspace storage
2020-04-08 15:08:32 +02:00
TheAIBot fcfad08260 fix overflow check in subspace storage
Previous and new value:
0x7ffffff -- old 134.217.727
0x7fffffff -- new 2.147.483.647
2020-04-08 14:59:53 +02:00
Hornwitser 112cad8a49 Merge pull request #282 from Hornwitser/small-things
Small things
2020-04-06 14:50:55 +02:00
Hornwitser ccb07d6eab Add support for Factorio 0.18.x
Add option to bump patch number and override factorio_version in the
generated mod and update the serialize library to support the inventory
bar access methods of both 0.17 and 0.18.
2020-04-06 14:41:51 +02:00
Hornwitser 621c834126 Merge pull request #281 from Hornwitser/reconnection-logic
Reconnection logic
2020-04-04 18:39:36 +02:00
Hornwitser 75cd6d38db Fix instance exit notification
Fix improper notification on Factorio server exit causing plugins and
the instance tracking to fail.
2020-04-04 15:14:23 +02:00
Hornwitser 99ac8f4f33 Add commands sent statistic
Track RCON commands sent.
2020-04-04 12:18:26 +02:00
Hornwitser 75067e992d Add WebSocket usage statistics
Add Prometheus metrics for messages sent, connections made, connections
rejected, active connections, and active slaves.
2020-04-04 12:16:57 +02:00
Hornwitser 31b66339a0 Remove Google Analytics
Remove hard coded Google Analytics integration in web interface.
2020-04-04 10:15:27 +02:00
Hornwitser 8ebf0fc754 Add master server Web Root config
Add web_root option to the master server and use it create URLs for
resources hosted by it.  This permits hosting the master server web
interface behind reverse proxy under a dedicated path.
2020-04-04 10:05:37 +02:00
Hornwitser 5e9d9097e1 Link mods when creating save
Ensure the mods are linked in when creating a new save.
2020-04-03 22:32:12 +02:00
Hornwitser 3343d53d75 Allow deleting unassigned instances
Handle delete instance requests for instances that are not assigned to a
slave.
2020-04-03 22:22:23 +02:00
Hornwitser 541d2ad337 Add missing changlog entry for 737c9924
Add entry to changelog noting that the clusterioMod plugin has been
renamed to subspace_storage.
2020-04-03 21:15:57 +02:00
Hornwitser 59758f1576 Rename plugin Prometheus metrics
Rename Prometheus metrics exported by plugins to follow the
clusterio_<plugin_name>_<metric_name> naming scheme.
2020-04-03 21:14:30 +02:00
Hornwitser 706ffd9ed2 Detail usage of single and double quoted strings
Add section to contributing guidelines detailing that single quoted
strings should not be used.  The use of it has only resulted in the
addition of more inconsistency to the codebase.
2020-04-03 20:51:28 +02:00
Hornwitser 17973e767e Generate API docs with JSDoc
Add npm docs script to generate api docs with JSDoc.
2020-04-03 20:51:28 +02:00
Hornwitser 5c16809b5b JSDoc use autodetected module name
Change @module <module name> entries to just @module as JSDoc is able to
automatically determine the module name.
2020-04-03 20:51:28 +02:00
Hornwitser ee063300ed Check result of rcon commands in subspace_storage
Validate that the commands sent don't error out by checking the returned
result is empty.
2020-04-03 20:51:28 +02:00
Hornwitser 3cc84d6400 Use basicType helper in lib/database
Simplify the checking logic in lib/database by using the basicType
helper.
2020-04-03 20:51:28 +02:00
Hornwitser 65494a0a91 Move basicType to lib/helpers
Move the basicType function from the config lib to a shared helpers lib.
2020-04-03 20:51:28 +02:00
Hornwitser f05100c94d Remove unused files
Remove left over files that are no longer in use.
2020-04-03 20:51:28 +02:00
Hornwitser 9a4a535add Expose Slave to instance plugins
Allow the slave object to be accessed by instance plugins through the
this.slave property.
2020-04-03 20:51:28 +02:00
Hornwitser 3ff5e630ca Warn on unhandled Factorio ipc
Send warning to console if an ipc message is received but there were no
handlers registered to handle it.
2020-04-03 20:51:28 +02:00
Hornwitser c2df80a17f Move test/json to test/file/json
Move the json test files for the lib/database tests to test/file/json to
be consistent with the location of test files for other tests.
2020-04-03 20:51:28 +02:00
Hornwitser f9f0e5b5f7 Move test/temp to temp/test
Move the temporary files generated by the the tests to the top level
directory temp.
2020-04-03 20:51:28 +02:00
Hornwitser 028f00e58a Rename client.js to slave.js
Rename the client.js script to slave.js, and changed places where it's
refered to as client into slave.
2020-04-03 20:51:28 +02:00
Hornwitser ae86781b0d Add support for multiple Factorio installs
Allow the factorio_directory specified in the slave config to contain
multiple Factorio installs and add factior.version instance option to
select which install to use for the given instance.
2020-04-03 20:51:28 +02:00
Hornwitser 4c26fcee3a Fix connection shutdown logic
Fix the WebSocket link not being properly teared down on the master
side causing the handshake to deadlock when the slave tried to connect
again.
2020-04-03 20:44:20 +02:00
Hornwitser d4e072f376 Rewrite reconnection logic
Implement proper session tracking and resume on reconnect for the
WebSocket server.
2020-04-02 21:58:44 +02:00
Hornwitser 026b7f1306 Fix missing onExit calls
Add missing calls to instance plugin onExit hooks.
2020-04-02 19:56:39 +02:00
Hornwitser 8de3f54bc4 Move slowTest into test/integration/index
Deduplicate slowTest into the shared integration test library.
2020-04-02 15:24:32 +02:00
Hornwitser 43ce6aa58c Pass validation errors with InvalidMessage error
Pass the array describing the validation errors occuring with the
InvalidMessage error throw and print it to the console.
2020-04-02 14:39:18 +02:00
Hornwitser 0a9fff7578 Merge pull request #280 from Hornwitser/brand-as-alpha
Brand library mod as alpha
2020-03-23 21:22:41 +01:00
Hornwitser b73f2f6006 Brand library mod as alpha
Add alpha notice to the title and description for the Clusterio Library
Factorio mod.
2020-03-23 21:11:34 +01:00
Hornwitser 277b78bf6d Merge pull request #279 from Hornwitser/ditch-socket-io
Ditch socket.io
2020-03-23 02:49:31 +01:00
Hornwitser 1aef3c9609 Do not use shell for spawned integration nodes
When using the shell for spawning the master and slave integration
processes the shell becomes the process that is returned by the spawn
command.  This in turn means that the SIGINT that was supposed to be
sent to the master and slave processes got ignored by the shell instead.

Fix by spawning the node process directly instead of through a shell.
2020-03-23 02:38:40 +01:00
Hornwitser 9f74d7b2ec Fix shutdown logic for slave
Shutdown running instances and close the connection to the master server
on slave stop.  Remove call to process.exit on shutdown as it's no
longer needed.
2020-03-22 03:30:57 +01:00
Hornwitser 7aa340675b Gracefully shut down integration master and slave
Use SIGINT instead of the default SIGTERM when signalling to shut down
the integration master and slave subprocesses.  This should let nyc
properly write the coverage report before it exits.
2020-03-22 02:39:20 +01:00
Hornwitser 27ef00705b Set content type for /api/inventory
Set the correct content type to JSON to for the /api/inventory endpoints.
2020-03-22 02:10:54 +01:00
Hornwitser 38c60c221f Use sub-processes in integration testing
Spawn master, slave, and clusterctl in separate processes in the
clusterio integration tests and leave the test master and slave process
available for the whole test duration to allow plugins to use them as
well.  This removes the need for reimplementing the startup logic in the
test code and makes the existing startup code covered by tests.
2020-03-22 02:10:54 +01:00
Hornwitser 677512512e Replace socket.io with plain WebSocket
Replace socket.io with plain WebSocket using the ws library as it
doesn't really provide any useful tools over plain WebSocket when it
comes to implementing reliable communication that works across
reconnects.  The widespread browser support for WebSocket also means
it has little value to add for the web interface over WebSocket.
2020-03-22 02:10:54 +01:00
Hornwitser 808bd6b453 Validate messages sent
Validate messages sent over links with the json schema provided from
them.  This aids debugging as an exception will be thrown from the code
sending the bad content.
2020-03-22 02:10:54 +01:00
Hornwitser 941ceab935 Merge pull request #278 from Hornwitser/plugin-rewrite
Plugin rewrite and code improvements
2020-03-21 19:38:46 +01:00
Hornwitser 9dac57b36b Port Research Sync plugin
Rewriwe the researchSync plugin into plugins/research_sync.
2020-03-17 01:31:51 +01:00
Hornwitser 23be732a5c Merge pull request #277 from Hornwitser/config-rewrite
Initial config system rewrite
2020-03-17 00:52:49 +01:00
Hornwitser 737c9924b9 Port and rename clusterioMod plugin
Port the clusterioMod plugin to the new plugin system under the new
subspace_storage name.
2020-03-13 22:46:17 +01:00
Hornwitser d9fe408c42 Fix master web interface
Fix the breaking changes to the plugin struture and remove slave
information that is no longer present.
2020-03-13 22:46:17 +01:00
Hornwitser 090fbd6dff Expose master interfaces to plugin
Expose Express.js app, master config, instance and slave database, slave
connections and metrics to the master plugins.
2020-03-13 22:46:17 +01:00
Hornwitser cb1d744060 Handle errors during master plugin initialization
Catch errors thrown when constructing or initializing master pluigns and
wrap them in PluginError to ensure that they are properly reported as
plugin errors.
2020-03-13 22:46:17 +01:00
Hornwitser ddd2027739 Handle errors during instance startup
Catch errors and shut down the instance in question if the startup
failed.  This happens if for example the Factorio server errors out on a
script during startup.
2020-03-13 22:46:17 +01:00
Hornwitser 6a8346186f Implemented forwarding of requests to the master
Implement the forwardTo function for requests to the master server.
2020-03-13 22:46:17 +01:00
Hornwitser 5681ef9733 Fix plugin enabled config being inverted
Fix plugins being loaded when enable was set to false, and not loaded
when set to true.
2020-03-13 22:46:17 +01:00
Hornwitser 6f22942cc5 Fix incorrect instance lookups in plugins
Fix mistaken lookups missing the instance part, or trying to get the id
from the removed instance.id property.
2020-03-13 22:46:17 +01:00
Hornwitser 1e6b807ae3 Add Clusterio Library mod
Add a library mod for wrapping the Clusterio remote API and providing
related utilities.  This eases development of Clusterio related Lua code
by providing the same interface for both mods and modules and also makes
the serialize library available without having to duplicate it.

It is not possible to implement this through save patching as mods
cannot load Lua code from the level code like they can with code from
other mods.
2020-03-12 15:39:25 +01:00
Hornwitser 5ef6d02054 Add serialize library to clusterio module
Add library for serializing items and inventories to the clusterio
module.  This will provide a common codebase for plugins like
playerManager and trainTeleports to use in order to serialize
inventories to the master server.
2020-03-10 10:57:24 +01:00
Hornwitser eac17b4340 Remove reference to pm2
Remove the instructions for installing and using pm2 with clusterio, as
I don't know how to use this tool properly and can't keep the README up
to date on it.
2020-03-09 20:53:01 +01:00
Hornwitser 54224755e9 Add missing chalk dependency
Add chalk as a dependency as it's used in clusterctl.js.
2020-03-09 20:50:58 +01:00
Hornwitser 557d380112 Fix broken link to modules/clusterio/api.lua
Fix broken link to in docs/developing-for-clusterio.md to
modules/clusterio/api.lua.
2020-03-09 20:48:21 +01:00
Hornwitser 5e5149dc93 Implement new configuration system
Replace current configuration mess with a trimmed down version of the
original vision of #200.  Configs are collection of config groups that
have fields that can be set.  These groups allow organizing the fields
as well as opertunity for plugins to add their own groups.  By default
plugins have an auto generated group with the enabled field.
2020-03-09 20:48:13 +01:00
Hornwitser 370ee9ed04 Unminify formatBytes
There's no reason to minify formatBytes it saves nothing.  Use the
unminified version as published.
2020-01-14 08:09:39 +01:00
Hornwitser f4a0b88ff6 Remove remoteCommands plugin config
Remove the remoteCommand specific config allowRemoteCommandExecution.
The plugin has been removed and with the move to master managing the
instances/slaves it makes no sense to deny remote commands.
2020-01-14 08:09:39 +01:00
Hornwitser e137547664 Remove obsolete msBetweenCommands config
Remove the no longer used msBetweenCommands config.  The RCON connection
was changed to be limited to 5 concurrent commands instead.
2020-01-14 08:09:39 +01:00
Hornwitser d5c43cc3d6 Remove clusterioMod specific configs
Remove configuration options in the master config that's specific to the
clusterioMod plugin.
2020-01-14 08:09:39 +01:00
Hornwitser 459c039f17 Merge pull request #276 from Hornwitser/module-rework
Module rework
2020-01-07 16:16:25 +01:00
Hornwitser 892c803496 Add on_server_startup event
Add startup event to the Clusterio module API which is raised at startup
of a Factorio server.  This is to server as a replacement for the
on_init and on_configuration_changed events which cannot be used in
modules.

Adds clusterio_patch_number global from the patcher in order to
implement this.  This is a number that's incremented every time the save
is patched.
2020-01-05 23:19:34 +01:00
Hornwitser b8fb73f863 Add Factorio to Clusterio communication API
Add send_json to the Clusterio module's API which allows sending
arbitrary tables serialized as JSON to plugins that listen on the
channel specified.
2020-01-04 17:34:16 +01:00
Hornwitser 6cf16cc09e Reference by global in statistics_exporter
Store export function in a global table in the lua module for statistics
exporter in order to avoid a remote.call.
2020-01-04 17:34:16 +01:00
Hornwitser 0e9cd35c30 Add table of contents to Writing Plugins
Add table of contents for the Writing plugins document.
2020-01-04 17:34:16 +01:00
Hornwitser 66aa7e1e43 Add clusterio module with API for lua code
Add a shared API for lua code through a module named clusterio.
2020-01-04 17:34:16 +01:00
Hornwitser 221dc855f4 Add option to throw on non-empty RCON responses
Add option to FactorioServer.sendRcon to throw on non-empty responses
from Factorio.  This is useful for detecting errors returned by commands
that should not have a response.
2020-01-04 17:34:16 +01:00
Hornwitser f88e1f1d3a Remove dead code left over from 21d9fdf
Remove compatConfig definition after RCON is established which was left
over from 21d9fdf.
2020-01-04 17:34:16 +01:00
Hornwitser c091dc341e Implement require option for modules
Implement option allowing files to be required from control.lua be
specified in modules.json.
2020-01-04 17:34:16 +01:00
Hornwitser e89e54ee27 Implement dependencies specification for modules
Implement dependency ordering and version checking for modules.
2020-01-04 17:34:16 +01:00
Hornwitser 80bd75ba16 Rework module loading
Change module loading to use a module.json file describing the module
and patch in all files in a directory path passed to the patcher instead
of having to pass the files names and their content.  This allows the
specification of which files should be passed to the event_handler lib
and an easy implementation of loading stand alone modules.
2020-01-04 17:34:16 +01:00
Hornwitser 11992d1839 Merge pull request #275 from Hornwitser/statistics-fixes
Statistics plugin test and minor fix
2020-01-04 17:33:48 +01:00
Hornwitser 610859eb3b Improve error message from statistics_exporter
If the JSON parse failed it will usually be because Factorio returned an
error message instead of JSON, pass this along for easier debugging.
2019-12-29 04:44:16 +01:00
Hornwitser 37d8eedc0d Add tests for statistics_exporter plugin
These were already made at the time I ported the plugin but somehow I
forgot to add them to the statistics rework commit (26eefb0) :/.
2019-12-29 04:42:13 +01:00
Hornwitser 9d2e89ec63 Merge pull request #274 from Hornwitser/document-version-requirement
Document version requirement
2019-12-29 04:25:32 +01:00
Hornwitser 1b61d09a07 Add docs section on supported versions
Add section in the contributing docs about which version of Node.js and
Factorio is supported.

[skip travis]
2019-12-27 06:26:22 +01:00
Arvids Godjuks ac3aa5ffee Merge pull request #271 from Hornwitser/remove-old-stats
Remove old statistics code
2019-12-25 12:02:18 +02:00
Hornwitser b2a902f784 Update Node.js requirement documentation
Update Node.js version specified in README.md to be explicit about which
versions below 12 is supported, and use version 12 by default.
2019-12-23 23:53:40 +01:00
Hornwitser f700e98e8e Remove old statistics code
Remove left over code from the old statistics gathering and display
which was replaced with Prometheus+Grafana.
2019-12-22 00:54:45 +01:00
Hornwitser 770c961900 Merge pull request #270 from Hornwitser/plugin-rewrite
Plugin system rewrite
2019-12-21 00:49:02 +01:00
Hornwitser 1873f9bda8 Remove UPSdisplay plugin
Remove the redundant UPSdisplay plugin.  UPS statistics is gathered by
the statistics_exporter plugin.
2019-12-20 21:26:29 +01:00
Hornwitser 26eefb0476 Rewrite statistics export
Add automatic export of metrics from slaves to the master server.  And
rewrite the statistics export plugin to use this new system.
2019-12-20 21:26:29 +01:00
Hornwitser 8229d1955c Skip broken Prometheus client tests
Skip Prometheus client tests that assume the environment is isolated
from the rest of the tests.
2019-12-20 19:14:35 +01:00
Hornwitser b4c7824101 Add homebrew Prometheus client
Add an implementation of a Prometheus client.  This is because the
prom-client implementation seems to have missunderstood how Prometheus
is supposed to work and doesn't allow updating collectors via
asynchronous callback.
2019-12-20 19:14:35 +01:00
Hornwitser 1f6574a89c Improve master shutdown
Close active connections and http servers during master shutdown and let
node exit on its own.  If this fails with an error set output streams to
blocking and report it then shutdown with process.exit.

Also add double SIGINT handling to immediately shutdown.
2019-12-20 02:37:27 +01:00
Hornwitser 8b9bb59357 Fix fast-test
Fix incorrect locations for slowTest in clusterio integration tests and
add slowTest to the patch integration tests.  Fixes fast-test being
broken.
2019-12-14 17:53:34 +01:00
Hornwitser b71c392b32 Port globalChat to new plugin system
Port the globalChat plugin to the new plugin system.  Since there's no
config system yet the config options for it is removed.
2019-12-14 17:51:54 +01:00
Hornwitser bcceae687f Reimplement plugin system
Implement a new plugin system based subclassing pre-defined base clases
and communicating with pre-defined link messages.
2019-12-14 17:48:56 +01:00
Hornwitser 513651393b Implement forwarding to instances for events
Implement forward to instance for Event messages.
2019-12-14 17:31:02 +01:00
Hornwitser e5115df28b Implement broadcast for events
Add broadcastTo property to events that broadcasts them to all entities
of the specified target that's downstream to the connection it was sent
to.
2019-12-13 22:37:58 +01:00
Hornwitser 21d9fdff7b Remove old plugin system
Remove the old client/instance fused plugin system along with
instanceManagement in client.js and getPlugins in master.js.
2019-12-13 22:37:49 +01:00
Hornwitser 22c325394f Merge pull request #269 from Hornwitser/link-refactor-2
Another link refactor
2019-12-13 22:29:57 +01:00
Hornwitser ca21ff741b Add changelog entry for communication rewrite
Add entry to changelog detailing the rewrite of the communication layer.
2019-12-13 21:57:05 +01:00
Hornwitser d4dd290654 Add communication layer docs
Add breif description of the link, connector and messages communication
concepts implemented by lib/link.
2019-12-13 21:57:05 +01:00
Hornwitser f0afbf0a33 Complete test coverage for lib/link/link.js
Add tests for the remaining uncovered lines in lib/link/link.js.
2019-12-13 21:57:05 +01:00
Hornwitser 5dd1490c75 Fix clusterctl error on exit
Fix code still using the close method on the Link class which was
removed in 3bccbac.
2019-12-13 21:57:05 +01:00
Hornwitser 9c8ae2bf06 Remove debug logging
Remove left over verbose debug logging.
2019-12-13 21:57:05 +01:00
Hornwitser 5d8fa4e4ce Refactor instance communication
Change Instance class to be a subclass of Link and refactor the
communication to and establisment of instances to use a virtual
connection between the slave and the instance.  This is simplifies the
implementation of plugin communication for instances.

Replace InstanceRequest with a forwardTo option to Request which does
the task of forwarding in a much cleaner way.
2019-12-13 21:57:05 +01:00
Hornwitser 9f24d200b7 Ignore config in top level directory only
Change .gitignore to only ignore the config files in the top level
directory of repository.  If these files exist elsewhere it's likely
either intended or an error.
2019-12-13 21:57:05 +01:00
Hornwitser 47cff048f0 Improve link error messages
Reverse the reversal of source/target in message errors and add link
source and target to the error given when a validator is missing on a
link.
2019-12-13 21:39:54 +01:00
Hornwitser b24778dce6 Add filter argument to getNewestFile
Add filter predicate to allow filtering out files that are not desired
from getNewestFile.  This is useful for filtering out .tmp.zip files.
2019-12-13 21:39:54 +01:00
Hornwitser 62afcaadab Use shared StartupError class
Remove separate StartupError class definition in master.js in favor of
the shared StartupError class in lib/error.js.
2019-12-13 21:39:54 +01:00
Hornwitser b59fd80a7b Merge pull request #268 from Hornwitser/port-1.2.4
Port 1.2.4 fixes
2019-12-13 21:37:17 +01:00
Hornwitser 7a03b84858 Port fix bcrypt failing to install on Windows
The maintainers of bcrypt have in their infinite wisdom decided to stop
distributing Windows binaries.  Their inner workings remain a mystery.

This is a port of 5fe98a0.
2019-12-11 02:14:00 +01:00
Survaaki a2d10c8c02 Add changelog entry from 1.2.4
Add changelog entry for the change in the removed master mod uploading.
2019-12-11 02:14:00 +01:00
Hornwitser 3e7f497669 Port remove obsolete flow statistics from clusterioMod
Remove the obsolete and broken statistics gather from the clusterioMod
plugin.  The statisticsExporter plugin already exports all of the
statistics (and more) that was exported here.

This is a port of 3e7d4a8.
2019-12-11 02:14:00 +01:00
Hornwitser f1ac41bc4c Port fix researchSync updating researched technologies
Add check in filter_updated_techs that ignores already researched
technologies.

This is a port of b0eb188.
2019-12-11 02:14:00 +01:00
Hornwitser b2239aeb96 Add changelog entry from 1.2.4
Add changelog entry for the removal of the combinator signaling which
was first implemented in the master branch.
2019-12-11 02:13:57 +01:00
Hornwitser 34d4604bab Merge pull request #265 from Hornwitser/link-refactor
lib/link refactor and plugin preparations
2019-11-30 00:53:42 +01:00
Hornwitser ea4f3ab2bf Remove uncoloured formatting of factorioOutput
Since chalk autodetects the color support in the terminal there is no
need for a non-coloured version of formatOutput.
2019-11-30 00:08:10 +01:00
Hornwitser c6fd46e3c3 Replace sources/targets with links in messages
Replace the sources/targets concept with a links array in the message
definitions.  Fixes the problem of the actual links the messages applies
to being ambigious when there's multiple sources and targets.
2019-11-30 00:08:10 +01:00
Hornwitser 3be264c9a9 Add integration testing of commands
Add testing of the clusterctl commands against the master and slave
code.
2019-11-30 00:08:10 +01:00
Hornwitser b24c129e38 Improve formating for instance output
Use colours instead of dim in the coloured output from clusterctl as not
all terminals support dim.
2019-11-30 00:08:10 +01:00
Hornwitser f5d2b058d9 Fix deleteInstance returning early
Fix deleetInstance returning success before the instance have actually
been deleted.
2019-11-30 00:08:10 +01:00
Hornwitser 3f86011db1 Disable instance plugins
Disable the completely broken instance plugins as they break the
integration tests.
2019-11-30 00:08:10 +01:00
Hornwitser f8aa947a98 Add tests for resolveInstance
Add tests for the resolveInstance function in clusterctl.
2019-11-30 00:08:10 +01:00
Hornwitser bfff297d9a Add tests for clusterctl's output formatting
Add tests for formatOutput and formatOutputColored by using the same
lines as the line parser uses for testing.
2019-11-30 00:08:10 +01:00
Hornwitser 3bccbac496 Refactor to a connector based concept in lib/link
Remove connection and client subclasses of Link and put the connection
logic into a "connector" class.  The connector deals with sending
and receiving messages, handling the connection and reconnection of lost
connections and giving sequence numbers to messages.

This move the logic of the actual connection away from the link
sub-classes, which in turn means the connection should be a lot easier
to mock in tests.
2019-11-30 00:08:10 +01:00
Hornwitser a3eb39712c Make FactorioServer.exampleSettings static
Convert the exampleSettings method to be a static method.
2019-11-30 00:08:10 +01:00
Hornwitser 039b41d3e6 Move events and requests into messages
Merge the events and requests categories in lib/link into messages and
use the class name of the message to determine which handler to call.

Also swaps the order of the arguments for forwardInstanceRequest to be
consistent with the rest of the lib/link code and fixes up the docs for
requests and events.
2019-11-30 00:08:10 +01:00
Hornwitser 278dc9b2d2 Remove empty instanceManager file
Remove the empty/incomplete instanceManager implementation.
2019-11-30 00:08:10 +01:00
Hornwitser 2d3307c045 Remove unused binary option from plugin configs
Remove the unimplemented "binary" option from all plugin config files.
Setting this option to something other than "nodePackage" would
supposedly run a binary and feed it with the stdout of the factorio
proccess, but that was never implemented.
2019-11-30 00:08:10 +01:00
Hornwitser 5228e0a4d7 Merge pull request #261 from Hornwitser/daemonize
Daemonize Slaves
2019-11-25 23:15:15 +01:00
Hornwitser d9910b0750 Merge pull request #263 from Hornwitser/contributing-docs
Add contributing instructions
2019-11-25 23:13:53 +01:00
Hornwitser 1dea5fddbe Forward Factorio logs to clusterctl
Add instance output event that is forwarded by the master server to
control connections that are subscribing to them.  Add code subscribing
to the relevant instance output when dealing with instances in
clusterctl.
2019-11-23 15:18:20 +01:00
Hornwitser f2c3006046 Remove obsolete fileOps code
Remove sync functions that should not be used as well as of functions
that provide functionality that is already implemented by fs-extra.
2019-11-23 15:18:20 +01:00
Hornwitser 554cab4fb0 Add SIGKILL detection to lib/factorio/server
Check if Factorio terminated as a result of SIGKILL and throw an error
suggesting the system may be low on memory as a cause.  Also print exit
code if the unexpected shutdown occured without a signal.
2019-11-23 15:18:20 +01:00
Hornwitser 7a2abaa5db Daemonize slave
Rework client.js to support running multiple instances at once and be
managed remotely and implement the neccessary communication
infrastructure for this.

Due to circular dependencies on the implementation of this the master
branch is significantly broken after this.
2019-11-23 15:16:47 +01:00
Hornwitser 5858d97857 Remove playerManager specific CLI tool
Remove the playerManager plugin specific CLI tool delete_players.js
2019-11-21 22:25:15 +01:00
Hornwitser bc41f7afa6 Clean up generateSSLcert test
Remove dependency to fileOps in generateSSLcert test file and clean up
the code to follow a higher standard.
2019-11-21 22:25:15 +01:00
Hornwitser 65f75d86e6 Add id based database load and save functions
Add functions to save and load a Map of objects indexed by the id
property in the objects.
2019-11-21 22:25:15 +01:00
Hornwitser fcae3ef0c1 Use detected Factorio version
Remove Factorio_version from the config file and use the detected
version of Factorio instead.
2019-11-21 22:25:11 +01:00
Hornwitser 7b2ba58701 Add contributing instructions
Add instructions detailing the recommended git workflow as well as some
code style guidelines.
2019-11-21 17:11:57 +01:00
Hornwitser 27fc68da0f Remove remoteCommands plugin and interface
Remove the half baked remote rcon command interface in preparation for a
better version based on a new socket.io communication layer.
2019-11-20 22:37:25 +01:00
Hornwitser ca2967e255 Fix clusterioMod item autosaving
Fix the autosave code being broken by the move in e4aae88.
2019-11-20 22:37:25 +01:00
Hornwitser d38e9a53c4 Remove broken combinator signaling
Remove badly implemented and broken remote combinator signaling.
2019-11-20 22:37:25 +01:00
Hornwitser d2d058e7d6 Remove the broken serverManager plugin
The remote management interface will completely change with the
implementation of #201.
2019-11-20 22:37:25 +01:00
Hornwitser 2f3b4c1aa8 Remove redundant API call in globalChat
Remove call to getInstanceName which in turn calls the rather expensive
/api/slaves in the globalChat plugin.
2019-11-20 22:37:25 +01:00
Hornwitser f3171c2feb Remove support for uploading mods to master
Remove the uploadMod and checkMod APIs as these have become more or less
obsolete after Factorio implementd syncing mods to the server in game.
2019-11-20 22:37:23 +01:00
Hornwitser 1d26212e1d Remove port and password arguments to client.js
Remove undocumented command line options set the ports and password used
in the instance.  These are not compatible with remote management.
2019-11-20 22:36:14 +01:00
Hornwitser 0474355175 Fix README port from 1.2.3
Fix the new instructions for installing clusterio on Windows not being
corrected to install the master branch when it was ported over.
2019-11-20 22:34:10 +01:00
Hornwitser ba7626ff2d Merge pull request #255 from Hornwitser/port-1.2.3-fixes
Port 1.2.3 fixes
2019-11-18 01:54:14 +01:00
Hornwitser 707f06be76 Port update Windows install instructions
Remove the reference to the out of repo Windows install script and list
updated commands to install Clusterio stable in the readme itself.

This is a port of e1771ec.
2019-11-15 15:44:22 +01:00
Hornwitser 35f3f4f02b Add changelog entry from 1.2.3
Add the fixed Hotpatch changelog entry from 1.2.3.
2019-11-15 15:41:11 +01:00
Hornwitser 31ab3a09f6 Port add fileReadDelay to inventoryImports config
Fix the inventoryImports client side code never receiving the script
output by specifying fileReadDelay and thereby getting the old
behaviour.  This broke with the introduction of the tail mode script
output mode which requires a newline to be written at the end of
the output.

This is a port of d810b56.
2019-11-15 15:39:43 +01:00
Hornwitser 7c79ea44b6 Port fix inventoryImports command
Fix the inventory imports command trying to count up the removed quick
bar slots as well as using player_main instead of character_main.

Add warning that the lua commands file containing the unminified vesion
of the command has not been kept up to date.  There are multiple changes
that has been done previously so these have already diverged.

This is a port of 10414eb.
2019-11-15 15:39:20 +01:00
Hornwitser 7359f9075e Port increase limit of Json payloads to master server
The default limit of 100kb to JSON payloads of bodyParser can be
exceeded when running modded games with researchSync.  Increase the
limit to 10mb as a bandaid to fix this issue.

This is a port of 25eb5e5.
2019-11-15 15:38:56 +01:00
Hornwitser f4b2300d9b Port disable uploading of mods to master by default
Set uploadModsToMaster to false in config.json.dist.  This feature is
not necessary as the integrated mod portal in Factorio provides a better
method to synchronize mods.

This is a port of 3c5b108.
2019-11-15 15:38:22 +01:00
Hornwitser 680d196584 Merge pull request #251 from Hornwitser/server-robustness
Improve handling of FactorioServer.stop()
2019-10-28 22:53:10 +01:00
Hornwitser bdf8cd1152 Kill Factorio process if it's dected to hang
Send SIGKILL instead of SIGTERM when it's dectected that Factorio hangs
on shutdown.  This should hopefully resolve the travis ci hanging
issuse.
2019-10-28 22:30:57 +01:00
Hornwitser 246a8031c1 Add hang detection to FactorioServer.stop()
Attempt to detect if the Factorio server has hanged in
FactorioServer.stop() and send a second SIGTERM if this is the case.
2019-10-28 21:58:54 +01:00
Hornwitser a781c2f988 Wait for RCON connecting on FactorioServer.stop()
If the RCON client is currently in the process of connecting to the
Factorio server then wait for it to complete the connection to avoid it
emmiting out an error connecting in FactorioServer.stop().
2019-10-28 21:58:05 +01:00
Hornwitser 9082b40abd Update rcon-client
Update rcon-client to 4.2.0 in order to remove workaround for issue with
rcon-client 4.1.0 that has been resolved.
2019-10-28 21:54:19 +01:00
Hornwitser f163e6a39b Merge pull request #250 from Hornwitser/code-cleanup
Code cleanup
2019-10-28 20:24:50 +01:00
Hornwitser 283d47d88b Debug ci failure
Output factorio integration log to the travis log.
2019-10-28 20:04:30 +01:00
Hornwitser be02e57012 Remove mkdirp dependency
Replace usage of mkdirp with fs-extra.mkdirp and remove the mkdirp
dependency and the unused mkdirp-promise dependency.
2019-10-28 19:46:22 +01:00
Hornwitser cb8d7e240f Remove more remanent remote map code
Remove JavaScript code exported on the web interface that relates to the
removed remote map plugin.
2019-10-28 19:46:22 +01:00
Hornwitser 6f5534c45b Test for numeric strings in addItem and removeItem
Add tests asserting that numeric strings passed to the addItem and
removeItem methods to ItemDatabes throws.
2019-10-28 19:46:22 +01:00
Hornwitser 467249579d Add missing changelog entry for f4ca5d9
Add note in the changelog describing the feature added by f4ca5d9.
2019-10-28 19:46:22 +01:00
Hornwitser 3fdc27238b Make _doleDivisionFactor local
Turn _doleDivisionFactor into a variable local to the module instead of
implicitly global.
2019-10-28 19:46:22 +01:00
Hornwitser 72b16de227 Remove logging from averaged-timeseries
The logging implemented in averaged-timeseries consists solely of
printing "Logging enabled!".  Remove this pointless logging.
2019-10-28 19:46:22 +01:00
Hornwitser 9a2b2c9f8d Speed up generateSSLcert test
Significantly speed up the generateSSLtest by reducing the bits in the
key from 2048 to 512.
2019-10-28 19:46:22 +01:00
Hornwitser e4aae88614 Move clusterioMod specific code into the plugin
Move item database, tests, endpoint definitions for /api/place,
/api/remove, /api/inventory, /api/inventoryAsObject, and the web
interface view for the storage page into the clusterioMod plugin.
2019-10-28 19:46:22 +01:00
Hornwitser bc444b79c2 Remove broken client download command
Remove the client download command which would download pre-releases.
The mod add command is a complete replacement for it.
2019-10-28 19:46:21 +01:00
Hornwitser ff3fc3b94b Remove .js suffix in require paths
Remove the .js suffix in require paths to make requires consistent
across the project.
2019-10-28 19:45:34 +01:00
Hornwitser 6bde8a2219 Remove .js suffix from node invocations
Remove the .js suffix from help texts of node invocations as well as
actual invocations as this is not necessary.  Removing it opens up the
possibility of restructuring the code into a directory with the same
name.
2019-10-28 19:45:32 +01:00
Hornwitser a9a382b993 Mark package as 2.0.0 development version
Change the version in package.json to 2.0.0-alpha
2019-10-28 19:44:56 +01:00
Hornwitser e287ca6ea9 Merge pull request #248 from Hornwitser/save-patch
Replace Hotpatch with a save patcher
2019-10-27 17:23:27 +01:00
Hornwitser 5d9d3981ff Remove getLua and getCommand from clusterTools
Remove the unused and badly behaved getCommand and getLua functions from
clusterTools.  The main issue with these is that encourage the use of
large commands that have negative impact on the performance of the
servers, as such large commands have to be transmitted to all players
that are connected.  These functions also have poor handling of lua
comments and will fail on strings containing for example "--".
2019-10-24 22:49:16 +02:00
Hornwitser 1595555154 Refactor statisticsExporter lua code
Rewrite lua part of statisticsExporter to to use game.table_to_json and
the save patching based module system.
2019-10-24 22:49:16 +02:00
Hornwitser 1a035e849e Replace Hotpatch with save patching
Remove Hotpatch and implement a save patcher in its place.  The save
patcher takes a normal Factorio save and adds modules to it that's
loaded by a generated control.lua file.  This greatly reduces the
difficulty of adding an existing game to Clusterio and using scenario
code in plugins.

The mechanism for selecting what code to patch into the saves from
plugins is temorary and expected to change with #220.
2019-10-24 22:49:16 +02:00
Hornwitser 079e31cf05 Implement hashStream
Add a hashStream method to lib/hash that does the same as hashFile only
with a stream instead of a file path and implement tests for both of
these methods.
2019-10-24 22:49:16 +02:00
Hornwitser e6d995d97a Merge pull request #242 from Hornwitser/finish-factorio-server
Rewrite Factorio server spawning and communication
2019-10-24 22:48:19 +02:00
Hornwitser c08d97e698 Document change to instance ports and password
Add entry to changelog detailing the change to null by default entries
for the ports and password in the instance config, and update the
comment in it to reflect the recent changes.
2019-10-24 15:14:40 +02:00
Hornwitser 4d94fb9a6c Fix blank port/password being sent to master
Fix client sending a blank port/password when these are configured as
null and generated by the FactorioServer class.
2019-10-24 15:14:34 +02:00
Hornwitser 93480c8b29 Refactor getFactorioLocale
Move getFactorioLocale to the new lib/factorio namespace and refactor it
to use async functions instead of callbacks.  Split tests into
integration with factorio and testing of the function itself.

Also removes the cacheing from the function itself as this breaks the
isolated testing of it.
2019-10-14 18:56:46 +02:00
Hornwitser a01b971ba2 Rewrite Factorio server spawning
Break out the server process management to a new FactorioServer class in
lib/factorio.  This class takes responsibility for managing a factorio
server, communicating with it and handling the stopping of it.  It also
replaces the previous effort to do this in lib/factorioServer.

This change also updates the version of rcon-client, and since the
author of it has updated it to use UTF-8 the fork is no longer
necessary.  It also removes the messageInterface implementation
in favor of just calling the rcon-client library directly since
this has rate limiting logic built in.

The lib/factorio library is intended to abstract all the interfacing
that happens with Factorio.
2019-10-14 18:56:46 +02:00
Hornwitser 6b4ab6660c Generate instance ID separate from rcon password
Create a random instance ID using Math.random instead of deriving it
from the RCON password with a hombrew hash algorithm.  Remove
stringUtils as it's no longer needed.
2019-10-14 15:51:45 +02:00
Hornwitser 33e9f9d983 Link lib/scenarios instead of copying it
Link the instance directory's scenario folder to lib/scenarios instead
of copying it.  This ensures it stays up to date.
2019-10-14 15:51:45 +02:00
Hornwitser cbcf868c9e Refactor fileOps.getNewestFile
Simplify getNewestFile by converting it to an async function, use the
fs-extra promise based fs functions and remove the files argument as
well as the callback.

Refactors the tests as well to use the test/temp folder.
2019-10-14 15:51:45 +02:00
Hornwitser 6a870be233 Implement symlinking of shared mods
Add symlinkMods function which takes an instance and a shared mods
folder as input and updates or creates the instance mods folder from it
by symlinking the mods in the shared mods folder to the instance mods
folder.

On Windows this uses hard links as the symlinks require elevated
privileges.
2019-10-14 15:51:45 +02:00
Hornwitser 2ff52a1cf2 Move clusterioMod file creation
Move the creation of the output.txt, orders.txt and txbuffer.txt script
output files out of the client.js code and into the clusterioMod.
2019-10-14 15:51:45 +02:00
Hornwitser 3202e2d150 Merge pull request #246 from Hornwitser/port-1.2.2-fixes
Port 1.2.2 fixes
2019-10-13 17:57:26 +02:00
Hornwitser debb33eb3f Change instructions to install master branch
Change back the instructions in README.md to install the master branch
of clusterio and add warnings for this in the relevant locations
directing users to the stable branch for instructions on how to install
the stable version.
2019-10-13 15:36:06 +02:00
Hornwitser 34fc6f0820 Port exit if masterAuthToken is missing
Exit client.js with a non-zero exit code if it's detected that the
masterAuthToken is missing in the config.

This is a port of 8b4ec58.
2019-10-13 14:45:27 +02:00
Hornwitser e8d9d160c4 Port recommend running without pm2 first
Recommend running master server without pm2 first as this makes
discovering any potential errors much simpler.

This is a port of 6e26d32.
2019-10-13 14:45:27 +02:00
Hornwitser 999fb79570 Fix changelog entries for 1.2.2
Move node v10 requirement to 1.2.2 and add changelog entry for updating
bcrypt (which has already been done for the master branch.)
2019-10-13 14:19:35 +02:00
Hornwitser 77571d3c71 Port swap curl with wget in install instructions
Swap out curl with wget in the install instructions as wget is
pre-installed on Ubuntu.

This is a port of 72b56a0 and dbef2bd.
2019-10-13 14:19:15 +02:00
Hornwitser de20635aea Port reorder install instructions
Reorder the install pm2 step to be after the npm install step in the
instructions.  This is necessary to avoid npm creating some files in the
home directory owned as root and then failing the npm install step
because of this.

This is a port of c25bf23.
2019-10-12 22:53:44 +02:00
Hornwitser 5d24329456 Port fix crash in recount_cluster_research_progress
Check that the relevant research is present in the research mapping
before accessing it in recount_cluster_research_progress.

This is a port of 0cdb5d0.
2019-10-12 22:46:08 +02:00
Hornwitser 4eda983cf4 Port filter out lower levels when syncing research
Fix the next level of an infinite research getting updated with the
progress of the previous level from other nodes in the cluster by
filtering out infinite researches with a lower level than the local
level.

This is a port of 075d54f.
2019-10-12 22:45:43 +02:00
Hornwitser 7f82680511 Port set progress when completing infinite researches
Set the progress for the next level of an infinite research when
unlocking it via a script.  This is necessary when the research is
actively being researched  as Factorio will carry over the progress for
the current level into the next one when set via a script.

It's possible for the new progress to be non-zero if the next level was
on the research queue.

This is a port of 62f3a99.
2019-10-12 22:45:07 +02:00
Hornwitser 9098b3cf19 Port rename research to tech
Use tech as the loop variable for researches as this is consistent with
the rest of the researchSync module.

This is a port of 3b1ed1a
2019-10-12 22:44:13 +02:00
Hornwitser 183902ced3 Port fix incorrect property checks in researchSync
Fix naive checks for properties being set of the form obj[key] into the
proper form Object.hasOwnProperty.call(obj, key).  The reason for this
is that if the key has a value of a built-in property like 'constructor'
then a check of the form obj[key] will always return true even if that
property hasn't been set on the object.

This is a port of 6a6274e.
2019-10-12 22:43:27 +02:00
Arvids Godjuks b3eae22221 Merge pull request #241 from Hornwitser/port-1.2.1-fixes
Port version 1.2.1 fixes
2019-09-19 15:43:08 +02:00
Hornwitser 0b3e4fe6f4 Port fix license in package.json
Correct license entry being ISC instead of MIT in package.json.

This is a port of 7e2ca58.
2019-09-19 14:50:21 +02:00
Hornwitser 7d1942b615 Port spawn factorio server deteached in client.js
Sending ^C via the terminal on Linux may cause SIGINT to be sent twice
to the factorio process, causing it to exit without completing the
saving.  This happens because the shell sends the interupt to all
members of the process group for the job and this includes both the
node client process and the factorio server process.

Avoid this situation by passing detached to spawn when the platform is
Linux, which causes the process to be created with its own process group
and therefore not targeted by a SIGINT from the shell.

This unfortunately does not work on Windows as we can't send SIGINT or
^C to a process there.  Doing subprocces.kill on Windows terminates the
process similarly to how SIGKILL works on Linux.

This is a port of 6b090cd.
2019-09-19 14:50:21 +02:00
Hornwitser 9eb9291314 Port update node-factorio-api to 0.3.8
Note that the versions 0.3.6 and 0.3.7 had issues with an old
mod_cache.json being present.

This is a port of 78de723.
2019-09-19 14:50:21 +02:00
Arvids Godjuks b068550775 Merge pull request #238 from Hornwitser/move-templates
Move EJS templates
2019-09-18 18:16:00 +02:00
Hornwitser 6726c2b16f Refactor EJS string gluing code
Rewrite awful string gluing of HTML with JS to use the slightly less
awful EJS string gluing method.
2019-09-18 16:54:26 +02:00
Hornwitser 706a1dbd39 Indent static/browserconfig.xml
Make static/browserconfig.xml readable by indenting it's content.
2019-09-18 16:54:26 +02:00
Hornwitser a62e8d17a3 Move ejs templates to views/
Move the ejs templates to the default Express.js views/ folder and
change their extension to .ejs.  Reconfigure Express.js to look for
templates in the views folders as well as the plugins folder.  This
enables plugins to render templates using paths of following format
"pluginName/path/to/page-or-view".
2019-09-18 16:54:11 +02:00
Arvids Godjuks a4983d75fb Merge pull request #237 from Hornwitser/missing-coverage
Add missing files to coverage report
2019-09-18 15:35:29 +02:00
Hornwitser 1fd8ae954b Add missing files to coverage report
Add missing.js test files that requires any files that currently does
not have any unit tests defined for them.  This should hopefully make it
so that when unit tests are added for parts of these files the coverage
will increase instead of decreasing.

Note that this does not include CLI Tools/ as those are not testable.
2019-09-18 15:25:19 +02:00
Arvids Godjuks e15462439a Merge pull request #235 from Hornwitser/config-part-3
Config Rewrite Part 3
2019-09-18 14:06:50 +02:00
Arvids Godjuks 312c45bda5 Merge pull request #236 from Hornwitser/dedup-clustertools
Remove duplicated clusterTools code
2019-09-18 14:05:05 +02:00
Hornwitser aaf43143df Add compression to getInstanceName
Set the compression header when doing requests in getInstanceName.
2019-09-18 13:55:01 +02:00
Hornwitser c5cae30d83 Remove duplicated clusterTools code
Remove getCommand, getInstanceName, getSafeLua, and
checkHotpatchInstallation definitions from plugins and use the
implementation in lib/clusterTools for these functions instead.
2019-09-18 13:54:22 +02:00
Hornwitser a783bd9651 Refactor out config injection in modManager
Change config in modManager to be passed to each individual functions
instead of requiring it be present at load time.
2019-09-16 16:04:08 +02:00
Hornwitser 5f33cf4d3b Remove config management
Remove lib/manager/configManager.js and the config management command
line interface as well as the serverManager's web interface for config
management.  The configuration rewrite will work differently and is not
compatible with configManager.js.
2019-09-16 16:04:08 +02:00
Hornwitser dd865b42b5 Fix incorrect handling of instance ids
After 651b09b the instance ID would be treated differently depending on
wheteher it was a string or a number in the master server.  This caused
researchSync to break as it would send the ID as a number while the
master server would have it stored as a string.

Fix by coercing instanceID to a string in the master HTTP API.
2019-09-16 16:04:08 +02:00
Hornwitser 858449c57a Move client config loading to startClient
Move the configuration loading step in client.js into the startClient
function in order to separate it from the client code definition.
2019-09-16 16:04:08 +02:00
Hornwitser 53a5ce062b Ensure instance name is a valid Windows file name
Add check for instance name that ensures it being a valid Windows file.
See the microsoft article about naming files for more details:
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2019-09-16 16:04:02 +02:00
Hornwitser 1ba9e68603 Move instance directory to Instance class
Replace the instancedirectory variable with an Instance class that holds
the directory and has a .path() to get paths relative to the instance
directory.
2019-09-15 13:43:14 +02:00
Arvids Godjuks ec223e9d9e Merge pull request #234 from Hornwitser/move-tests
Move unit tests to test/ folder
2019-09-14 18:21:09 +02:00
Hornwitser a63c341209 Move unit tests to test/ folder
Move unit tests for client, master, and the lib to test/ and move plugin
unit tests to <plugin dir>/test.
2019-09-14 14:49:52 +02:00
Arvids Godjuks 74c99b9e13 Merge pull request #233 from Hornwitser/client-refactor-and-fixes
Client refactor and fixes
2019-09-14 02:14:38 +02:00
Hornwitser bcb22d2af7 Securely generate rcon password
Replace Math.random() based password generation for the rcon interface
with a more secure method based crypto.randomBytes.
2019-09-14 01:18:51 +02:00
Hornwitser eb1b863cec Remove unimplemented mods update command
Remove the mods update command from the client command line interface as
this calls the non-existant modManager.updateAllMods function.
2019-09-14 01:15:36 +02:00
Hornwitser 2428cba2bd Default to ports in the dynamic range
Use random ports in the dynamic range as define by RFC 6335 for
instaces.
2019-09-14 01:15:01 +02:00
Hornwitser 500c9584eb Remove commented out itemStats code
Remove old commented out code dealing with item statistics that is no
longer relevant.
2019-09-14 00:56:22 +02:00
Hornwitser 18a904d9de Use as-table for listInstances
Rewrite listInstances to use as-table instead of manually coded table
generation.
2019-09-14 00:54:32 +02:00
Hornwitser 85daf6df19 Rename listPlugins to listInstances
Rename incorrectly named function listPlugins to listInstances.
2019-09-14 00:53:50 +02:00
Arvids Godjuks a57b0a3b5e Merge pull request #231 from Hornwitser/auth-master
Port socket.io Authentication to master
2019-09-11 17:52:10 +02:00
Hornwitser 8aa66e9e68 Port authentication of socket.io server
Authenticate connections made to the socket.io server via a token query
parameter that's validated in a similar manner to the x-access-token for
HTTP requests.

This is a port of 0c45e57.
2019-09-11 16:59:11 +02:00
Hornwitser d378287941 Restructure Changelog
Make "Changelog" the top level header, and move the existing headers one
level down.
2019-09-10 23:53:21 +02:00
AlienXAXS ea5c0d5aa3 Fixes editSlaveMeta and db AddItem errors (#228)
Fixed editSlaveMeta to pass a number to db.addItem method
2019-09-10 17:52:09 +02:00
Arvids Godjuks 094d44a7f5 Merge pull request #227 from Hornwitser/config-part-2
Config Rewrite Part 2
2019-09-09 14:06:50 +02:00
Hornwitser 1bbf1542d2 Refactor client.js command handling into functions
Move the various actions performed by client.js into separate async
functions and do the command action logic if client.js is invoked as a
module directly.
2019-09-09 12:44:09 +02:00
Hornwitser 98900e72dc Merge masterIP and masterPort into masterURL
Merge the masterIP and masterPort configuration entries into a single
masterURL entry.  Fixes the problem with connecting to an HTTPS version
of the master server and also makes it easier to host the master server
behind a reverse proxy.

This breaks all known code interfacing Clusterio.
2019-09-09 12:44:09 +02:00
Hornwitser da99b39402 Remove compiled TypeScript code
Remove the compiled output of veladon's clusterioModel project.  The
source code can be found at https://github.com/veladon/clusterioModel
and doesn't really belong in Clusterio as it's specific to the train
teleports plugin.
2019-09-06 15:43:44 +02:00
Arvids Godjuks aba22960a6 Merge pull request #226 from Hornwitser/check-tests
Check the exit status of the tests script
2019-09-06 12:55:17 +02:00
Hornwitser 0ec37414c2 Deduplicate test script and check exit status
Remove the explicit ignoring of the exit status of the test scripts.
This makes the Travis CI build be considered a failure if one of the
tests fail.  Also rewrite the fast-test script to use the test script.
2019-09-06 00:16:13 +02:00
Arvids Godjuks 2e50921d32 Documentation adjustments (#225)
README.md update - adjusted install instruction to be against the stable branch
2019-09-05 14:55:42 +02:00
TheAIBot e14f7c22fd Merge pull request #222 from Hornwitser/config-part-1
Config Rewrite Part 1
2019-09-05 11:33:43 +02:00
Hornwitser a31778c7ff Remove unused config parameter to pluginManager
Remove the unused library initialization parameter config to
pluginManager and reorganize pluginManager to no longer be defined by a
function.
2019-09-04 20:43:13 +02:00
Hornwitser 35546b4b18 Move loading of config in master to startServer
Move the loading of the config file in the master server to the
startServer function.  Remove config.json.test as it's no longer needed.
2019-09-04 20:43:13 +02:00
Hornwitser 651b09b613 Refactor database and item retreival
Change the ugly global slaves object to be a member of the db object and
implement the items database as a separate integrety verifying class in
lib/database.
2019-09-04 20:43:13 +02:00
Arvids Godjuks e1263a40b1 Merge pull request #221 from Hornwitser/fix-socketio
Fix master socket.io broken by a3fc1dd
2019-09-04 18:28:22 +02:00
Hornwitser 23ce86aef6 Fix master socket.io broken by a3fc1dd
Add missing attach calls to the master socket.io object that was
accidentally removed by the refactoring in a3fc1dd.  Unfortunately I've
been unable to determine if socket.io actually supports listening on
multiple servers at the same time, this needs further investigation.
2019-09-04 17:39:49 +02:00
Hornwitser 5d6f6de379 Export app as a separate entry from master.js
Change the module exports from master.js to allow additional objects to
be passed to allow better unit testing.
2019-09-04 17:36:06 +02:00
Hornwitser af2471b427 Remove config dependency in lib/authenticate
Restructure lib/authenticate to set the authentication secret at a later
point in time via setAuthSecret.  This breaks playerManager.
2019-09-04 17:36:06 +02:00
Arvids Godjuks 8c1c8d07a9 Merge pull request #224 from Hornwitser/duck-tape-researchsync
Duck tape researchsync tests
2019-09-04 17:34:28 +02:00
Arvids Godjuks b0ad02d412 Merge pull request #219 from Hornwitser/dev-notice
Add development warning
2019-09-04 17:32:51 +02:00
Arvids Godjuks 7d61cd9716 Updated README.md to warn about master usage
And checkout the 1.2.x branch instead or use the release.
2019-09-04 16:22:58 +02:00
Hornwitser 31298c7c1c Duct tape together researchSync tests
Add nock that responds to the requests the researchSync module makes
during the tests, and disable broken tests.
2019-09-04 14:59:25 +02:00
Hornwitser b191f7d831 Use instanceName in researchSync
Remove silly roundabout way to the get the current instance name.
2019-09-04 14:59:21 +02:00
Hornwitser f4ca5d92a1 Pass instance name to plugins
Add instanceName entry to the config passed to plugins that contains the
name of the current instance.
2019-09-04 14:59:01 +02:00
Hornwitser 703423ae54 Add development branch warning at startup
Add a big banner based warning telling this is the development branch
for clusterio to the startup of client.js and master.js.
2019-09-01 23:34:52 +02:00
Hornwitser 84f48e36f4 Improve error reporting at startup
Add StartupError that differentiates unhandled errors from errors with
known causes and use it to only print the "please report" banner when an
unhandled error occurs.

Also makes the error reporting more visible in the console.
2019-09-01 23:24:32 +02:00
Hornwitser 6ee84c41bf Add development branch notice
Add notice to the top of README.md stating this is the development
branch for 2.0 and that the 1.2.x branch should be used for production
systems.
2019-08-30 19:34:23 +02:00
Arvids Godjuks ef66ab37da Merge pull request #218 from Hornwitser/remove-remanent-remotemap
Remove remanent remoteMap code
2019-08-29 14:36:51 +02:00
Hornwitser b73b202513 Remove remanent remoteMap code
Remove remoteMap specific code left behind after the removal of the
remoteMap plugin in 6415784.  The plugin interface should be versatile
enough to allow all of this code to reside inside the plugin itself.
2019-08-29 11:38:57 +02:00
Arvids Godjuks 5d28718175 Merge pull request #215 from Hornwitser/various-fixes
Various unrelated fixes and improvements
2019-08-24 16:36:26 +02:00
Hornwitser 3b08b73c37 Remove unused function parseJsString
Remove unused and dangerous parseJsString function from
inventoryImports.
2019-08-24 11:55:19 +02:00
Hornwitser 9c9b4463a4 Fix nested headers in static/nodes
Fix instance names being nested in both h2 and h5 headers, which is not
allowed in html.
2019-08-24 11:35:57 +02:00
Hornwitser fb2519cb50 Add fast-test and fast-cover npm scripts
Add alternate test and coverage scripts that skips tests that are slow.
2019-08-24 11:25:40 +02:00
Hornwitser 5a9e2a8825 Fix inventoryImports/interfacingFunctions test
Fix test in inventoryImports/interfacingFunctions breaking due to
masterAuthSecret not being defined in the config.
2019-08-24 11:25:40 +02:00
Hornwitser a39a17b220 Throw Error objects in inventoryImports/interfacingFunctions
Change throwing of strings objcts to throw proper Error objects.
2019-08-24 11:25:40 +02:00
Hornwitser 940a7b9180 Refactor getFactorioLocale spec
Fix the incorrect error validation, be more lenient about empty values
and convert the checks to use async functions instead of error prone
callbacks.

Remove excessive getFactorioLocale checks in master.spec.
2019-08-24 11:25:40 +02:00
Hornwitser ab2a377ca5 Remove Prometheus pushgateway code
Remove code pushing metrics to a hardcoded Prometheus pushgateway that
is no longer in operation.  There's no point supporting the use of a
pushgateway as there's already a full client implemented and since the
master server is not ephemeral or batch job it shouldn't be using a
pushgateway in the first place.
2019-08-24 11:25:40 +02:00
Hornwitser ea2f291025 Remove reference to old authentication scheme
Remove long comment and commented out code about an old sanity check in
/api/place.
2019-08-24 11:25:40 +02:00
Hornwitser 3e80e40223 Move database and masterMods folder creation
Move the code creating the database and masterMods folders into the
startServer function in master.js.
2019-08-24 11:25:39 +02:00
Hornwitser ed54db0db9 Handle errors during master startup
Print a message urging unhandled exceptions thrown by startup to be
reported to the GitHub issue tracker and initiate shutdown.
2019-08-24 11:09:01 +02:00
Hornwitser 7dcd46a99a Add .vs to .gitignore
Prevent Visual Studio workspace files from getting accidentally added to
the project.
2019-08-24 11:04:58 +02:00
Hornwitser 0d4ad2f075 Fix global leaks detected by mocha
Fix globals leaked in stringUtils, isJson and static/nav.html.
2019-08-24 11:02:50 +02:00
Hornwitser 0a9ddef120 Check for globals leaking during test
Add --check-leaks to mocha to have it report globals that leak during
the testing.
2019-08-24 11:02:50 +02:00
Hornwitser 38462d3069 Set User-Agent according to GitHub's request
GitHub requests that the user agent be set to either the name of the
user or the application[1].  Set user agent to the name of this
application as they request.

[1] https://developer.github.com/v3/#user-agent-required
2019-08-24 11:02:50 +02:00
Hornwitser 0398bd10f8 Remove debug code
Remove debug switch and console.log guarded by it to log every post
request to /api/getId as it's not needed.
2019-08-24 11:02:50 +02:00
Arvids Godjuks 55164cc48f Merge pull request #213 from Hornwitser/ssl-gen-refactor
Refactor SSL certificate creation
2019-08-23 17:44:07 +02:00
Hornwitser 44b7d0aa19 Refactor SSL certificate creation
Rewrite the generateSSLcert lib to be async, use the sslCert and
sslPrivKey config entries and invoke it only if needed during master
server startup instead of in the post install script.  Also add note to
the changelog covering both this and the fix in c645fac.
2019-08-23 17:24:39 +02:00
Arvids Godjuks cc87654742 Merge pull request #212 from Hornwitser/revert-nonsense
Revert "Use old doledivision as default, new NN based dole is optional"
2019-08-23 17:04:20 +02:00
Hornwitser ec7a93c14f Revert "Use old doledivision as default, new NN based dole is optional"
Revert nonsensical commit 2ef6c93b05
adding personal Visual Studio workspace files.
2019-08-23 16:51:14 +02:00
Arvids Godjuks ca03bed8b7 Merge pull request #211 from clusterio/Remove-hardcoded-paths
Replace hardcoded paths with paths from the config
2019-08-23 15:41:58 +02:00
Arvids Godjuks 88739a952a Merge pull request #210 from Hornwitser/update-node
Update Node.js and dependencies
2019-08-23 15:39:41 +02:00
TheAIBot c645fac3de Replace hardcoded paths with paths from the config 2019-08-23 15:08:20 +02:00
Hornwitser c1699fe33d Update dependencies
Update old dependencies.  Fix change in mocha causing it to not exist
afer the tests are done and a change in the command parsing causing the
tests to no longer be detected.
2019-08-23 14:51:10 +02:00
Hornwitser 69a1ff7817 Use Node.js version 10
Update instructions, Dockerfile and Travis CI to use Node.js 10.
Specify that Node.js version 10 or greater is needed in the engines
section of package.json (note that this is only advisory.)  And document
this as a breaking change in CHANGELOG.md.
2019-08-23 14:50:22 +02:00
Arvids Godjuks e4386d57e8 Merge pull request #209 from Hornwitser/cleanup-travis
Cleanup travis and coverage script
2019-08-23 14:42:07 +02:00
Hornwitser bfa8a99daa Cleanup travis and coverage script
Do not install git, tar and curl as these packages are installed by
default on Travis CI.  Move the codecov upload to the travis script and
do not include the codecov token as it's not needed when uploaded from
Travis CI.  Make the cover npm script into something that can be runned
locally and move the Travis CI version to a new ci-cover script.  Remove
the verbose flag from the tar extration of the factorio headless server.
2019-08-23 14:17:56 +02:00
Arvids Godjuks d72bcbdf01 Merge pull request #204 from Hornwitser/refactor-master
Refactor master to fix unit tests
2019-08-23 14:45:47 +03:00
Hornwitser 71c07fa527 Change GitHub references (#205)
Change references to Danielv123/factorioClusterio on GitHub to
clusterio/factorioClusterio.
2019-08-21 23:18:17 +02:00
Hornwitser 8898e923de Fix travis test integration
Add config.json.test as the the config for running the tests on travis
and add script command to copy it to config.json there.  Should fix the
unit test for master failing at loading the config.
2019-08-21 12:43:09 +02:00
Hornwitser a3fc1dda8c Refactor master.js
Move server initialization code to startServer and call it only when
master.js is being runned directly.  Also refactor the server listen
step to work properly when https is configured and only listen after
plugins have completed loading.

Remove the async function wrapper around the master.js code.  This is
just dumb and makes the code untestable.
2019-08-21 12:43:09 +02:00
Hornwitser a828f50e57 Refactor master.spec.js
Ensure exceptions are properly passed down to the unit test framework
with promises, check the return status of requests, use config to
generate an access token, reduce code duplication and reformat to be
consistent.
2019-08-21 12:43:09 +02:00
Hornwitser 442472e412 Actually fix masterPlugins visibility
The masterPlugins array should not be a global.  The ejs templates
already have it provided through locals.masterPlugins and plugins get it
through onLoadFinish.  Make masterPlugins local to master.js again and
fix the ejs templates to use locals.masterPlugins.
2019-08-21 12:43:09 +02:00
Godmave 3f2bd2653e #update to new hotpatch (#203)
- fixes freeplay scenario
- fixes that clicking the admin gui border kills the server
2019-08-10 11:31:03 +02:00
Godmave 5bbd879f34 Merge pull request #199 from Hornwitser/fix-getinstances
Fix broken require in lib/getInstances.js
2019-08-09 11:36:48 +02:00
Hornwitser 81aa2141b9 Fix broken require in lib/getInstances.js
The require change in f76c84c for lib/getInstances.js accidentally left
out the lib/ prefix causing it to break.
2019-08-09 11:06:48 +02:00
Arvids Godjuks 9c108dd4dd Merge pull request #197 from Hornwitser/remove-remotemap
Remove outdated remoteMap
2019-08-07 17:55:25 +03:00
Hornwitser 6415784e8a Remove outdated remoteMap
Remove the old and outdated remoteMap plugin.  Also removes the
directory for linvodb in databases and the troubleshooting step related
to linvodb.
2019-08-07 16:03:51 +02:00
Arvids Godjuks 206f3db042 Merge pull request #195 from Hornwitser/cover-fix
Fix the coverage and test scripts
2019-08-07 16:42:00 +03:00
Hornwitser fb28693dec Disable remoteMap tests
Disable the tests for the old and outdated remoteMap plugin as it breaks
the whole testing harness due to the linvodb3 dependency being missing.
2019-08-07 15:23:25 +02:00
Hornwitser 79ff52aa20 Replace istanbul with nyc
Use nyc instead of istanbul as the later has been deprecated and is no
longer being developed.  Fix the cover and test scripts to work again
with nyc and add the output dir for nyc into .gitignore.
2019-08-07 15:21:30 +02:00
Arvids Godjuks cb4616f47c Merge pull request #194 from Hornwitser/slim
Removing unused or excessive dependencies
2019-08-07 14:20:56 +03:00
Hornwitser bc7f3804c8 Remove right-pad
Remove unused dependency right-pad.
2019-08-07 10:18:14 +02:00
Hornwitser a452749caa Remove app-root-path
Resolve the relative path using path.relative instead of some hacky
string substitutions.
2019-08-07 10:18:14 +02:00
Hornwitser 1e8fb85122 Remove bcryptjs
Remove unused dependency bcryptjs.
2019-08-07 10:18:14 +02:00
Hornwitser 66c0238a3d Remove undescore
A 50 kb library is not needed to make a function run once, especially
not when it doesn't work.  _.once does not modify the function passed to
it to only run once it returns a new function that only runs once.
Since the result of once was discarded, it had no effect.

Remove underscore and implement instanceManagement running once properly
with 3 lines of code.
2019-08-07 10:18:14 +02:00
Hornwitser ca0281ae58 Remove pointless micro-optimization
This saved 30ms of the startup on my machine.  Even if it saved a second
it's still pretty pointless as we're not depending on the master server
startup being super fast.
2019-08-07 10:18:14 +02:00
Hornwitser 200c8dbdeb Use --only=production on npm install
Add --only=production flag to Dockerfile and the README entries
describing how to install nodes.  This will avoid pulling in a lot of
dependencies that are not needed for running master/slaves.
2019-08-07 10:18:14 +02:00
Hornwitser d0afcbf0ac Move html5-validator to devDependencies
html5-validator is only used for testing the master server web interface
and does not need to be included on production installs.
2019-08-07 10:18:14 +02:00
Hornwitser 13a069a798 Reimplement hashFile locally
Replace the 10 MB of dependencies that is hash-files with a homebrew
Promise based implementation of hashFile that uses crypto and fs.  This
somehow also ends up being the same amount of code as the one using
hash-files.
2019-08-07 10:18:14 +02:00
Hornwitser 0e849488e2 Remove snyk
It's not supposed to be installed as a dependency in the first place,
and this seems like the wrong solution to there being too many stinking
dependencies to keep track the security of.
2019-08-07 10:18:14 +02:00
Hornwitser 6264bd9888 Update node-factorio-api
The 0.3.5 release of node-factorio-api removed the dependency to
documentation, which itself pulls in another 500 dependencies.  Update
to 0.3.5 to avoid having this pulled in.
2019-08-07 10:18:14 +02:00
Hornwitser cbd1939a74 Stop pretending there's documentation
Remove docs scripts, documentation.yml and the documentation dependency.
2019-08-07 10:18:14 +02:00
Godmave 7cf9319624 Merge branch 'master' into master 2019-08-06 19:52:41 +02:00
Godmave de2141ec50 make masterPlugins visible to other things again 2019-08-06 19:45:18 +02:00
Godmave 0df1b8e1fd Add tailing of files and move socketio connect after plugin load (#191)
#add tailing of files to prevent lost data between factorio and clusterio
#move socketio connect to the bottom after the plugins are loaded 
  to prevent plugins not getting the connection event
#add some more transfer compression in different places
2019-08-05 07:47:57 +02:00
Godmave ec0243d7cf #resolve merge conflict 2019-08-04 17:54:58 +02:00
Godmave f32624586b #resolve merge conflict 2019-08-04 17:51:09 +02:00
Godmave 6ea47cb6af Merge branch 'master' into master 2019-08-04 17:47:32 +02:00
Godmave a204163a6c #replace stray whitespaces with tabs again 2019-08-04 17:26:50 +02:00
Hornwitser f76c84c3f1 Move node_modules/_app/ into lib/ as a package (#190)
Move the contents of node_modules/_app into lib/ as there shouldn't be
any content in node_modules.  This directory is automatically managed
and pruned by npm.

Add lib/ as a local package in package.json, this allows scripts
everywhere to require it's content as "lib/path" instead of relying on
local paths.  This shadows the StdLib Node.js Bindings package, but I
could care less.

Change all requires with lib as a local path and _app to use just lib.
2019-08-04 15:52:36 +02:00
Godmave 6caef9bda9 #add some more compression
#add tailing of files to prevent lost data between factorio and clusterio
#move socketio connect to the bottom after the plugins are loaded to prevent plugins not getting the connection event
2019-08-04 15:45:17 +02:00
Godmave 30cda4de3b Merge pull request #11 from Danielv123/master
merge
2019-08-04 15:43:09 +02:00
Hornwitser a13d4c6805 Use published version of rcon-client-fork (#188)
Switch to a version of rcon-client-fork that's published on npm.  Avoids
having to put build files in the git repo.
2019-07-30 16:28:43 +03:00
Godmave 56b805e19b Merge pull request #187 from Zeroto/master
Switch to rcon-client git dep with dep build output in it
2019-07-30 14:06:34 +02:00
Sander Homan 5fd3a6b188 Switch to rcon-client git dep with dep build output in it 2019-07-30 14:03:56 +02:00
psihius 9d1e60aa03 Remove Doomsday scenario, should be reworked as dynamic plugin. See doomsday-save-point for scenario code 2019-07-30 11:49:24 +02:00
Godmave d819644b01 Merge pull request #186 from Zeroto/master
Fix notifying about the same research being finished
2019-07-30 08:12:21 +02:00
Sander Homan 042230dc2c Fix notifying about the same research being finished
This is just a bandaid to fix a symptom. The underlying cause is still there, but at least it shouldn't spam the players anymore.
2019-07-29 20:31:42 +02:00
Hornwitser a00183e94c Switch to rcon-client fork (#183)
Switch to using fork of rcon-client where the commands and the responses
are not converted to ASCII before being sent over the wire.  Should fix
the issue with Russian text showing up as garbled letters in the chat as
well as the issue with train schedules using >= and <= conditions.

If rcon-client upstream is updated with the utf8 patch this commit can
be reverted with the version the patch was included in.
2019-07-28 14:42:44 +02:00
Arvids Godjuks c95d9f9464 Added "Doomsday" scenario (#184)
* Added doomsday scenario
2019-07-27 22:59:23 +03:00
Danielv123 a1237d8cfd Added CLI script for deleting players from playerManager 2019-07-26 23:55:33 +02:00
Godmave 35101a7e32 Merge pull request #10 from Danielv123/master
sync
2019-07-26 18:55:46 +02:00
Godmave 95d06293f5 Merge pull request #181 from Hornwitser/patch-1
Fix wrong argument order
2019-07-26 18:53:58 +02:00
Hornwitser 2714b09e5d Fix wrong argument order
Fix compression not applying because the order of the callback and
options arguments were swapped in the call to Needle.get for the
request_cluster_data function.
2019-07-26 18:51:16 +02:00
Godmave e733cfde03 Merge pull request #180 from Godmave/master
add compression to research sync
2019-07-25 23:23:57 +02:00
Godmave 008ea139c6 #add compression 2019-07-25 23:22:46 +02:00
psihius 3338c439dd Empty meta fix for querying new slave check 2019-07-25 22:20:34 +02:00
psihius b956438d8d INVALID REQUEST is not a valid json key for a variable, end up being "undefined" 2019-07-25 22:08:33 +02:00
Godmave cdbbdd41c7 Merge pull request #9 from Danielv123/master
sync
2019-07-23 20:34:51 +02:00
Master-Guy e2775adcc3 /api/rconPasswords fix & /api/slaves security fix (#179)
Fix for /api/slaves to not send rconPasswords
Fix for /api/rconPasswords to work properly and send partial information
2019-07-20 15:52:47 +02:00
Master-Guy d909b4aa47 Fix for /issues/177 (#178)
/api/editSlaveMeta: Removed unneeded rcon check
/api/getSlaveMeta: Removed useless password check
/api/checkMod: Added authenticate.middleware
/api/slaves: Renamed to /api/rconPasswords and changed to authenticate.middleware
/api/runCommand: Changed to authenticate.middleware
2019-07-20 14:33:54 +02:00
Master-Guy 49611a2062 /api/slaves POST (optimized) (#176)
* Adds a /api/slaves POST call that requires authentication. It also returns the rconPasswords, unlike the GET call.
2019-07-19 15:37:49 +03:00
AreYouScared ee6ab6ab72 Remove GPS pings in crosschat (#172)
* Remove GPS pings in crosschat

Cross-server gps pings are annoying, lets fix that

* Lets just do it the right way <3

Should be better

* Take 2

Let's try that again
2019-07-19 12:05:03 +02:00
Arvids Godjuks de92ca96b9 Merge pull request #173 from Master-Guy/master
Add "info" field between client and master
2019-07-18 18:15:37 +03:00
Arvids Godjuks 77281d0662 Merge pull request #174 from MrRozdor/log-research-sync
Add logging to researchSync plugin
2019-07-18 16:25:27 +03:00
TechnoStrife 2831c07000 Add logging to researchSync plugin 2019-07-17 23:21:57 +03:00
Master-Guy 6ac8c0711f JSON fix
Small fix for JSON
2019-07-17 19:23:53 +02:00
Master-Guy c9cbc48d95 2/2 Add "info" field between client and master
Adds a "info" field to config.json and allows the client to send the contents of this info field to the master, so it also shows up (i.e. with /api/slaves)
2019-07-17 18:40:01 +02:00
Master-Guy 50370bf858 1/2 Add "info" field between client and master
Adds a "info" field to config.json and allows the client to send the contents of this info field to the master, so it also shows up (i.e. with /api/slaves)
2019-07-17 18:35:02 +02:00
Godmave d1fd4c3565 Merge pull request #171 from MCRcortex/patch-1
added res.sendStatus(200); to stop client errors throwing
2019-07-17 14:07:14 +02:00
MCRcortex 8672afe940 added res.sendStatus(200); to stop client errors throwing
according to the docs
"The methods on the response object (res) in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging."
Last sentence is the critical one.
Sending a status code 200 should fix the issue
2019-07-17 22:04:29 +10:00
Godmave adf6763a0d Merge pull request #170 from MrRozdor/fix-researchsync
Fix researchSync that it counts progress only from techs on same level
2019-07-17 07:16:48 +02:00
TechnoStrife d48a824a69 Fix researchSync that it counts progress only from techs on same level 2019-07-17 00:31:15 +03:00
Godmave f28b73cf31 Merge pull request #169 from MrRozdor/spread_researching
Fix for migation to new researchSync version
2019-07-15 21:50:38 +02:00
TechnoStrife fec3cf5ddf Fix for migation to new researchSync version 2019-07-15 22:49:39 +03:00
Godmave c7cfb8ce70 Merge pull request #168 from MrRozdor/spread_researching
Make any node be able to research anything
2019-07-15 21:27:49 +02:00
TechnoStrife 93c210f0c7 Merge branch 'master' into spread_researching
# Conflicts:
#	sharedPlugins/researchSync/dumpResearch.lua
#	sharedPlugins/researchSync/index.js
2019-07-15 22:14:09 +03:00
TechnoStrife 251ec696a3 Fixes && debugging 2019-07-15 18:29:53 +03:00
Godmave 773a2c412e Merge pull request #167 from veladon/pr/3
fix for stats exporter console errors
2019-07-15 15:05:21 +02:00
veladon c0499e7f27 Merge branch 'master' into pr/3 2019-07-15 13:57:27 +01:00
veladon 0a65946bf4 fix for /api/logstats 2019-07-15 13:47:53 +01:00
veladon 233410add7 fix for stats exporter console errors (#166) 2019-07-15 13:49:49 +02:00
veladon fea9fb28ca Merge branch 'master' of https://github.com/veladon/factorioClusterio 2019-07-15 12:46:45 +01:00
veladon e1b510b441 fix for stats exporter console errors 2019-07-15 12:46:38 +01:00
veladon 34301eb381 Merge pull request #2 from Danielv123/master
merge
2019-07-15 12:44:43 +01:00
TechnoStrife 4a0956d174 Fix a bug causing all slaves data gets cleared on master startup
Cherry-picked from fix-slaves-data-cleanup-on-startup
2019-07-15 11:39:11 +03:00
TechnoStrife 3b814eeea4 Fix a bug causing all slaves data gets cleared on master startup (#165) 2019-07-15 10:34:02 +02:00
TechnoStrife 98c6795f8f Make researchSync work for any research on any node 2019-07-15 00:46:21 +03:00
Godmave 21dd80df9b Merge pull request #8 from Danielv123/master
sync
2019-07-14 20:54:46 +02:00
Arvids Godjuks 0b34f07954 Dump only enabled research (#164) 2019-07-14 17:48:20 +02:00
Daniel c978e8e748 Merge pull request #163 from psihius/master
Mass sync does not spam
2019-07-14 15:58:07 +02:00
psihius 71870daaba Made so mass tech sync does not make sounds or messages, only notifies at the end 2019-07-14 15:00:55 +02:00
Daniel 5a60f317ba Merge pull request #162 from psihius/master
Update to research sync plugin
2019-07-14 14:06:24 +02:00
psihius b666a909ab Formatting and logging adjustments 2019-07-14 01:32:34 +02:00
psihius 0afe35b67c Updated research sync to handle infinite science and hopefully stop it from apply same research in a loop annoying everyone 2019-07-14 01:25:48 +02:00
veladon 096ead5bc4 Merge pull request #1 from Danielv123/master
merge
2019-07-13 18:14:51 +01:00
TechnoStrife fa4c10dce2 Code refactor 2019-07-12 18:52:47 +03:00
Daniel Vestøl 1f8ac50950 Better error message for statisticsExporter 2019-07-09 22:50:28 +02:00
Godmave 10d5b0da9b Merge pull request #7 from Danielv123/master
pull updates
2019-07-06 19:57:53 +02:00
Daniel Vestøl 6c32271c56 Fixed rcon timeout in the right place, set to 200s for 1250kB commands 2019-07-06 12:44:31 +02:00
Daniel Vestøl a122f2b09e Increased rcon timeout to 20s, increased maxPending to 5 2019-07-06 00:46:22 +02:00
Daniel Vestøl f35be2f355 Set max pending rcon requests to 1 2019-07-05 23:42:51 +02:00
Daniel Vestøl d66d22a9cd Updated rcon-client 2019-07-05 23:07:25 +02:00
Godmave 7d6a5bb6d2 Merge pull request #6 from Danielv123/master
getting up to date
2019-06-24 18:51:21 +02:00
Daniel bf2e78d530 Merge pull request #159 from veladon/master
Added cluster model js to /lib/
2019-06-10 14:11:04 -07:00
veladon bf4b7fe1b8 Added cluster model js to /lib/ 2019-06-10 21:40:31 +01:00
Daniel 66ba706c56 Use neural net dole divider by default on new installs
See "useNeuralNetDoleDivider": true,

If this causes issues, simply set it to false in config.json. If you find normal dole division to work badly as well, set disableFairItemDistribution to true.
2019-06-02 15:27:50 +02:00
Daniel 79c9ac839e Merge pull request #158 from axapaxa123/master
New NN, added debt system
2019-06-02 06:26:11 -07:00
Daniel 7f59b200b1 Merge pull request #153 from veladon/master
player to character inventory renames and extra info for EACCESS problem
2019-06-02 06:24:23 -07:00
axapaxa123 d719a73328 New NN, added debt system 2019-06-02 15:13:49 +02:00
axapaxa123 616bb9cc8f Merge pull request #1 from Danielv123/master
Merge pull request #152 from axapaxa123/master
2019-06-02 15:04:32 +02:00
veladon 3ae55cbd27 Updated defines.inventory.player_<x> to defines.inventory.character_<x> as per 0.17.35 change to rename character entity type from "player" to "character". 2019-05-13 20:41:15 +01:00
Daniel 7ae9ff6649 Merge pull request #152 from axapaxa123/master
New NN, updated required data
2019-05-13 10:42:53 -07:00
axapaxa123 0b3007d22d New NN, updated required data 2019-05-13 19:22:55 +02:00
veladon bd7a116860 Added extra instructions to readme for the EACCESS common problem 2019-05-12 18:10:44 +01:00
Daniel Vestøl 0c9ed582cc Fixed scenario_loader 2019-04-22 13:45:09 +02:00
Daniel d7bfc2193e Merge pull request #151 from axapaxa123/master
Neural Dole v2
2019-04-12 11:54:12 -07:00
axapaxa123 ce9e1d876a Updated neural network and interface 2019-04-10 01:14:39 +02:00
axapaxa123 86d4f3dba4 Longer accounting period, moved creation of gauge 2019-04-10 01:13:06 +02:00
axapaxa123 0fe5633926 Moved gauge creation, 0 items don't fail requests 2019-04-10 01:11:32 +02:00
axapaxa123 595b4e5ac9 Update master.js 2019-04-03 14:40:42 +02:00
axapaxa123 7d2ac5808f Update dole_nn_base.js 2019-04-03 14:40:11 +02:00
axapaxa123 fddafaec09 Update remove.js 2019-04-03 14:39:29 +02:00
Daniel Vestøl abf4b5d926 Updated freeplay script to 0.17 version 2019-03-31 17:56:34 +02:00
Daniel Vestøl 00dfae8bd1 Fixed another bug in scenarioLoader 2019-03-24 20:34:53 +01:00
Daniel Vestøl 2ef6c93b05 Use old doledivision as default, new NN based dole is optional 2019-03-24 20:18:03 +01:00
Daniel Vestøl 2165acb171 Merge branch 'neuralDoleDivision' 2019-03-24 20:16:32 +01:00
Daniel Vestøl 8357fa23ce Fixed bug in scenarioLoader 2019-03-24 20:14:47 +01:00
Daniel Vestøl 94d434c8ef Initial release of neural net based dole divider 2019-03-23 00:14:23 +01:00
Daniel Vestøl 56cf131c70 Moved most of /api/remove to its own file 2019-03-22 19:29:56 +01:00
Daniel Vestøl a2be560b8b Updated hotpatch 2019-03-13 20:13:03 +01:00
Daniel a3e33f6164 Merge pull request #148 from Danielv123/snyk-fix-9pjp3n
[Snyk] Fix for 7 vulnerable dependencies
2019-03-10 06:23:51 -07:00
Daniel 01e96a4dcb Merge pull request #149 from wchristian/patch-1
independant -> independent
2019-03-06 22:19:20 -08:00
Christian Walde (Mithaldu) d189a82494 independant -> independent 2019-03-07 05:03:45 +01:00
Daniel Vestøl c15709ddc1 0.17 update: Fixed hotpatch and scenario_loader 2019-02-27 11:05:39 +01:00
snyk-bot 3f29a722bb fix: .snyk & package.json to reduce vulnerabilities
The following vulnerabilities are fixed with a Snyk patch:
- https://snyk.io/vuln/npm:hawk:20160119
- https://snyk.io/vuln/npm:http-signature:20150122
- https://snyk.io/vuln/npm:mime:20170907
- https://snyk.io/vuln/npm:minimatch:20160620
- https://snyk.io/vuln/npm:request:20160119
- https://snyk.io/vuln/npm:tunnel-agent:20170305
- https://snyk.io/vuln/npm:uglify-js:20151024
2019-02-25 06:48:23 +00:00
Daniel Vestøl 103409b0cd Added setting disableFairItemDistribution to disable dole division factor 2019-02-18 19:49:49 +01:00
Daniel 7e0b131557 Merge pull request #147 from joshricker/patch-1
Remove command splitting
2019-02-18 10:27:45 +01:00
joshricker f9a9722215 Remove command length.
Remove command length for better compatible with large mod packs.
2019-02-18 03:26:11 -06:00
Daniel Vestøl 78061df0c2 Removed dead asset source from remoteMap 2.0 2019-02-04 13:02:07 +01:00
Daniel Vestøl 0024b84745 Added license note for Angels Icons 2019-02-04 12:50:20 +01:00
Daniel df7100ecba Merge pull request #146 from Teoxoy/master
Added logo
2019-02-01 07:48:53 +01:00
Daniel df1875f24d Update README.md 2019-02-01 07:48:33 +01:00
Teoxoy d9afb92ade added logo 2019-02-01 00:05:25 +01:00
Daniel Vestøl 09f476ec40 Added support for 3625 Angels item icons 2019-01-31 14:34:02 +01:00
Daniel Vestøl 846853813a Added rcon tool and better prometheus monitoring for websockets 2019-01-05 14:35:43 +01:00
Daniel Vestøl 064ee0827a Updated Hotpatch to 1.1.11, might have fixed some desync issues 2019-01-02 13:50:18 +01:00
Daniel Vestøl d859cb554a Merge branch 'search' 2018-12-17 22:15:07 +01:00
Daniel Vestøl eaa8d5373c Readded whitespaces 2018-12-17 22:14:51 +01:00
Heilig, Eric 6855923e58 formatting 2018-12-17 14:06:33 -07:00
Heilig, Eric a8fb5197fd allow searching for multiple criteria separated by space 2018-12-17 14:01:38 -07:00
Daniel abc94a16c1 Merge pull request #143 from J-Lassauw/ui_overhaul
Removed Duplicate entity draw entry
2018-12-15 18:59:15 +01:00
J-Lassauw ceaf69810b Removed Duplicate entity draw entry 2018-12-15 17:57:07 +00:00
Daniel Vestøl 2efb628884 Merge remote-tracking branch 'origin/dev' 2018-12-15 16:48:41 +01:00
Daniel d4600c960e Merge pull request #142 from J-Lassauw/ui_overhaul
Web UI Overhaul
2018-12-15 16:47:38 +01:00
J-Lassauw 81fa5abae0 Small fix to make sure old pages using the side bar go to the new nav 2018-12-14 14:30:36 +00:00
J-Lassauw dc9d8bf987 Added fetch mod meta functionality
Fixed bug in settings with new UI
Re-added mod nav options
2018-12-13 16:56:09 +00:00
J-Lassauw 1788d72f8f Updated node details and settings style
Fixed nav spacing.
Added a header to the storage table.
2018-12-11 22:31:30 +00:00
J-Lassauw 659b071eb6 Started web UI overhaul
Added bootstrap
Broke some functionality with additional mods
2018-12-09 15:05:52 +00:00
Daniel Vestøl 274c5af9f5 Added new auth function to lib/authenticate 2018-12-09 15:59:11 +01:00
Daniel Vestøl 8c0a4b1a9e . 2018-11-27 21:00:12 +01:00
Daniel Vestøl 3b9dd6dcc8 Fixed bug with serverManagers web interface 2018-11-21 07:35:51 +01:00
Daniel Vestøl 9b6f8cf652 Fixed error related to getting playerCount 2018-11-17 16:09:46 +01:00
Daniel Vestøl ce13ad50e0 Fixed desync when viewing scenario code in hotpatch 2018-11-17 11:05:10 +01:00
Daniel Vestøl 18c486855d Revamped scenario loader for cross platform compat 2018-11-15 21:37:42 +01:00
Daniel Vestøl 12a0ebf8d1 Scenario loading has big issues with paths, this is a bandaid 2018-11-14 22:17:09 +01:00
Daniel Vestøl 081d9a5bea Made scenario loading less insanely spammy 2018-11-13 21:28:13 +01:00
Daniel Vestøl 1e868b4ff4 Changed hotpatch scenario dir for visibility, made scenario loading recursive 2018-11-13 21:24:42 +01:00
Daniel Vestøl 58b609cbd0 Fixed portable instances 2018-11-10 17:29:32 +01:00
Daniel Vestøl 2c6b8becd4 Added setting disableImportsOfEverythingExceptElectricity 2018-11-07 19:24:04 +01:00
Daniel Vestøl 1676dc80db Merge branch 'dependencyRemoval' 2018-10-23 11:13:36 +02:00
Daniel Vestøl 52d35ab955 Added new plugin for reporting production/kills/construction statistics to Prometheus 2018-10-23 11:10:59 +02:00
Daniel Vestøl aaf4d8b6f1 Removed graphing functionality that utilized linvodb 2018-10-23 11:10:26 +02:00
Daniel Vestøl 8766529e5b Removed linvodb/leveldown dependency 2018-10-22 11:06:21 +02:00
Daniel Vestøl 1b518964fc Updated readme 2018-10-18 21:30:23 +02:00
Daniel Vestøl c4a2b50c08 Fixed some broken statistics 2018-10-17 21:50:32 +02:00
Daniel Vestøl 3f4309fab0 Added pushgateway for prometheus statistics 2018-10-12 23:28:10 +02:00
Daniel Vestøl 2855268e34 Attempted fix for breaking /slaves/ 2018-10-11 14:47:13 +02:00
Daniel Vestøl ffe42c523a Added trainTeleports to plugin list in readme 2018-10-04 19:33:36 +02:00
Daniel db20e9c780 Merge pull request #138 from jakedraddy/master
Add plugin list to readme (with discordchat plugin).
2018-10-01 23:39:15 +02:00
Jacob Draddy 02a401c6c3 Add plugin list to readme (with discordchat plugin). 2018-10-01 17:34:40 -04:00
Daniel Vestøl 94ac9ca624 Added scenario_loader plugin, updated hotpatch (new saves now gives the starting items) 2018-09-29 15:21:35 +02:00
Daniel Vestøl e2566d2d1c [big change] Added serverManager plugin 2018-09-27 14:35:41 +02:00
Daniel Vestøl 852311e84c Plugin loading race condition, resolves #136 2018-09-27 13:48:41 +02:00
Daniel Vestøl 810e4a27b5 Removed snyk 2018-09-27 10:35:25 +02:00
Daniel Vestøl 8a84f4087a Bugfix and usability improvements 2018-09-23 09:17:07 +02:00
Daniel Vestøl 087ba8dabe Improved docs, disabled remoteMap by default 2018-09-09 19:55:09 +02:00
Daniel Vestøl 07b9d0a0f5 Fixed getMac spam in VMs, added log rotation 2018-08-23 10:04:19 +02:00
Daniel Vestøl 857a6824fc Added analytics 2018-08-16 14:52:26 +02:00
Daniel Vestøl 765146b559 Plugin loading now respects disabled plugins 2018-08-16 14:52:09 +02:00
Daniel Vestøl ffa2d0b0fb Fixed windows crash on ctrl+c preventing factorio from saving 2018-08-16 12:19:18 +02:00
Daniel Vestøl a76e07c07b Enable and disable plugins from the commandline (non functional as of yet) 2018-08-15 23:15:01 +02:00
Daniel Vestøl e71c183b48 Collet http performance statistics for plugins as well 2018-08-14 13:05:17 +02:00
Daniel Vestøl 1faa3671e4 configManager.js - more useful return values 2018-08-11 19:59:35 +02:00
Daniel Vestøl 5630f7f7f6 Rewrote master plugin loading 2018-08-10 17:17:19 +02:00
Daniel Vestøl da268d57e7 Serverside rendering of /slaves for more plugin functionality 2018-08-10 15:51:41 +02:00
Daniel Vestøl 8518cd54c6 Introduce more tools for use by plugins 2018-08-10 15:50:46 +02:00
Daniel 3a7527f053 Merge pull request #135 from Valansch/master
Readme typo
2018-08-01 14:21:26 +02:00
Valansch cac481307e Fixed typo in README 2018-08-01 13:54:37 +02:00
Daniel Vestøl 9461978a6e Reduced log spam 2018-07-30 13:30:03 +02:00
Daniel Vestøl 4fed43bad6 [big change] Moved core clusterio Lua mod functionality into its own plugin 2018-07-28 13:45:05 +02:00
Daniel f7f2ce6897 Merge pull request #133 from Gerkiz/patch-1
Added docker support
2018-07-25 21:58:03 +02:00
Gerkiz 49124021a8 Added docker support
Docker doesn't really do anything with the  '/quit'.
However, changing from 'quit' to 'serverprocess.kill("SIGINT");' solved this issue.
The node exits with:
'[dumb-init] Child exited with status 0. Goodbye.'
2018-07-25 21:47:03 +02:00
Daniel Vestøl 329f721b11 Fixed invalid json in config.json.dist 2018-07-25 02:09:00 +02:00
Daniel Vestøl 8abeb26dce Commandline tools to install/uninstall plugins from github 2018-07-25 01:14:43 +02:00
Daniel Vestøl 958cdfa15a Fixed node client.js manage * mods * 2018-07-24 23:14:02 +02:00
Daniel Vestøl d72a45cc66 Added autosaveInterval to the config.json.dist 2018-07-24 23:01:51 +02:00
Daniel Vestøl d913d59d72 Handle instance creation failing gracefully upon lacking internet 2018-07-24 23:00:42 +02:00
Daniel Vestøl 9ce4f064ad Add favicon to master (I think, just a quick-fix) 2018-07-24 22:59:40 +02:00
Godmave d9638b58ea #add configurable items autosave (default 1 minute, config.autosaveInterval) 2018-07-23 19:16:53 +02:00
Godmave 260f8a0bb5 Merge pull request #5 from Danielv123/master
Merge pull request #131 from Godmave/master
2018-07-23 19:13:44 +02:00
Daniel b12f12a91f Merge pull request #131 from Godmave/master
Fix inventory import
2018-07-23 15:19:33 +02:00
Godmave 7784fecb12 #add inventory items up instead of overwriting them 2018-07-21 00:21:27 +02:00
Godmave 68637d78b8 Merge pull request #3 from Danielv123/master
Get current
2018-07-20 23:45:50 +02:00
Daniel dc52d64c7e Merge pull request #123 from Danielv123/snyk-fix-y7mchf
[Snyk] Fix for 1 vulnerable dependencies
2018-07-18 10:42:34 +02:00
Daniel Vestøl adbf28d6bd Made travis run against node 9 instead of 10 2018-07-18 10:33:39 +02:00
Daniel Vestøl 42037fc451 client.js now adds Hotpatch to new maps 2018-07-18 10:30:35 +02:00
Daniel Vestøl 8e9bcc6a62 Untracked package-lock.json 2018-07-18 10:20:20 +02:00
Daniel 789e9d37c8 Merge pull request #130 from Sir3lit/patch-1
This will build correctly
2018-07-18 09:56:45 +02:00
Sir3lit 5c2eb009e2 This will build correctly
The build is successful :)
2018-07-18 02:06:19 -04:00
Daniel f91ff41dff Merge pull request #129 from Godmave/master
Add hotpatch
2018-07-17 18:10:16 +02:00
Godmave 94b2bb5463 #add hotpatch to master 2018-07-16 23:10:56 +02:00
Godmave 002dae9911 Merge pull request #2 from Danielv123/master
get latest
2018-07-16 23:06:15 +02:00
Daniel Vestøl e73a6052fe Instance creation with external instance paths are now working 2018-07-15 17:43:26 +02:00
Daniel Vestøl 724248b09b Merge branch 'master' into #85 2018-07-15 16:10:28 +02:00
Daniel Vestøl 3557b2d0ad Merge remote-tracking branch 'origin/master' 2018-07-15 00:02:11 +02:00
Daniel 1d5d8abd12 Merge pull request #128 from hintss/patch-1
fix typo in log message
2018-07-14 21:44:57 +02:00
Daniel Vestøl 98131ee66e Very big changes to help custom config paths 2018-07-13 22:56:10 +02:00
Daniel Vestøl 509a13062e Merge branch '#85' 2018-07-13 22:55:38 +02:00
Daniel Vestøl 02c545c650 Small change allowing plugins to add sidebar entries 2018-07-13 22:54:58 +02:00
hintss af42b1fcd3 fix typo in log message 2018-07-13 01:36:12 -07:00
Daniel 5dcbf45487 Merge pull request #127 from chrisgbk/master
Fix race condition in node client download
2018-07-10 13:20:35 +02:00
chrisgbk 9501861285 Fix race condition in node client download
Fix race condition in node client download
2018-07-10 04:23:40 -06:00
snyk-bot f8bf979327 fix: .snyk & package.json to reduce vulnerabilities
The following vulnerabilities are fixed with a Snyk patch:
- https://snyk.io/vuln/npm:lodash:20180130
2018-07-03 06:07:59 +00:00
Daniel Vestøl 6962324a45 Tried to fix path problem in the master db 2018-06-28 15:34:31 +02:00
Daniel Vestøl cf0c3b012a Added config settings for globalChat (broadcast all messages) 2018-06-27 11:40:20 +02:00
Daniel ca49564fb7 Update README.md 2018-06-27 00:18:36 +02:00
Daniel Vestøl f13d90aa58 Merge branch '#85' 2018-06-27 00:13:27 +02:00
Daniel Vestøl 9721071ffb Merge branch 'master' into #85 2018-06-27 00:12:41 +02:00
Daniel Vestøl a77aeb9e5b Improved readme 2018-06-27 00:11:28 +02:00
Daniel Vestøl bf19001055 Fixed --config, is probably unstable on client.js 2018-06-24 16:48:08 +02:00
Daniel Vestøl 443e5dc286 Arguments: --port --masterPort --sslPort --config --rcon-port --rcon-password 2018-06-24 16:46:31 +02:00
385 changed files with 40376 additions and 18084 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
# No need to invalidate cache just because we try speeding up the build
.dockerignore
Dockerfile
docker-compose*
# Install dependencies during the build
node_modules
packages/**/node_modules
plugins/**/node_modules
# Download factorio during the build
factorio
factorio.tar.gz
# We build internal mods during build, mods from local test env should not be applied
sharedMods
sharedModsDisabled
# These files should never be required during the build
.git
.github
dist
temp
+1
View File
@@ -0,0 +1 @@
dist/
+356
View File
@@ -0,0 +1,356 @@
"use strict";
module.exports = {
"plugins": [
"node",
],
"env": {
"node": true,
"commonjs": true,
"es6": true,
},
"parserOptions": {
"ecmaVersion": 11,
},
"overrides": [
{
"files": ["test/**/*.js", "plugins/*/test/**/*.js"],
"env": {
"mocha": "true",
},
"rules": {
"prefer-arrow-callback": "off",
"no-invalid-this": "off",
"node/no-unpublished-import": "off",
"node/no-unpublished-require": "off",
},
},
{
"files": ["packages/web_ui/src/**/*.jsx", "packages/lib/browser.js", "plugins/*/web/**/*.jsx"],
"env": {
"browser": true,
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
},
},
"rules": {
"node/callback-return": "off",
"node/exports-style": "off",
"node/file-extension-in-import": "off",
"node/global-require": "off",
"node/handle-callback-err": "off",
"node/no-callback-literal": "off",
"node/no-deprecated-api": "off",
"node/no-exports-assign": "off",
"node/no-extraneous-import": "off",
"node/no-extraneous-require": "off",
"node/no-missing-import": "off",
"node/no-missing-require": "off",
"node/no-mixed-requires": "off",
"node/no-new-require": "off",
"node/no-path-concat": "off",
"node/no-process-env": "off",
"node/no-process-exit": "off",
"node/no-restricted-require": "off",
"node/no-sync": "off",
"node/no-unpublished-bin": "off",
"node/no-unpublished-import": "off",
"node/no-unpublished-require": "off",
"node/no-unsupported-features/es-builtins": "off",
"node/no-unsupported-features/es-syntax": "off",
"node/no-unsupported-features/node-builtins": "off",
"node/prefer-global/buffer": "off",
"node/prefer-global/console": "off",
"node/prefer-global/process": "off",
"node/prefer-global/text-decoder": "off",
"node/prefer-global/text-encoder": "off",
"node/prefer-global/url-search-params": "off",
"node/prefer-global/url": "off",
"node/prefer-promises/dns": "off",
"node/prefer-promises/fs": "off",
"node/process-exit-as-throw": "off",
"node/shebang": "off",
},
},
],
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": ["error", "never"],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "error",
"arrow-parens": "off",
"arrow-spacing": ["error", { "after": true, "before": true }],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "only-multiline",
},
],
"comma-spacing": "error",
"comma-style": ["error", "last"],
"complexity": "error",
"computed-property-spacing": ["error", "never"],
"consistent-return": "error",
"consistent-this": "error",
"curly": "error",
"default-case": "error",
"default-case-last": "error",
"default-param-last": "error",
"dot-location": ["error", "property"],
"dot-notation": ["error", { "allowPattern": "^[a-z]+(_[a-z]+)*$" }],
"eol-last": "error",
"eqeqeq": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": ["error", "declaration"],
"function-call-argument-newline": "off",
"function-paren-newline": "off",
"generator-star-spacing": ["error", { "before": false, "after": true }],
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "error",
"indent": ["error", "tab", { "SwitchCase": 1 }],
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": "error",
"line-comment-position": "off",
"linebreak-style": ["error", "unix"],
"lines-around-comment": "off",
"lines-around-directive": "off",
"lines-between-class-members": ["error", "always"],
"max-classes-per-file": "off",
"max-depth": "error",
"max-len": ["error", { "code": 120 }],
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-comment-style": ["error", "separate-lines"],
"multiline-ternary": "off",
"new-cap": "error",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-console": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-constructor-return": "error",
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-mixed-operators": [
"error",
{
"groups": [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["in", "instanceof"],
],
},
],
"no-multi-assign": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"no-proto": "error",
"no-restricted-exports": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "off",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": ["error", { "allow": ["yargs"] }],
"no-spaced-func": "error",
"no-tabs": ["error", { "allowIndentationTabs": true }],
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-use-before-define": "error",
"no-useless-backreference": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"no-void": ["error", { "allowAsStatement": true }],
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "off",
"operator-linebreak": ["error", "after", {
"overrides": {
"?": "before",
":": "before",
"||": "before",
"&&": "before",
},
}],
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "error",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-exponentiation-operator": "error",
"prefer-named-capture-group": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"quote-props": "off",
"quotes": ["error", "double", { "avoidEscape": true }],
"radix": ["error", "always"],
"require-atomic-updates": "off", // XXX Not sure why this triggers on process.title = ...
"require-await": "off",
"require-jsdoc": "off",
"require-unicode-regexp": "off",
"rest-spread-spacing": ["error", "never"],
"semi": "error",
"semi-spacing": "error",
"semi-style": "off",
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": ["error", "never"],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": ["error", "always"],
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": ["error", { "requireReturn": false }],
"vars-on-top": "error",
"wrap-iife": "error",
"wrap-regex": "off",
"yield-star-spacing": "error",
"yoda": ["error", "never", { "exceptRange": true }],
"node/callback-return": "off",
"node/exports-style": "off",
"node/file-extension-in-import": "off",
"node/global-require": "error",
"node/handle-callback-err": "error",
"node/no-callback-literal": "error",
"node/no-deprecated-api": "error",
"node/no-exports-assign": "error",
"node/no-extraneous-import": "error",
"node/no-extraneous-require": "error",
"node/no-missing-import": "error",
"node/no-missing-require": "error",
"node/no-mixed-requires": "error",
"node/no-new-require": "error",
"node/no-path-concat": "error",
"node/no-process-env": "error",
"node/no-process-exit": "error",
"node/no-restricted-require": "off",
"node/no-sync": "error",
"node/no-unpublished-bin": "error",
"node/no-unpublished-import": "error",
"node/no-unpublished-require": [
"error",
{ "allowModules": ["webpack", "webpack-merge", "webpack-dev-middleware"] },
],
"node/no-unsupported-features/es-builtins": "error",
"node/no-unsupported-features/es-syntax": "error",
"node/no-unsupported-features/node-builtins": "error",
"node/prefer-global/buffer": "error",
"node/prefer-global/console": "error",
"node/prefer-global/process": "error",
"node/prefer-global/text-decoder": "error",
"node/prefer-global/text-encoder": "error",
"node/prefer-global/url-search-params": "error",
"node/prefer-global/url": "error",
"node/prefer-promises/dns": "off",
"node/prefer-promises/fs": "off",
"node/process-exit-as-throw": "error",
"node/shebang": "error",
},
};
+1
View File
@@ -0,0 +1 @@
package-lock.json -diff
+39
View File
@@ -0,0 +1,39 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm i --no-optional
- run: wget -q -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
- run: npx lerna bootstrap --hoist
- run: npx lerna run build
- run: npm test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
- run: npm i --no-optional
- run: npx lerna bootstrap --hoist
- run: npm run lint
+16 -20
View File
@@ -11,8 +11,9 @@ pids
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
# Coverage directories used by tools like nyc and codecov
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -22,33 +23,28 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
dist/
node_modules/*
!node_modules/_app
# Autogenerated docs output
/docs/api
node_modules
Factorio*
/factorio
/instances
/sharedMods
/database
database/items.db
config.json
/plugin-list.json
/config-master.json
/config-control.json
/config-slave.json
package-lock.json
secret-api-token.txt
mod_cache.json
static/pictures/spritesheet.js
static/pictures/spritesheet.png
static/external
static/export
saves
/temp/
# weird pattern to ignore sharedPlugins except not ignore some core modules
!sharedPlugins/
sharedPlugins/*
!sharedPlugins/inventoryImports/
!sharedPlugins/UPSdisplay/
!sharedPlugins/remoteMap/
!sharedPlugins/remoteCommands/
!sharedPlugins/researchSync/
!sharedPlugins/globalChat/
\.cache-require-paths\.json
# IDE
.idea/
.vs/
+10 -5
View File
@@ -1,10 +1,15 @@
language: node_js
node_js:
- "node"
- 12
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y curl git tar
- curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
- tar -xvf factorio.tar.gz
- tar -xf factorio.tar.gz
- npm install -g codecov
script:
- npm run cover
- npx lerna bootstrap --hoist --no-ci
- node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --output-dir temp/test/sharedMods
- curl -o temp/test/sharedMods/subspace_storage_1.99.8.zip -L https://github.com/clusterio/subspace_storage/releases/download/1.99.6/subspace_storage_1.99.8.zip
- npx lerna run build
- npm run-script ci-cover
- codecov -f coverage/lcov.info
- npm run-script lint
+195
View File
@@ -0,0 +1,195 @@
# Changelog
## 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.
- Added remote save management with the ability to list, create, upload, download, delete and load saves for instances.
### 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.
- 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 stripping of long paths in the Factorio server log.
- Added option to configure maximum number of commands to send in parallel.
- Added option to auto start instances on slave startup.
- Added metric mapping of slave and instance ids to their names, allowing them to be displayed by name in queries that join with the mapping.
- Added option to 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.
- Added Node.js based installer to simplify setting up a cluster.
### Changes
- Added error handling during master startup.
- Factorio game and rcon port now defaults to a random port above 49151.
- Removed unimplemented mods update command.
- Fixed rcon password being generated with Math.random().
- 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 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 the playerManager specific command CLI tools/delete_player.js.
- Creating an instance, assigning it to a slave, creating a save for an instance and starting an instance is now four separate commands.
- Removed oddball limits to slaves.json size.
- Moved slave specific and instance specific configuration into their own configuration files.
- Removed unused binary option from plugin config.
- Removed info and shout command from globalChat plugin - Removed mirrorAllChat and enableCrossServerShout configuration options for globalChat plugin.
- Removed UPSdisplay plugin.
UPS statistics is exported by the statistics exporter plugin.
- Master server now defaults to hosting on https on port 8443.
- Renamed client to slave.
- Removed rotation of factorio-current.log files.
### Breaking Changes
- Removed lib/authenticate.
Breaks playerManager.
- The masterIP and masterPort config entries has been merged into masterURL.
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.
- 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.
- 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 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
- 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 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.
- 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.
- Renamed clusterio_endpoint_hit_gauge to clusterio_http_enpoint_hits_total
- 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_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.
- 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.
- Node.js versions below 12 are no longer supported.
## 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.
## 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.
- Updated the Windows install instructions.
## 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.
### Breaking Changes
- Node.js versions below 10 are no longer supported.
## 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]).
- Fixed package.json incorrectly reporting the license as ISC.
[#217]: https://github.com/clusterio/clusterio/issues/217
[#229]: https://github.com/clusterio/clusterio/issues/229
### Breaking Changes
- Added authentication to the socket.io server running on the master.
+57 -13
View File
@@ -1,19 +1,63 @@
FROM node:latest
RUN apt-get update && apt install git -y
FROM node:12 as subspace_storage_builder
RUN apt update && apt install -y git
WORKDIR /
RUN git clone https://github.com/clusterio/subspace_storage.git
WORKDIR /subspace_storage
RUN npm install \
&& node build
RUN mkdir factorioClusterio
RUN git clone -b dev https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xvzf factorio.tar.gz
FROM node:12 as clusterio_builder
RUN apt update \
&& apt install -y wget \
&& mkdir /clusterio
WORKDIR /clusterio
RUN wget -q -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
# Copy project files into the container
COPY . .
WORKDIR factorioClusterio
RUN mkdir instances sharedMods sharedPlugins
RUN npm install \
&& npx lerna bootstrap --hoist
RUN node client.js download
# Install plugins. This is intended as a reasonable default, enabling plugins to make for fun gameplay.
# If you want a different set of plugins, consider using this as the base image for your own.
#RUN npm install @clusterio/plugin-subspace_storage
#RUN npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
LABEL maintainer "danielv@live.no"
COPY --from=subspace_storage_builder /subspace_storage/dist/ /clusterio/sharedMods/
EXPOSE 8080 34167
VOLUME /factorioClusterio/instances
VOLUME /factorioClusterio/sharedMods
VOLUME /factorioClusterio/sharedPlugins
# Build submodules (web UI, libraries, plugins etc)
RUN npx lerna run build
# Build Lua library mod
RUN node packages/lib/build_mod --build --source-dir packages/slave/lua/clusterio_lib \
&& mv dist/* sharedMods/ \
&& mkdir temp \
&& mkdir temp/test \
&& cp sharedMods/ temp/test/ -r \
&& ls sharedMods
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
# Remove node_modules
RUN find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \;
FROM frolvlad/alpine-glibc AS clusterio_final
RUN apk add --update bash nodejs npm
COPY --from=clusterio_builder /clusterio /clusterio
WORKDIR /clusterio
# Install runtime dependencies
RUN npm install --production
RUN npx lerna bootstrap -- --production --no-optional
LABEL maintainer "danielv@danielv.no"
FROM frolvlad/alpine-glibc AS clusterio_testing
RUN apk add --update bash nodejs npm
COPY --from=clusterio_builder /clusterio /clusterio
WORKDIR /clusterio
# Install runtime dependencies
RUN npm install
RUN npx lerna bootstrap
RUN npm install chalk semver
+1 -1
View File
@@ -1,4 +1,4 @@
MIT License
This product uses the MIT License
Copyright (c) 2016 Daniel Vestøl
+219 -124
View File
@@ -1,209 +1,304 @@
# factorioClusterio
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio?ref=badge_shield)
<img src="./logo.svg" width="100%" align="right">
| Branch | Coverage | Build |
|--------|----------| ----- |
| Master | [![codecov](https://codecov.io/gh/Danielv123/factorioClusterio/branch/master/graph/badge.svg)](https://codecov.io/gh/Danielv123/factorioClusterio) | [![Build Status](https://travis-ci.org/Danielv123/factorioClusterio.svg?branch=master)](https://travis-ci.org/Danielv123/factorioClusterio) |
| Dev | [![codecov](https://codecov.io/gh/Danielv123/factorioClusterio/branch/dev/graph/badge.svg)](https://codecov.io/gh/Danielv123/factorioClusterio) | [![Build Status](https://travis-ci.org/Danielv123/factorioClusterio.svg?branch=dev)](https://travis-ci.org/Danielv123/factorioClusterio) |
<br/>
<br/>
<br/>
If you want to connect to a clusterio cluster, please reffer to the [client](https://github.com/Danielv123/factorioClusterioClient)
# Clusterio
Discord for development/support/play: https://discord.gg/5XuDkje
## Important notice
Clusterio 2.0 is still in alpha, however the previous stable has been abandoned and is no longer supported.
Despite being alpha it's reasonably stable now and there's no major breakages expected before a stable release of 2.0.
If you are starting a new cluster it's highly recommended to use the 2.0 alpha.
### 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.
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
### Table of contents
* [Introduction & methodology](#introduction)
* [Introduction](#introduction)
* [Features](#features)
* [Plugins](#plugins)
* [Installation](#installation)
* [Ubuntu setup](#ubuntu-setup)
* [Windows setup](#windows-setup)
* [MacOS setup](#macos-setup)
* [Installing Plugins](#installing-plugins)
* [Configure Master Server](#configure-master-server)
* [Managing Factorio mods](#managing-factorio-mods)
* [Setting up shared storage](#setting-up-shared-storage)
* [Running Clusterio](#running-clusterio)
* [Setting up remote slaves](#setting-up-remote-slaves)
* [Setting up clusterioctl](#setting-up-clusterioctl)
* [Common problems](#Common-problems)
* [Ubuntu setup](#ubuntu-setup)
* [Windows setup](#windows-setup)
* [Command cheatsheet](#cheatsheet)
## Introduction
Features:
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/subspace_storage).
- Entities to send/recieve items
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.
- Cross dimensional storage
- Sending of liquids
## Features
- Sending of circuit network signals
- 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.
- Inventory combinator to display item levels in the cluster (and epoch time)
- User list management for synchronizing in-game admins, whitelisted users, and bans to all the servers in the cluster.
- Reporting of graphs and UPS on master interface
- Integrated support for exporting statistics for the whole cluster to Prometheus via a single metrics endpoint.
Connection diagram:
- Extensive plugin support for adding your own cross server features to Factorio using Clusterio's communication backbone.
![http://i.imgur.com/7FdVfgB.png](http://i.imgur.com/7FdVfgB.png)
There can be any number of clients connected to each slave, and any number of slaves connected to a master but there can only be one master server in each cluster.
## Plugins
**How does it work?**
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:
Traditional factorio mods have always been limited by the games deterministic design. This gives us a very bug free and predictable game, but doesn't allow us cool stuff such as internet communication.
Clusterio exploits one of the games logging features, game.write_file and RCON to communicate between servers. Sending an item from one server to another takes this path:
- [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/subspace_storage): Provide shared storage that can transport items between servers via teleport chests.
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
- [Inventory Sync](/plugins/inventory_sync/README.md): Synchronizes your inventory between servers.
1. server1: Chest has stuff in it, write the contents to a file and delete them from the game world
There's also plugins developed and maintained by the community:
2. client.js on server1: File has stuff in it, parse and send to the master for storage
- [Discord Bridge](https://github.com/Hornwitser/discord_bridge) (@hornwitser/discord_bridge): Bridges chat between instances and Discord.
By Hornwitser.
- [Server Select](https://github.com/Hornwitser/server_select/tree/clusterio-2.0) (@hornwitser/server_select): In-game GUI for connecting to other server in the cluster.
Originally by Godmave, ported to 2.0 by Hornwitser.
3. master: server1 gave us stuff, store it in the storage and write some statistics
Want to make your own plugin?
Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for where to start.
4. server2: get-chest is empty, write a request to file
5. client.js on server2: Request file has stuff in it, parse and send a request to master for more items of that type
## Installation
6. master: server2 asked for stuff, check if we have enough and how much demand there is, then send however much is appropriate back
Clusterio runs on Node.js v12 and up, it's distributed via npm and comes with a guided install script.
If you alread have a recent Node.js installed you can set it up in a new directory with.
7. client.js on server2: We were allowed to import x of item y, run command /c remote.call("clusterio", "importMany", "{'copper-plate':120}")
npm init "@clusterio"
This process works the same for both items and liquids, independant on what mods are used. Yes, modded items are fully supported.
Otherwise see below for OS specific instructions.
Clusterio also handles a few other neat things, such as giving you access to epoch time, transmitting combinator signals between worlds (and any other application who wants to) and
creating graphs on the master web interface.
## Ubuntu setup
### Ubuntu setup
NodeJS does not support EOL ubuntu releases. Make sure you are on the most recent LTS release or newer.
1. Install Node.js v12 or higher.
For 20.04 LTS or below the version of Node.js provided by the Ubuntu repos are too old and you will have to use the nodesource PPA, otherwise you may skip the first line.
Master and all slaves:
wget -qO - https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt install nodejs
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt install -y nodejs python-dev git wget curl tar build-essential
sudo npm install pm2 -g
git clone https://github.com/Danielv123/factorioClusterio.git
cd factorioClusterio
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.gz
npm install
node ./lib/npmPostinstall.js
2. Create a new directory and run the Clusterio installer:
downloads and installs nodejs, pm2, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
mkdir clusterio
cd clusterio
npm init "@clusterio"
*Copy config.json.dist to config.json* - Otherwise it will crash, as you'd expect.
Make sure to note down the admin authentication token it provides at the end as you will need it later.
**Master**
3. If you chose to use local factorio directory for the Factorio installation then download the headless build of Factorio and unpack it:
pm2 start master.js --name master
**Server Host**
To download the mod for all its non vanilla features and items, (optional, but very recommended)
wget -O factorio.tar.xz https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.xz
node client.js manage shared mods add clusterio
Add the master API key, found in the masters install directory (secret-api-token.txt) to your config.json masterAuthToken field
To specify a version of Factorio to download replace "latest" in the URL with a version number like "1.0.0".
To create a new instance (its own save, set of mods and config files)
4. Optionally copy the generated systemd service files in `systemd` folder to `/etc/systemd/system/`.
node client.js start [instancename]
To launch an instance with pm2
pm2 start --name [instancename] client.js -- start [instancename]
use `nano config.json` to change settings.
**Ubuntu with Docker**
Clusterio has limited support for using docker.
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.
sudo docker build -t clusterio --no-cache --force-rm clusterio
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
sudo docker run --name slave -e MODE=client -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.
sudo docker run --name slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/clusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
## Windows setup
The -v flag is used to specify the instance directory.
Your instances (save files etc) will be stored there.
-->
Clusterio is built up of multiple parts. Here is a quick guide:
### Windows setup
Master = master.js
1. Download and install the latest LTS release from http://nodejs.org.
Server host (Slave) = client.js + factorio server
2. Create a new empty directory for the installation and navigate into it.
Open a PowerShell window in this new directory by Shift+Right clicking inside it and choosing "Open PowerShell window here" and then run the following command.
Game Client = The people connecting to the server
npm init "@clusterio"
All Server Hosts (Slaves) AND Game Clients need to be running the clusterio mod located at [github](https://github.com/Danielv123/factorioClusterioMod) Install it by dropping it into either the sharedMods folder or instances/[instanceName]/instanceMods folder.
Make sure to note down the admin authentication token it provides at the end as you will need it later.
There are no requirements for other mods, they can be ran in any configuration allowed by the base game.
3. If you chose to use local factorio directory for the Factorio installation then download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder.
**Requirements**
download and install nodeJS 8 or 9 from http://nodejs.org
### MacOS Setup
download and install git from https://git-scm.com/
1. Install the latest Node.js LTS release from http://nodejs.org or use brew (`brew install node`).
reboot when you are done, then proceed to the next steps. *reboots matter*
2. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
**Master**
mkdir clusterio
cd clusterio
npm init "@clusterio"
1. download and run https://puu.sh/toFHl/01eebbb333.bat
Make sure to note down the admin authentication token it provides at the end as you will need it later.
2. Copy config.json.dist to config.json
3. If you chose to use local factorio directory for the Factorio installation you will need to obtain and copy a mac version of Factorio and unpack it to to the `factorio` folder in your clusterio installation folder.
3. Follow the instructions in the bat file
4. Run `node client.js manage shared mods add clusterio`
### Installing Plugins
5. type `node master.js` to start the server
For well known plugins you can select them during installation and no further steps are necessary to make them work.
Installing plugins that are not offered by the installer consists of two steps.
First install the package the plugin is provided by via npm, for example
**Server Host**
npm install @clusterio/plugin-subspace_storage
1. download and run https://puu.sh/toFHl/01eebbb333.bat
Then tell clusterio that this plugin exists by adding it as a plugin.
2. Copy config.json.dist to config.json
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
3. Follow the instructions given
This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by master, slave and ctl.
If you have slaves or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them.
The clusteriomaster, clusterioslave and clusterioctl commands has the plugin sub-command so you do not need to install clusteriomaster to add plugins, instead use the clusterio command you have available.
4. Type `node client.js start [instancename]` to create a new instance.
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).
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
Change masterIP to something like 31.152.123.14 (provided by master server owner)
e war
Change masterPort to something like 8080 (provided by master server owner)
## Configure Master Server
Repeat step 3 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
By default the master server will listen for HTTP on port 8080.
You can change the port used with the command
**GameClient**
npx clusteriomaster config set master.http_port 1234
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
When changing the port you will also need to change the address slaves connect with.
For the standalone installation mode you can use
1. Download the same version of the mod as the slave is running from [the mod portal](https://mods.factorio.com/mods/Danielv123/clusterio) or [github](https://github.com/Danielv123/factorioClusterioMod
npx clusterioslave config set slave.master_url http://localhost:1234/
2. Drop it into ./factorio/mods
If you plan to make your cluster available externally set the address
that it will be accessible under with, for example
3. Run factorio and connect to slave as a normal MP game. You will find the port number to connect to at http://[masterAddress]:8080
npx clusteriomaster config set master.external_address http://203.0.113.4:1234/
## Cheatsheet
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.
**To create a new instance/start it**
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.
node client.js start [instanceName]
**Other instance management tools:**
```
node client.js delete [instanceName]
node client.js list
```
**To update clusterio to the latest version:**
## Managing Factorio mods
1. Download the latest zip version of factorio for your platform manually from factorio.com. Place it in the project root folder and call it "factorio" (folder name is specified in config.json)
Clusterio currently only provides a manual method for running mods on servers by copying them either to each instance's individual `mods` folder, or by copying them into a slave installation's `sharedMods` folder which will link/copy them to the `mods` folder of each instance on that slave if the file doesn't already exist in the `mods` folder for that instance.
Mods are copied from `sharedMods` on Windows and symlinked on Linux, this means that when mods are removed from `sharedMods` the mods will also be removed from the instance's `mods` folder on Linux if it got there by being linked by Clusterio, but not on Windows.
2. Grab the latest version of the repo
For managing the mod settings the `mod-settings.dat` and `mod-list.json` files will also be linked/copied over if present in `sharedMods`, but since Factorio creates these files automatically it might be necessary to delete them from each instance's `mods` folder for changes to them in `sharedMods` to take effect.
```
git pull
Better remote management of mods is a planned feature.
npm install
```
3. Download the latest version of the factorioClusterioMod from its github repo
```
node client.js manage shared mods add clusterio
```
## Setting up shared storage
The chests teleporting items to and from the shared storage on the master server has been moved into the Subspace Storage mod and plugin.
To get this to work you will need to install the Subspace Storage plugin and copy the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) into the `sharedMods` folder on each slave installation.
Do not install the [Clusterio mod](https://mods.factorio.com/mod/clusterio), this was for the old 1.2.x version of Clusterio and is not compatible with Clusterio 2.0.
## Running Clusterio
After completing the installation start up the master server and at least one slave separately.
The installer provides the `run-master` and `run-slave` scripts to make this simple.
Once the master process is running you can log into the web interface which is hosted by default on http://localhost:8080/ (adjust the port number if you changed it), use the admin authentication token provided from the installation to log in.
The basics of setting up a Factorio server from the web interface is to create an instance, assign it to a slave and then click start.
### Running via Systemd
The install script creates systemd service scripts for clusteriomaster and clusterioslave (if applicable) to start up as the user than ran the installer.
If you have copied these files over to `/etc/systemd/system/` then you can startup clusterio as a background service using:
#### To start as a service run:
sudo systemctl start clusteriomaster # for the master server
sudo systemctl start clusterioslave # for each server hosting a slave
#### To automatically get it to start on boot:
sudo systemctl enable clusteriomaster # for the master server
sudo systemctl enable clusterioslave # for each server hosting a slave
## Setting up remote slaves
Run the installer as described in the installation section and choose "Slave only" as the operating mode to install, it'll ask for a master URL and an authentication token.
The URL is the same as what is needed to connect to the web interface, and the athentication token can be generated on the Slaves page in the web interface.
Once you start up the slave it should show up in the Slaves list and be available for assigning and running instances on.
## Setting up clusterioctl
There's a command line interface available for Clusterio which is installed separately with the same installer as for the master and slave.
Run the installer as described in the installation section and choose "Ctl only" as the operating mode to install, you can do this in the same directory as you have installed other clusterio component(s) to.
The installer will ask for a master URL and an authentication token, these are the same as you would use to connect to the web interface.
If you want to use a different user for the command line interface you can generate an authentication token for an existing user with
npx clusteriomaster bootstrap generate-user-token <username>
## 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.
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:
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
### 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.
TLDR: the tested fix is:
- open your windows firewall and go to advanced settings
- click on inbound rules and click on new rule...
- select port and click next >
- select TCP and select specific local ports and type in the ports that you want to open (comma separated) and click next > 3 times
- give the rule a name (like 'web server' or something), give it a description (optionally) and click finish
-999
View File
@@ -1,999 +0,0 @@
const fs = require('fs-extra');
const https = require('follow-redirects').https;
const needle = require("needle");
const child_process = require('child_process');
const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const ncp = require('ncp').ncp;
const Rcon = require('rcon-client').Rcon;
const hashFiles = require('hash-files');
const _ = require('underscore');
const deepmerge = require("deepmerge");
const getMac = require('getmac').getMac;
const rmdirSync = require('rmdir-sync');
const ioClient = require("socket.io-client");
// internal libraries
const objectOps = require("./lib/objectOps.js");
const fileOps = require("_app/fileOps");
const stringUtils = require("./lib/stringUtils.js");
const modManager = require("./lib/manager/modManager.js");
const configManager = require("./lib/manager/configManager.js");
// require config.json
var config = require('./config');
var global = {};
if (!fs.existsSync(config.instanceDirectory)) {
fs.mkdirSync(config.instanceDirectory);
}
if (!fs.existsSync("./sharedPlugins/")) {
fs.mkdirSync("sharedPlugins");
}
if (!fs.existsSync("./sharedMods/")) {
fs.mkdirSync("sharedMods");
}
const instance = process.argv[3];
const instancedirectory = config.instanceDirectory + '/' + instance;
const command = process.argv[2];
// Set the process title, shows up as the title of the CMD window on windows
// and as the process name in ps/top on linux.
process.title = "clusterioClient "+instance;
// make sure we have the master access token (can't write to master without it since clusterio 2.0)
if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
console.log("ERROR invalid config!");
console.log("Master server now needs an access token for write operations. As clusterio slaves depends \
upon this, please add your token to config.js in the field named masterAuthToken. \
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
}
const needleOptionsWithTokenAuthHeader = {
headers: {
'x-access-token': config.masterAuthToken
},
};
var instanceInfo = {};
var commandBuffer=[];
// messageInterface Management
setInterval(function(){
let command=commandBuffer.shift();
if(command){
messageInterfaceInternal(command[0], command[1], command[2], command[3]);
}
},config.msBetweenCommands || 50);
// function to handle sending commands into the game
async function messageInterfaceInternal(command, callback, resolve, reject) {
// try to save us if you send a buffer instead of string
if(typeof command == "object") {
command = command.toString('utf8');
}
if(false && process.platform == "linux" && typeof command == "string" && serverprocess) {
/*
to send to stdin, use:
serverprocess.stdin.write("/c command;\n")
*/
serverprocess.stdin.write(command+"\n");
if(typeof callback == "function"){
callback();
}
resolve();
} else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
try {
let str = await client.send(command+"\n");
if(typeof callback == "function") callback(str)
resolve(str)
} catch (err) {
console.log("RCON failed, server might be paused or shutting down");
// console.log(err);
// reject(err);
if(typeof callback == "function"){
callback();
}
}
}
}
function messageInterface(command, callback) {
return new Promise((resolve,reject) => {
commandBuffer.push([command,callback, resolve, reject]);
});
}
// handle commandline parameters
if (!command || command == "help" || command == "--help") {
console.error("Usage: ");
console.error("node client.js start [instance name]");
console.error("node client.js list");
console.error("node client.js delete [instance]");
console.error("To download the latest version of the Clusterio lua mod, do");
console.error("node client.js manage shared mods download clusterio");
console.error("For more management options, do");
console.error("node client.js manage");
process.exit(1);
} else if (command == "list") {
let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory);
instanceNames.unshift("Name:");
let longestInstanceName = 0;
// determine longest instance name
instanceNames.forEach(function(instance){
if(instance.length > longestInstanceName) longestInstanceName = instance.length;
});
let displayLines = [];
// push name coloumn to array
instanceNames.forEach(function(instance){
while(instance.length < longestInstanceName+1){
instance += " ";
}
displayLines.push("| "+ instance + "|");
});
// create port colloumn
let factorioPorts = [];
instanceNames.forEach(function(instance){
let factorioPort
if(instance.includes("Name:")){
factorioPort = "Port:"
} else {
factorioPort = require(config.instanceDirectory+"/"+instance+"/config").factorioPort;
}
factorioPorts.push(factorioPort);
});
factorioPorts.forEach((port, index) => {
let longestPort = 0;
factorioPorts.forEach((port, index) => {
if(port.toString().length > longestPort) longestPort = port.toString().length;
});
while(port.toString().length < longestPort){
port += " ";
}
factorioPorts[index] = port;
});
instanceNames.forEach(function(instance, index){
displayLines[index] += " " + factorioPorts[index] + " |";
});
displayLines.forEach(line => console.log(line));
process.exit(0);
} else if (command == "manage"){
// console.log("Launching mod manager");
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
function usage(instance, tool, action){
if(tool && tool == "mods"){
console.log('node client.js manage '+instance+' '+tool+' ["list", "search", "add", "remove", "update"]');
} else if(tool && tool == "config") {
console.log('node client.js manage '+instance+' '+tool+' ["list", "edit"]');
} else {
console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config"]') + ' ...');
}
}
const tool = process.argv[4] || "";
const action = process.argv[5] || "";
if(instance){
if(tool == "mods"){
(async function(){try{
// allow managing mods
if(action == "list"){
console.log(await modManager.listMods(instance));
} else if(action == "search"){
console.log(await modManager.findMods(process.argv[6]));
} else if(action == "add" || action == "download"){
await modManager.addMod(process.argv[6], instance);
} else if(action == "remove" || action == "rm" || action == "delete"){
await modManager.removeMod(process.argv[6], instance);
} else if(action == "update"){
await modManager.updateAllMods();
} else {
usage(instance, tool);
}
process.exit(0);
}catch(e){
console.log("Got error from modManager:")
console.log(e);
}})();
} else if(tool == "config"){
// allow managing the config
if(action == "list" || action == "show" || action == "display"){
configManager.displayConfig(instance);
} else if(action == "edit"){
let newConfigValue = "";
process.argv.forEach((arg, i)=>{
if(i >= 8){
newConfigValue += " "+arg;
} else if(i >= 7){
newConfigValue += arg;
}
});
configManager.editConfig(instance, process.argv[6], newConfigValue);
} else {
usage(instance, tool);
}
} else {
usage(instance);
}
} else {
console.log('Usage:');
usage(instance);
}
// process.exit(0);
} else if (command == "delete") {
if (!process.argv[3]) {
console.error("Usage: node client.js delete [instance]");
process.exit(1);
} else if (typeof process.argv[3] == "string" && fs.existsSync(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
fileOps.deleteFolderRecursiveSync(config.instanceDirectory+"/" + process.argv[3]);
console.log("Deleted instance " + process.argv[3]);
process.exit(0);
} else {
console.error("Instance not found: " + process.argv[3]);
process.exit(0);
}
} else if (command == "download") {
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log(url);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(url, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
process.exit(0);
});
}
} else if (command == "start" && instance === undefined) {
console.log("ERROR: No instanceName provided!");
console.log("Usage: node client.js start [instanceName]");
process.exit(0);
} else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) {
// if instance does not exist, create it
console.log("Creating instance...");
fs.mkdirSync(instancedirectory);
fs.mkdirSync(instancedirectory + "/script-output/");
fs.mkdirSync(instancedirectory + "/saves/");
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
fs.mkdirSync(instancedirectory + "/mods/");
fs.mkdirSync(instancedirectory + "/instanceMods/");
// fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
`);
// this line is probably not needed anymore but Im not gonna remove it
fs.copySync('sharedMods', instancedirectory + "/mods");
let instconf = {
"factorioPort": process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
"clientPort": process.env.RCONPORT || Math.floor(Math.random() * 65535),
"clientPassword": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
}
console.log("Clusterio | Created instance with settings:")
console.log(instconf);
// create instance config
fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4));
let name = "Clusterio instance: " + instance;
if (config.username) {
name = config.username + "'s clusterio " + instance;
}
let serversettings = {
"name": name,
"description": config.description,
"tags": ["clusterio"],
"max_players": "20",
"visibility": config.visibility,
"username": config.username,
"token": config.token,
"game_password": config.game_password,
"verify_user_identity": config.verify_user_identity,
"admins": [config.username],
"allow_commands": config.allow_commands,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": config.auto_pause,
}
fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4));
let createSave = child_process.spawnSync(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
'--create', instancedirectory + '/saves/save.zip',
]
);
console.log("Instance created!");
process.exit(0);
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) {
// Exit if no instance specified (it should be, just a safeguard);
if(instancedirectory != config.instanceDirectory+"/undefined"){
var instanceconfig = require(path.resolve(instancedirectory,'config'));
instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
if(process.env.FACTORIOPORT){
instanceconfig.factorioPort = process.env.FACTORIOPORT;
}
if(process.env.RCONPORT){
instanceconfig.rconPort = process.env.RCONPORT;
}
} else {
process.exit(1);
}
console.log("Deleting .tmp.zip files");
let savefiles = fs.readdirSync(instancedirectory + "/saves/");
for(i = 0; i < savefiles.length; i++){
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
fs.unlinkSync(instancedirectory + "/saves/" + savefiles[i]);
}
}
console.log("Deleting logs");
// clean old log file to avoid crash
// file exists, delete so we don't get in trouble
try {
fs.unlinkSync(instancedirectory+'/factorio-current.log');
} catch (err){
if(err){
console.log(err);
} else {
console.log("Clusterio | Deleting old logs...");
}
}
// move mods from ./sharedMods to the instances mod directory
try{fs.mkdirSync(instancedirectory + "/instanceMods/");}catch(e){}
try{rmdirSync(instancedirectory + "/mods");}catch(e){}
try {
// mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
fs.mkdirSync(instancedirectory + "/mods/");
} catch(e){}
console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
fs.copySync('sharedMods', instancedirectory + "/mods");
console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
fs.copySync(instancedirectory + "/instanceMods", instancedirectory + "/mods");
process.on('SIGINT', function () {
console.log("Caught interrupt signal, sending /quit");
messageInterface("/quit");
});
// Spawn factorio server
//var serverprocess = child_process.exec(commandline);
fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
if(err) {
console.error("ERROR!")
console.error("Your savefile seems to be missing. This might because you created an instance without having factorio\
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/\n");
throw err;
}
// implicit global
serverprocess = child_process.spawn(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
'--start-server', latestSave.file,
'--rcon-port', Number(process.env.RCONPORT) || instanceconfig.clientPort,
'--rcon-password', instanceconfig.clientPassword,
'--server-settings', instancedirectory + '/server-settings.json',
'--port', Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
], {
'stdio': ['pipe', 'pipe', 'pipe']
}
);
serverprocess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
process.exit();
});
serverprocess.stdout.on("data", (data) => {
// log("Stdout: " + data);
if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){
console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!");
console.error("Attempting reconnect...");
client.close();
client.connect();
}
// we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
console.log('Fact: ' + data);
});
serverprocess.stderr.on('data', (chunk) => {
console.log('ERR: ' + chunk);
});
// connect to the server with rcon
if(true || process.platform != "linux"){
// IP, port, password
client = new Rcon();
// check the logfile to see if the RCON interface is running as there is no way to continue without it
// we read the log every 2 seconds and stop looping when we start connecting to factorio
function checkRcon() {
fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
// if (err) console.log(err);
if(data && data.indexOf('Starting RCON interface') > 0){
client.connect({
host: 'localhost',
port: Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: instanceconfig.clientPassword,
timeout: 5000
});
} else {
setTimeout(function(){
checkRcon();
},5000);
}
});
}
setTimeout(checkRcon, 5000);
client.onDidAuthenticate(() => {
console.log('Clusterio | RCON Authenticated!');
instanceManagement(); // start using rcons
});
client.onDidConnect(() => {
console.log('Clusterio | RCON Connected!');
// getID();
});
client.onDidDisconnect(() => {
console.log('Clusterio | RCON Disconnected!');
process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
});
} else if(process.platform == "linux"){
// don't open an RCON connection and just use stdio instead, does not work on windows.
instanceManagement();
}
// set some globals
confirmedOrders = [];
lastSignalCheck = Date.now();
});
}
// ensure instancemanagement only ever runs once
_.once(instanceManagement);
function instanceManagement() {
/* Open websocket connection to master */
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort);
socket.on("hello", data => {
console.log("SOCKET | registering slave!");
socket.emit("registerSlave", {
instanceID: instanceconfig.unique,
});
setInterval(B=> socket.emit("heartbeat"), 10000);
});
console.log("Started instanceManagement();");
// load plugins and execute onLoad event
let pluginDirectories = fileOps.getDirectoriesSync("./sharedPlugins/");
let plugins = [];
for(let i=0; i<pluginDirectories.length; i++) {
let I = i
let log = function(message) {
console.log("Clusterio | "+ pluginDirectories[I] + " | " + message);
}
// these are our two config files. We need to send these in case plugin
// wants to contact master or know something.
let combinedConfig = deepmerge(instanceconfig,config,{clone:true})
let pluginConfig = require("./sharedPlugins/" + pluginDirectories[i] + "/config.js");
if(!global.subscribedFiles) {
global.subscribedFiles = {};
}
if(pluginConfig.binary == "nodePackage"){
// require index.js.main() of plugin and execute it as a class
let pluginClass = require("./sharedPlugins/" + pluginDirectories[I] + "/index.js");
plugins[I] = new pluginClass(combinedConfig, async function(data, callback){
if(data.toString('utf8')[0] != "/") {
log("Stdout: " + data.toString('utf8'));
return true;
} else {
return messageInterface(data.toString('utf8'), callback);
}
}, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
socket, // socket.io connection to master (and ES6 destructuring, yay)
});
if(plugins[I].factorioOutput && typeof plugins[I].factorioOutput === "function"){
// when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
serverprocess.stdout.on("data", data => plugins[I].factorioOutput(data.toString()));
}
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
// between those plugins instead.
throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
}
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
// Do something
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
}
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
// run once in case a plugin wrote out information before the plugin loaded fully
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
// send file contents to plugin for processing
function fileChangeHandler(eventType, filename) {
if(filename != null){
setTimeout(
()=>{
// get array of lines in file
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for(let i = 0; i < stuff.length; i++) {
if(stuff[i] && !stuff[i].includes('\u0000\u0000')) {
try{
plugins[I].scriptOutput(stuff[i]);
}catch(e){console.log(e)}
}
}
},
pluginConfig.fileReadDelay || 0
);
}
}
}
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
} else if(pluginConfig.binary != "nodePackage" && pluginConfig.binary){
// handle as fragile executable plugin
let args = pluginConfig.args || [];
plugins[I]=child_process.spawn(pluginConfig.binary, args, {
cwd: "./sharedPlugins/"+pluginDirectories[i],
stdio: ['pipe', 'pipe', 'pipe'],
});
/*
to send to stdin, use:
spawn.stdin.write("text\n");
*/
// If plugin has subscribed to a file, send any text appearing in that file to stdin
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string") {
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
// between those plugins instead.
throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
}
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
// Do something
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
}
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, function (eventType, filename) {
// get array of lines in file
// TODO async
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for (let i = 0; i < stuff.length; i++) {
if (stuff[i]) {
plugins[I].stdin.write(stuff[i]);
}
}
});
}
// these are our two config files. We need to send these in case plugin
// wants to contact master or know something.
// send through script-output file, maybe more compat?
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, JSON.stringify(combinedConfig)+"\r\n");
// send directly through stdin
// plugins[i].stdin.write(JSON.stringify(combinedConfig)+"\n");
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
plugins[i].stdout.on("data", (data) => {
if(data.toString('utf8')[0] != "/") {
log("Stdout: " + data.toString('utf8'))
} else {
messageInterface(data.toString('utf8'));
}
});
plugins[i].stderr.on("data", (data) => {
log("STDERR: " + data);
});
plugins[i].on('close', (code) => {
log(`child process exited with code ${code}`);
});
} else {
// This plugin doesn't have a client portion
}
}
// world IDs ------------------------------------------------------------------
hashMods(instance, function(modHashes){
setInterval(getID, 10000);
getID();
function getID() {
messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
var payload = {
time: Date.now(),
rconPort: instanceconfig.clientPort,
rconPassword: instanceconfig.clientPassword,
serverPort: instanceconfig.factorioPort,
unique: instanceconfig.unique,
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
mods:modHashes,
instanceName: instance,
playerCount:playerCount.replace(/(\r\n\t|\n|\r\t)/gm, ""),
}
function callback(err, mac) {
if (err) {
mac = "unknown";
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
}
payload.mac = mac;
console.log("Registered our precense with master "+config.masterIP+" at " + payload.time);
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if (err && err.code != "ECONNRESET"){
console.error("We got problems, something went wrong when contacting master");
console.error(err);
} else if (response && response.body) {
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
console.log(response.body);
}
});
}
if(global.mac){
callback(undefined, global.mac);
} else {
getMac(callback);
}
});
}
});
// Mod uploading and management -----------------------------------------------
// get mod names and hashes
// string: instance, function: callback
setTimeout(function(){hashMods(instance, uploadMods)}, 5000);
function uploadMods(modHashes) {
// [{modName:string,hash:string}, ... ]
for(let i=0;i<modHashes.length;i++){
let payload = {
modName: modHashes[i].modName,
hash: modHashes[i].hash,
}
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if(err) console.log(new Error("Unable to contact master server! Please check your config.json."));
if(response && body && body == "found") {
console.log("master has mod "+modHashes[i].modName);
} else if (response && body && typeof body == "string") {
let mod = response.body;
if(config.uploadModsToMaster){
console.log("Sending mod: " + mod);
// Send mods master says it wants
// response.body is a string which is a modName.zip
var req = request.post({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod',
headers: {
"x-access-token": config.masterAuthToken,
},
}, function (err, resp, body) {
if (err) {
console.log(new Error("Unable to contact master server! Please check your config.json."));
} else {
console.log('URL: ' + body);
}
});
var form = req.form();
form.append('file', fs.createReadStream(config.instanceDirectory+"/"+instance+"/mods/"+mod));
} else {
console.log("Not sending mod: " + mod + " to master because config.uploadModsToMaster is not enabled")
}
}
});
}
}
// flow/production statistics ------------------------------------------------------------
oldFlowStats = false
setInterval(function(){
fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) {
if(!err && data) {
let timestamp = Date.now();
data = data.split("\n");
let flowStats = [];
for(let i = 0; i < data.length; i++) {
// try catch to remove any invalid json
try{
flowStats[flowStats.length] = JSON.parse(data[i]);
} catch (e) {
// console.log(" invalid json: " + i);
// some lines of JSON are invalid but don't worry, we just filter em out
}
}
// fluids
let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts
// items
let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts
// merge fluid and item flows
let totalFlows = {};
for(let key in flowStat1) totalFlows[key] = flowStat1[key];
for(let key in flowStat2) totalFlows[key] = flowStat2[key];
if(oldFlowStats && totalFlows && oldTimestamp) {
let payload = objectOps.deepclone(totalFlows);
// change from total reported to per time unit
for(let key in oldFlowStats) {
// get production per minute
payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000);
if(payload[key] < 0) {
payload[key] = 0;
}
}
for(let key in payload) {
if(payload[key] == '0') {
delete payload[key];
}
}
console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) {
// we did it, keep going
});
}
oldTimestamp = timestamp;
oldFlowStats = totalFlows;
fs.writeFileSync(instancedirectory + "/script-output/flows.txt", "");
}
});
// we don't need to update stats quickly as that could be expensive
}, 60000*5);
// provide items --------------------------------------------------------------
// trigger when something happens to output.txt
fs.watch(instancedirectory + "/script-output/output.txt", function (eventType, filename) {
// get array of lines in file
let items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n");
// if you found anything, reset the file
if (items[0]) {
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
}
for (let i = 0; i < items.length; i++) {
if (items[i]) {
let g = items[i].split(" ");
g[0] = g[0].replace("\u0000", "");
// console.log("exporting " + JSON.stringify(g));
// send our entity and count to the master for him to keep track of
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', {
name: g[0],
count: g[1],
instanceName: instance, // name of instance
instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password
}, needleOptionsWithTokenAuthHeader, function (err, resp, body) {
if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]);
if(config.logItemTransfers){
if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`);
}
});
}
}
});
// request items --------------------------------------------------------------
setInterval(function () {
// get array of lines in file
let items = fs.readFileSync(instancedirectory + "/script-output/orders.txt", "utf8").split("\n");
// if we actually got anything from the file, proceed and reset file
if (items[0]) {
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
// prepare a package of all our requested items in a more tranfer friendly format
var preparedPackage = {};
for (let i = 0; i < items.length; i++) {
(function (i) {
if (items[i]) {
items[i] = items[i].split(" ");
items[i][0] = items[i][0].replace("\u0000", "");
items[i][0] = items[i][0].replace(",", "");
if (preparedPackage[items[i][0]]) {
// if we have buffered some already, sum the new items
if (typeof Number(preparedPackage[items[i][0]].count) == "number" && typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(preparedPackage[items[i][0]].count) + Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
// else just add em in without summing
} else if (typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
}
// this condition will NEVER be triggered but we know how that goes
} else if (typeof Number(items[i][1]) == "number") {
preparedPackage[items[i][0]] = {
"name": items[i][0],
"count": Number(items[i][1]),
"instanceName":instance,
"instanceID":instanceconfig.unique,
};
}
}
})(i);
}
// request our items, one item at a time
for (let i = 0; i < Object.keys(preparedPackage).length; i++) {
// console.log(preparedPackage[Object.keys(preparedPackage)[i]]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) {
if (response && response.body && typeof response.body == "object") {
// buffer confirmed orders
confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count}
if(config.logItemTransfers){
console.log(`Imported ${response.body.count} ${response.body.name} from master`);
}
}
});
}
// if we got some confirmed orders
// console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders));
//if (!(confirmedOrders.length>0)){return;}
let cmd="local t={";
for(let i=0;i<confirmedOrders.length;i++)
{
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
if(cmd.length>320) // Factorio max packet size is 508
{
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
cmd="local t={";
}
}
if (!(cmd==="local t={")){
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
}
confirmedOrders=[];
}
}, 1000);
// COMBINATOR SIGNALS ---------------------------------------------------------
// get inventory from Master and RCON it to our slave
setInterval(function () {
needle.get(config.masterIP + ":" + config.masterPort + '/api/inventory', function (err, response, body) {
if(err){
console.log("Unable to get JSON master/api/inventory, master might be unaccessible");
} else if (response && response.body) {
// Take the inventory we (hopefully) got and turn it into the format LUA accepts
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver
try {
var inventory = JSON.parse(response.body);
var inventoryFrame = {};
for (let i = 0; i < inventory.length; i++) {
inventoryFrame[inventory[i].name] = Number(inventory[i].count);
if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
}
}
inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
// console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
let first = true;
let cmd="local s={";
for (let key in inventoryFrame)
{
cmd+='["'+key+'"]='+inventoryFrame[key]+",";
if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508
{
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
cmd="local s={";
first = false;
}
}
if (!(cmd==="local s={")){
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
}
messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')");
} catch (e){
console.log(e);
}
}
});
}, 1550);
// Make sure world has its worldID
setTimeout(function(){
messageInterface("/silent-command remote.call('clusterio','setWorldID',"+instanceconfig.unique+")")
}, 20000);
/* REMOTE SIGNALLING
* send any signals the slave has been told to send
* Fetch combinator signals from the server
*/
socket.on("processCombinatorSignal", circuitFrameWithMeta => {
if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){
messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')");
}
});
// get outbound frames from file and send to master
// get array of lines in file, each line should correspond to a JSON encoded frame
let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n");
// if we actually got anything from the file, proceed and reset file
let readingTxBufferSoon = false;
let txBufferClearCounter = 0;
fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => {
if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?)
readingTxBufferSoon = true;
setTimeout(()=>{
txBufferClearCounter++;
fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => {
signals = signals.split("\n");
if (signals[0]) {
//if(txBufferClearCounter > 500){
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
// txBufferClearCounter = 0;
//}
// loop through all our frames
for (let i = 0; i < signals.length; i++) {
if (signals[i] && objectOps.isJSON(signals[i])) {
// signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master
// then we unpack and RCON in this.frame to the game later.
let framepart = JSON.parse(signals[i]);
let doneframe = {
time: Date.now(),
frame: framepart, // thats our array of objects(single signals);
}
// send to master using socket.io, opened at the top of instanceManagement()
socket.emit("combinatorSignal", doneframe);
} else {
// console.log("Invalid jsony: "+typeof signals[i])
}
}
}
});
readingTxBufferSoon = false;
},100);
}
});
} // END OF INSTANCE START ---------------------------------------------------------------------
// string, function
// returns [{modName:string,hash:string}, ... ]
function hashMods(instanceName, callback) {
if(!callback) {
throw new Error("ERROR in function hashMods NO CALLBACK");
}
function callback2(hash, modName){
hashedMods[hashedMods.length] = {
modName: modName,
hash: hash,
}
// check if this callback has ran once for each mod
if(hashedMods.length == instanceMods.length) {
callback(hashedMods);
}
//console.log(modname);
}
let hashedMods = [];
/*let mods = fs.readdirSync("./sharedMods/")*/
let instanceMods = fs.readdirSync(config.instanceDirectory+"/"+instanceName+"/mods/");
if(instanceMods.length == 0){
callback({}); // there are no mods installed, return an empty object
}
for(let o=0;o<instanceMods.length;o++) {
if(path.extname(instanceMods[o]) != ".zip") {
instanceMods.splice(instanceMods.indexOf(instanceMods[o]), 1); // remove element from array, we can't hash unzipped mods
}
}
for(let i=0; i<instanceMods.length; i++){
let path = config.instanceDirectory+"/"+instanceName+"/mods/"+instanceMods[i];
let name = instanceMods[i];
let options = {
files:path,
}
// options {files:[array of paths]}
hashFiles(options, function(error, hash) {
// hash will be a string if no error occurred
if(!error){
callback2(hash, name);
} else {
throw error;
}
});
}
}
-52
View File
@@ -1,52 +0,0 @@
{
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
"factorioDirectory": "factorio",
"instanceDirectory": "instances",
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
"publicIP": "localhost",
"__comment2": "IP the master web server runs at, can be ignored on the master system",
"masterIP": "localhost",
"__commentMasterPort": "HTTP port for the master to listen to, set to 0 or an empty string to not listen for HTTP requests",
"masterPort": 8080,
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not use HTTPS.",
"sslPort": 443,
"sslCert": "database/certificates/cert.crt",
"sslPrivKey": "database/certificates/cert.key",
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
"masterAuthToken": "",
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
"masterAuthSecret": "",
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
"username": "",
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
"token": "",
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
"factorio_version": "0.16.41",
"__comment5": "General user settings",
"verify_user_identity": true,
"auto_pause": false,
"allow_commands": "admins-only",
"game_password": "",
"description": "This server runs Clusterio",
"visibility": {
"public": true,
"lan": true
},
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
"itemStats": {
"maxEntries":60,
"entriesPerSecond": 0.01666666667
},
"logItemTransfers": false,
"uploadModsToMaster": true,
"msBetweenCommands": 10,
"allowRemoteCommandExecution": true
}
+7
View File
@@ -0,0 +1,7 @@
version: '3'
services:
unit_test:
image: clusterio
build: .
command: npm run test
+9
View File
@@ -0,0 +1,9 @@
version: '3'
services:
master:
build: .
command: npx clusteriomaster run
slave:
build: .
command: npx clusterioslave run
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000px" height="840px" viewBox="-0.5 -0.5 1000 840" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="550" width="320" height="250" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 547px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="547" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="780" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 300 750 L 300 730 L 440 730 L 440 140 L 376.37 140" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 371.12 140 L 378.12 136.5 L 376.37 140 L 378.12 143.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 723px; margin-left: 346px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="346" y="726" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="520" y="490" width="440" height="310" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 438px; height: 1px; padding-top: 487px; margin-left: 522px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="522" y="487" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><rect x="40" y="210" width="380" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 378px; height: 1px; padding-top: 207px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connecting</div></div></div></foreignObject><text x="42" y="207" fill="#000000" font-family="Helvetica" font-size="12px">connecting</text></switch></g><path d="M 120 150 L 120 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 228.88 L 116.5 221.88 L 120 223.63 L 123.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; coLine truncated

After

Width:  |  Height:  |  Size: 78 KiB

+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="919px" height="620px" viewBox="-0.5 -0.5 919 620" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="260" width="320" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 257px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="257" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="700" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 280 390 L 280 370 L 380 370 L 380 130 L 346.37 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 341.12 130 L 348.12 126.5 L 346.37 130 L 348.12 133.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 360px; margin-left: 291px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="291" y="363" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="420" y="190" width="160" height="160" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 158px; height: 1px; padding-top: 187px; margin-left: 422px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="422" y="187" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><path d="M 120 140 L 120 273.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 278.88 L 116.5 271.88 L 120 273.63 L 123.5 271.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 194px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">ready()</div></div></div></foreignObject><text x="151" y="197" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ready()</text></switch></g><rect x="40" y="80" width="320" height="80" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 77px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #00Line truncated

After

Width:  |  Height:  |  Size: 45 KiB

+321
View File
@@ -0,0 +1,321 @@
<mxfile host="Electron" modified="2021-08-30T18:05:06.237Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.9.6 Chrome/89.0.4389.128 Electron/12.0.16 Safari/537.36" etag="uzcLt7Oh4qtDsMY9WSdT" compressed="false" version="14.9.6" type="device">
<diagram id="GSbHU_PhJe-l3NQpc0X3" name="Connection Flow">
<mxGraphModel dx="710" dy="734" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="760" pageHeight="630" background="#EEF3FA" math="0" shadow="0">
<root>
<mxCell id="GTJXd52-XhRZD3rAr2nm-0" />
<mxCell id="GTJXd52-XhRZD3rAr2nm-1" parent="GTJXd52-XhRZD3rAr2nm-0" />
<mxCell id="2r3fOvGVNr9_K5ZBoRis-18" value="Client" style="text;html=1;align=left;verticalAlign=middle;resizable=1;points=[];autosize=0;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
<mxGeometry x="440" y="80" width="145" height="20" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-0">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="320.02" />
<mxPoint x="260" y="320.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-5" value="receive&lt;br&gt;resume" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-1">
<mxGeometry x="0.086" relative="1" as="geometry">
<mxPoint x="-20" y="-6" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-2">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="320.02" />
<mxPoint x="180" y="320.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-26" value="receive&lt;br&gt;register" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-3">
<mxGeometry x="-0.2268" relative="1" as="geometry">
<mxPoint x="-20" y="2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-13">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="40" y="285.02" />
<mxPoint x="40" y="580.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-27" value="bad handshake&lt;br&gt;timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-14">
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
<mxPoint x="28" y="17" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="2r3fOvGVNr9_K5ZBoRis-28" value="Wait for handshake" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
<mxGeometry x="160" y="270.02000000000004" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="HutjLCx0ygpoBEJLSh69-0" value="Server" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
<mxGeometry x="40" y="80" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="_PUvpjEMIo1z1dIbIUzw-0" value="&lt;p&gt;&lt;span&gt;Connection Flow&lt;/span&gt;&lt;br&gt;&lt;/p&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=0;fontSize=20;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
<mxGeometry x="40" y="30" width="230" height="20" as="geometry" />
</mxCell>
<mxCell id="fGsjKHM69yN9R0Hf5M05-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="fGsjKHM69yN9R0Hf5M05-0" target="IKjVovVhkWe_WipwIJL3-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="220" y="180" />
<mxPoint x="180" y="180" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="fGsjKHM69yN9R0Hf5M05-0" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="160" y="119.98" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-9">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="300" y="300.02" />
<mxPoint x="300" y="300.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-6" value="bad session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingLeft=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-4">
<mxGeometry x="-0.429" y="-1" relative="1" as="geometry">
<mxPoint x="29" y="-4" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-19">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-29" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-20">
<mxGeometry x="-0.1944" y="2" relative="1" as="geometry">
<mxPoint x="18" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-0" value="Validate resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="230" y="340.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-18">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-28" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-21">
<mxGeometry x="-0.2778" relative="1" as="geometry">
<mxPoint x="20" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-13">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="60" y="355.02" />
<mxPoint x="60" y="560.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-30" value="bad token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-24">
<mxGeometry x="-0.8093" y="1" relative="1" as="geometry">
<mxPoint x="29" y="23" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-2" value="Validate register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="90" y="340.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-7" target="2r3fOvGVNr9_K5ZBoRis-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="260.02" />
<mxPoint x="180" y="260.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-7" value="Send hello" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="90" y="199.96" width="120" height="30.02" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-9" target="2r3fOvGVNr9_K5ZBoRis-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="260.02" />
<mxPoint x="260" y="260.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-9" value="Send invalidate" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="230" y="199.96" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-13" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="160" y="550.02" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-15" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="160" y="480.02" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-18" target="IKjVovVhkWe_WipwIJL3-15">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="470" />
<mxPoint x="180" y="470" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-18" value="Send ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="90" y="410.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-19" target="IKjVovVhkWe_WipwIJL3-15">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="470" />
<mxPoint x="260" y="470" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-19" value="Send continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="230" y="410.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-44">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="630" y="240.02" />
<mxPoint x="630" y="240.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-32" value="previous&lt;br&gt;session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-31">
<mxGeometry x="0.086" relative="1" as="geometry">
<mxPoint x="-20" y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-49">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="550" y="250.02" />
<mxPoint x="550" y="250.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-34" value="no&lt;br&gt;session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-33">
<mxGeometry x="-0.2268" relative="1" as="geometry">
<mxPoint x="-20" y="5" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-54">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="440" y="215.02" />
<mxPoint x="440" y="570.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-36" value="bad handshake&lt;br&gt;timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-35">
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
<mxPoint x="35" y="-23" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-37" value="Wait for hello" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="530" y="199.96000000000006" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-60" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-39" target="IKjVovVhkWe_WipwIJL3-37">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-39" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="530" y="119.98" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-65" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-44" target="IKjVovVhkWe_WipwIJL3-61">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="660" y="360.02" />
<mxPoint x="660" y="360.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-44" value="Send resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="600" y="270.0199999999999" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-68" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-49" target="IKjVovVhkWe_WipwIJL3-67">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-49" value="Send register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="460" y="270.0199999999999" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-54" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="530" y="550.02" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-55" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="530" y="480.0199999999999" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-62">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="630" y="390.02" />
<mxPoint x="630" y="390.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-73" value="receive&lt;br&gt;invalidate" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-64">
<mxGeometry x="0.2133" relative="1" as="geometry">
<mxPoint x="-30" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-54">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="700" y="570.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-74" value="bad handshake&lt;br&gt;timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;rotation=90;spacingTop=7;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-70">
<mxGeometry x="-0.7644" y="2" relative="1" as="geometry">
<mxPoint x="-12" y="29" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-71" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-55">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="630" y="450.02" />
<mxPoint x="630" y="450.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-75" value="receive&lt;br&gt;continue" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];spacingRight=4;labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-71">
<mxGeometry x="-0.3167" y="-1" relative="1" as="geometry">
<mxPoint x="-19" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-61" value="Wait for continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="600" y="410.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-62" target="IKjVovVhkWe_WipwIJL3-49">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="550" y="320.02" />
<mxPoint x="550" y="320.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-62" value="Invalidate session" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="530" y="340.02" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-54">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="470" y="560.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-77" value="bad handshake&lt;br&gt;timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rotation=90;labelBackgroundColor=none;spacingBottom=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-69">
<mxGeometry x="-0.8148" y="1" relative="1" as="geometry">
<mxPoint x="12" y="33" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-72" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-55">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="550" y="470.02" />
<mxPoint x="550" y="470.02" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-76" value="receive&lt;br&gt;ready" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-72">
<mxGeometry x="-0.2389" y="2" relative="1" as="geometry">
<mxPoint x="-22" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="IKjVovVhkWe_WipwIJL3-67" value="Wait for ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
<mxGeometry x="460" y="410.02" width="120" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="760px" height="630px" viewBox="-0.5 -0.5 760 630" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="440" y="80" width="145" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 90px; margin-left: 442px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap; ">Client</div></div></div></foreignObject><text x="442" y="94" fill="#000000" font-family="Helvetica" font-size="12px" font-weight="bold">Client</text></switch></g><path d="M 260 300.02 L 260 320 L 260 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 260 338.9 L 256.5 331.9 L 260 333.65 L 263.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 316px; margin-left: 238px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />resume</div></div></div></foreignObject><text x="238" y="319" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 180 300.02 L 180 320 L 180 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 338.9 L 176.5 331.9 L 180 333.65 L 183.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 318px; margin-left: 158px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />register</div></div></div></foreignObject><text x="158" y="322" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 160 285 L 40 285 L 40 580 L 153.63 580" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 158.88 580 L 151.88 583.5 L 153.63 580 L 151.88 576.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 300px; margin-left: 91px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">bad handshake<br />timeout</div></div></div></foreignObject><text x="91" y="304" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">bad handshake...</text></switch></g><rect x="160" y="270.02" width="120" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 285px; margin-left: 161px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "Line truncated

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because it is too large. Load diff
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="440px" viewBox="-0.5 -0.5 800 440" style="background-color: rgb(238, 243, 250);"><defs/><g><path d="M 170 365.02 L 170 385 L 290 385 L 290 371.39" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 290 366.14 L 293.5 373.14 L 290 371.39 L 286.5 373.14 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 110 350 L 40 350 L 40 130 L 103.63 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 108.88 130 L 101.88 133.5 L 103.63 130 L 101.88 126.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="110" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 111px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="160" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">connected</text></switch></g><path d="M 290 335.02 L 290 315 L 180 315 L 180 328.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 333.9 L 176.5 326.9 L 180 328.65 L 183.5 326.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 350 335.02 L 350 135 L 216.37 135" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 135 L 218.12 131.5 L 216.37 135 L 218.12 138.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 320 335.02 L 320 235 L 216.37 235" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 235 L 218.12 231.5 L 216.37 235 L 218.12 238.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="270" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="320" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">resuming</text></switch></g><path d="M 500 149.96 L 500 223.59" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 500 228.84 L 496.5 221.84 L 500 223.59 L 503.5 221.84 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="460" y="119.96" width="80" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 135px; margin-left: 461px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">closed</div></div></div></foreignObject><text x="500" y="139" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">closed</text></switch></g><path d="M 520 259.96 L 520 280 L 630 280 L 630 266.33" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 630 261.08 L 633.5 268.08 L 630 266.33 L 626.5 268.08 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 480 229.96 L 480 190 L 480 156.33" fill="noneLine truncated

After

Width:  |  Height:  |  Size: 24 KiB

+36
View File
@@ -0,0 +1,36 @@
# 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>
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
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.
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
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.
## 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.
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.
+78
View File
@@ -0,0 +1,78 @@
# 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>
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.
## 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:
**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.
**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.
**title** (optional):
Text used to identify the config field in user interfaces.
**description** (optional):
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.
**optional** (optional):
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.
## Accessing Fields
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");
Or by getting the group first and then calling set or get on that.
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.
## 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.
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.
+408
View File
@@ -0,0 +1,408 @@
# 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.
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.
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.
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.
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
interfaces.
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 /.
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.
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.
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.
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 WebSocket 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 15.
### master.session_timeout
Time in seconds from the connection is closed following a heartbeat timeout until the session is invalidated and data loss occurs.
A higher timeout gives more time for a client to recconect and resume an active session over a connection that was dropped, but also leads to stale connections taking longer to clear out.
In the case of the master or client crashing the data loss is unavoidable.
Value should not be less than the configured `max_reconnect_delay` of the clients and it should be greater than 2 times `master.heartbeat_interval`.
Defaults to 60.
### 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.
Defaults to 8.
### master.stream_timeout
Timeout in seconds for proxy streams through the master that has been created but is not yet sending data to be automatically closed.
Streams that have started sending data before this timeout is not affected.
Defaults to 15.
### master.default_role_id
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>.load_plugin
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.name
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.
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.
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.
Defaults to "instances".
### slave.master_url
URL to connect to the master server to.
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.
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.
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.
Defaults to "localhost".
### slave.max_reconnect_delay
Maximum 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 clients trying to reconnect at the same time.
Defaults to 60.
## 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.
Defaults to "New Instance".
### instance.id
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.
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.
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.
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.
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.
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.
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.
Defaults to true.
### factorio.enable_whitelist
Turn on the whitelist on the server.
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.
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.
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.
Sample output when disabled:
0.487 Loading map /srv/clusterio/instances/test/saves/world.zip: 2615888 bytes.
0.614 Checksum for script /srv/clusterio/instances/test/temp/currently-playing/control.lua: 2390553941
1.277 Script @/srv/clusterio/instances/test/temp/currently-playing/modules/example/test.lua:7: Example log line.
Sample output when enabled:
0.487 Loading map saves/world.zip: 2615888 bytes.
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.
### 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.
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.
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.
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.
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.
Defaults to 5.
### <plugin_name>.load_plugin
Whether to load the given plugin on the instance.
Note that plugins need to be loaded on the master server in order for them to be loaded on instances.
Defaults to true.
## Control Configuration
### control.master_url
URL to connect to the master server to.
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.
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.
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
### control.max_reconnect_delay
Max duration in seconds to wait before attempting to reconnect with the master server after the connection is dropped.
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all clients trying to reconnect at the same time.
Defaults to 60.
+304
View File
@@ -0,0 +1,304 @@
# 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.
## Contents
- [Workflow](#workflow)
- [Starting a Feature Branch](#starting-a-feature-branch)
- [Setting up the Development Environment](#setting-up-the-development-environment)
- [Checking your code for errors](#checking-your-code-for-errors)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Editing your Feature Branch](#editing-your-feature-branch)
- [Resolving Merge Conflicts](#resolving-merge-conflicts)
- [Updating Your GitHub Fork](#updating-your-github-fork)
- [Changelog](#changelog)
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
- [Code Style](#code-style)
- [Naming Style](#naming-style)
- [Indenting](#indenting)
- [Line Length](#line-length)
- [Markdown](#markdown)
- [Pushing releases to npm](#pushing-releases-to-npm)
## 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.
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.
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/clusterio
After this running `git fetch upstream` will get the updated branches from the main repository.
Do not run `git pull upstream` it will mess thing up!
### 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:
npm install
npx lerna bootstrap --hoist
npx lerna run build
This installs dependencies needed by tests and links the packages up so they work from the git work tree.
To start a cluster from the repo you will first need to initialize it, this can be done using the installer:
node packages/create --dev
To add plugins to use in the development environment use
node packages/ctl plugin add ./path/to/plugin
It's important that the path starts with `./`.
The `plugins/` directory contains the core plugins for the project and can be added using the above mentioned command.
The documentation uses the binaries provided by the packages, but for the development environment you will need to call the packages directly:
npx clusteriomaster -> node packages/master
npx clusterioslave -> node packages/slave
npx clusterioctl -> node packages/ctl
Note that to use `clusterioctl` you will have to create a control config first:
node packages/master bootstrap create-ctl-config [admin-user]
Once you've set up the cluster you can use `node packages/master run` to run the master server and `node packages/slave run` to run the slave which connects to the master server.
For web development on the master there are also the following flags.
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
`--dev-plugin [name]` - Recompile the web module for the given plugin on the fly proving live reloading of it.
These options can be combined and multiple plugins can be actived for dev mode.
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `npx lerna run build` before you start the master without these flags again.
### 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 branch of the main repository.
To do this check out that branch from the main repository.
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.
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.
### 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:
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.
### 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
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.
### 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
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.
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
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.
### 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
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
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.
## 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.
## Supported Node.js and Factorio Versions
Clusterio run on Node.js v12 and up, 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 v12 is the default.
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
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.
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.
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");
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 template literals may also be used where appropriate.
### Indenting
JavaScript and Lua code should be indented with tabs.
Other files types should prefer tabs for indents where reasonable with the exception of Markdown.
### 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:
function foo(
really, long, argument, list,
over, multiple, lines
) {
if (
really && long && expression
|| over && multiple && lines
) {
// 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
```
+297
View File
@@ -0,0 +1,297 @@
# 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.
## Contents
- [Factorio Mods](#factorio-mods)
- [Factorio Scenarios](#factorio-scenarios)
- [event_handler interface](#event_handler-interface)
- [Communicating with Clusterio](#communicating-with-clusterio)
- [Clusterio Modules](#clusterio-modules)
- [Clusterio Lua API](#clusterio-lua-api)
- [Clusterio Plugins](#clusterio-plugins)
## 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 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.
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:
```lua
-- control.lua
local event_handler = require("event_handler")
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:
```json
{
"scenario": {
"name": "example-scenario",
"modules": [
"module_foo",
"module_bar"
]
}
}
```
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>
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:
- `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.
- `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.
- `on_nth_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.
- `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.
<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
```lua
-- example.lua
local function my_handler(event)
local name = game.player[event.player_index].name
game.print("It appears that " .. name .. " has died")
end
return {
on_init = function() game.print("example_lib init") end,
events = {
[defines.events.on_player_died] = my_handler,
},
}
```
And then in control.lua the following used to load it with `add_lib`:
```lua
-- control.lua
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.
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
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.
## 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.
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
{
"name": "my_module",
"version": "1.2.0",
"dependencies": {
"clusterio": "*",
"foo": ">=0.4.2"
},
"require": ["bar.lua"],
"load": ["foo.lua"]
}
```
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.
- `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.
- `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.
- `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.
- `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.
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.
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 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
**require path**
From a module
`local clusterio_api = require("modules/clusterio/api")`
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.
#### 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.
#### 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.
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.
Event data:
- `instance_id`: the id of the instance.
- `instance_name`: the name of the instance.
##### 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.
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.
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.
Parameters:
- `channel`: string identifying which channel to send it on.
- `data`: table that can be converted to JSON with game.table_to_json
### serialize library
**require path**
From a module
`local serialize = require("modules/clusterio/serialize")`
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.
#### serialize.serialize_equipment_grid(LuaEquipmentGrid)
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.
#### serialize.serialize_item_stack(LuaItemStack, destination_table)
Serialize the LuaItemStack passed into the target destination_table.
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.
#### serialize.serialize_inventory(LuaInventory)
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.
## Clusterio Plugins
See the [Writing Plugins](writing-plugins.md) document for details on the plugin interface and how to write plugins for Clusterio.
+268
View File
@@ -0,0 +1,268 @@
# Protocol
Clusterio communicates using a simple homebrew message passing protocol based on JSON over WebSocket with limited support for reliable transport of messages.
The protocol is implemented in [packages/lib/link](/packages/lib/link) and uses the `ws` library to handle the WebSocket connections on the Node.js side and the native WebSocket API on the browser side.
Two general kinds of messages are supported: events and requests.
Events are messages sent by one party and received by the other and offer no notification for the sender on whether it was actually received.
Requests are pairs of request-response messages where one side sends the requests and the other side replies with a response.
Apart from the handshake used to establish connections the protocol is directionless, and either party can send or receive both requests and events.
Events and requests are defined as instances of the `Request` and `Event` classes, and specify a unique type string, who can send and receive the message, optional forwarding of the message and the format of the data payload as a JSON schema.
All messages sent and received are validated with JSON schemas to catch potential errors in sending messages and avoid having to write lots of validation code in message handlers.
Messages are sent by calling the `.send()` method of the message instance and passing a `Link` instance to send it over and the data to send.
For the master server there's a `Link` instance for each slave and control connection stored in the `WsServer` class.
For slaves the `Slave` instance is a link to the master server, and each `Instance` instance is an in-memory link to the `Slave` instance and has a corresponding `InstanceConnection` link stored on the `Slave` instance.
For ctl and web_ui there's a `Control` instance that acts as the link to the master server.
Messages received invokes message handlers that were registered when the link was brought up.
These handlers are named after the name of the property the message is stored under with `EventHandler` appended for events and `RequestHandler` appended for requests.
The handlers are defined on the link class itself for core messages, and on the plugin class in the case of plugin defined messages.
## Connection flow
Clients connect via WebSocket to the `/api/socket` endpoint and upon establishing connection to the server the connection is in the handshake phase and the server will send a `hello` message to the client.
If the client has no previous session with the server it sends a `register_slave` or `register_control` message depending on what kind of client it is to the server.
If the register is valid then the server will respond with a `ready` message with a session token and the connection has left the handshake phase, otherwise the server will close the connection with a WebSocket close code indicating the kind of failure.
Upon leaving the handshake phase both parties should start sending heartbeat messages with the sequence number of the last received message.
Messages sent over the link should be stored in a send buffer and only cleared from the send buffer when a heartbeat with a `last_seq` equal or or higher is received or the session is invalidated.
If the connection goes stale or is unexpectedly closed the client can reastablish the same session by connecting to the server again and sending a `resume` message after received the `hello` from the server.
If the session resume is successful then the server will respond with a `continue` message and the connection is active again.
The client should then re-send all messages it has in its send buffer that is greater than the `last_seq` number in the `continue` message.
Otherwise the server will respond with `invalidate` and the client is expected to drop its send buffer start a new session with `register_slave` or `register_control`.
![Connection flow](../assets/connection_flow.svg)
## Connector flow
To manage the complexity of connecting, reconnecting, resuming or invalidating dropped sessions the WebSocket connection for a `Link` class is managed by a connector.
There's three kinds of connectors defined: `WebSocketClientConnector`, `WebSocketServerConnector` and `VirtualConnector`.
The virtual connector is for in-memory connections and doesn't have any connection state logic.
For the WebSocket connectors there's four states they can exist in:
- `closed` - The connector has just been created or has been explicitly closed.
- `connecting` (client only) - The connector is in the process of trying to connect to the server.
- `connected` - The connector is successfully connected.
- `resuming` - The connection dropped and the connector is in the process of attempting to resume it.
Along with the following events emitted when transition between states:
- `connect` From `connecting` (client) or `closed` (server) to `connected`
- `drop` - From `connected` to `resuming`
- `resume` - From `resuming` to `connected`
- `invalidate` - From `resuming` to `connecting` or `closed`
- `close` - From any state to `closed`
![Conector States](../assets/connector_states.svg)
### WsClientConnector
The client connector will keep trying to establish and mantain a connection after `.connect()` is called on it, stopping only in the case of authentication failure or `.close()`/`.setClosing()` being called.
Other than that, the logic is pretty straight forward.
![WsClientConnector](../assets/WsClientConnector.svg)
### WsServerConnector
The server connector doesn't do the work of connecting or validating the handshake.
Instead it's handed over a WebSocket connection from `WsServer` once there's only the step of sending the final message left to do.
Because of this there's no `connecting` state, and once a session times out from the `resuming` state it goes directly to closed.
![WsServerConnector](../assets/WsServerConnector.svg)
## Wire format
All communication in the protocol is done via WebSocket text frames containing a JSON payload with the following properties:
- `seq: integer | null` - The sequence number of the message, can only be null for handshake and heartbeats.
- `type: string` - The type of message
- `data: object` - Message data
Before a connection is established it's in the handshake phase and the `seq` property is `null`.
The `type` property is used to distinguish which of handshake, heartbeat, events or requests this message is and determines the expected format of the `data` property.
### Handshake Messages
The following messages are the only valid messages that can be sent during the handshake phase.
#### `hello`
Sent by the master server when the WebSocket connection has been opened.
- `version: string` - The version of the master, e.g. "2.0.0".
- `plugins: Object<string, string>` -
Object mapping plugin names to plugin version for plugins that are loaded on the master server.
#### `register_slave`
Slave handshake for establishing a new connection session.
- `token: string` - Authentication token.
- `agent: string` - Human readable string of the software connecting.
- `version: string` - The protocol version of the slave, e.g. "2.0.0".
- `name: string` - Name of the slave.
- `id: integer` - ID of the slave.
- `plugins: Object<string, string>` -
Object mapping plugin names to plugin version for plugins that are available on the slave.
#### `register_control`
Control handshake for enstablishing a new connection session.
- `token: string` - Authentication token.
- `agent: string` - Human readable string of the software connecting.
- `version: string` - The protocol version of the control, e.g. "2.0.0".
#### `resume`
Handshake for resuming an existing connection session where the connection had dropped unexpectedly.
- `session_token: string` - Session token to resume session with.
- `last_seq: integer | null` - Last message seq received by the client.
#### `invalidate`
Sent by the master server in reply to a resume for a session that cannot be resumed.
The client should start a new session with a register\_\* message in response.
#### `continue`
Sent by the master server to signal resuming the session was successful.
- `last_seq: integer | null` - Last message seq received by the server.
- `session_timeout: integer` - Session timeout in seconds.
- `heartbeat_interval: number` - Rate client should send heartbeats at.
#### `ready`
Sent by the master server to signal the connection has been established with a new session.
- `session_token: string` - Token to resume the session with.
- `session_timeout: integer` - Session timeout in seconds.
- `heartbeat_interval: number` - Rate client should send heartbeats at.
### Heartbeat message
Once connected as indicated by the `ready` and `continue` messages both sides of the connection starts sending heartbeats at the inverval given in the `ready` and `continue` messages.
The heartbeat should contain the `seq` of the last received message, and receiving a heartbeat with a given sequence number means that the other party of the link has received all messages up to that seq number.
#### `heartbeat`
Sent by both the master server and clients at the interval given in the continue/ready messages.
- `last_seq: integer` - Last message seq received over the link.
### Events
Events are messages send in one direction over the link, that the receiver is expected to act upon, but not make any replies (for that there's [Requests](#requests).
By convention message types ending in `*_event` are events.
The data payload of an event consists solely of event specific properties.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core events and their contents.
Plugins may defined their own events as well.
#### `*_event`
- any properties defined by the event type.
### Requests
Requests are messages which expects a response in return.
They function a lot like HTTP requests except that both parties of a connection can inniate a request that is expected to be responded to.
By convention message types ending in `*_request` are requests, and is expected to be replied to with a corresponding `*_response` message.
If an error occured while processing the request an error response containing an error property in the data payload is sent instead.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core requests and their contents.
Plugins may define their own requests as well.
#### `*_request`
- any properties defined by the request type.
#### `*_response` success
- `seq: integer` - The seq number of the request message.
- any additional properties defined by the request type.
#### `*_response` error
- `seq: integer` - The seq number of the request message.
- `error: string` - Human readable text message describing the error.
### Graceful Disconnect
To ensure no data is lost when closing the link it's important to send a prepare disconnect request and wait for it to complete before closing the WebSocket.
Either party of the link can send this request and the party sending the request should be the one that initates the close of the WebSocket.
#### `prepare_disconnect_request`
Request graceful disconnect of the connection.
The receiving party should stop tasks that send events and requests over the link and wait for pending requests it has over the link to complete, but it should not stop responding to requests sent to it.
Only after this process is done should the response be sent.
#### `prepare_disconnect_response`
Signals the preparation for disconnect is complete and that the connection can be closed.
The party that initiatied the prepare disconnect request may still send other requests/events over the link to finish its part of the disconnection proceedure.
If the error response variant is received it should be logged if possible and treated as a success.
### Example handshake
Server hello after opening the WebSocket.
```json
{
"seq": null,
"type": "hello",
"data": {
"version": "2.0.0"
}
}
```
Slave registering a new connection session.
```json
{
"seq": null,
"type": "register_slave",
"data": {
"token": "xyzabc123",
"agent": "Clusterio Slave",
"version": "2.0.0",
"name": "Foo",
"id": 123
}
}
```
Master server reply signaling the session is established and ready.
```json
{
"seq": null,
"type": "ready",
"data": {
"session_token": "xyz.azy.bzy",
"session_timeout": 60,
"heartbeat_interval": 15
}
}
```
Slave and master can now send events and requests over the connection.
+16
View File
@@ -0,0 +1,16 @@
## 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.
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>
## 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.
+23
View File
@@ -0,0 +1,23 @@
{
"plugins": [
"plugins/markdown"
],
"tags": {
"allowUnknownTags": false
},
"opts": {
"destination": "docs/api",
"recurse": true
},
"source": {
"include": [
"packages/master/src/",
"packages/master/master.js",
"packages/slave/slave.js",
"packages/ctl/ctl.js",
"packages/lib/",
"plugins/"
],
"excludePattern": "node_modules"
}
}
+101
View File
@@ -0,0 +1,101 @@
# 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]`.
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
To be written.
## Instances
To be written.
## 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.
### List users
ctl> user list
Lists all user accounts in the cluster along with some data for each.
### Create User
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.
### 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.
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.
### 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.
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
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.
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.
### Set Cluster Roles
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.
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
ctl> user delete <name>
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 joins the cluster again a new account will be made for them automatically.
## Roles
To be written.
+72
View File
@@ -0,0 +1,72 @@
# 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.
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
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.
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.
```apache
<Location /sub/path/>
ProxyPass "http://localhost:8080/"
</location>
<Location /sub/path/api/socket>
ProxyPass "ws://localhost:8080/api/socket"
</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.
## 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.
## 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`.
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:
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:
npx clusterioctl control-config set control.tls_ca cert.pem
+619
View File
@@ -0,0 +1,619 @@
# 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.
## Contents
- [Plugin Structure](#plugin-structure)
- [Defining the plugin class](#defining-the-plugin-class)
- [Logging Messages](#logging-messages)
- [Plugin Configuration](#plugin-configuration)
- [Communicating with Factorio](#communicating-with-factorio)
- [Defining Link Messages](#defining-link-messages)
- [Defining Events](#defining-events)
- [Defining Requests](#defining-requests)
- [Sending Link Messages](#sending-link-messages)
- [Handling connection events](#handling-connection-events)
- [Collecting Statistics](#collecting-statistics)
- [Adding Custom Commands to clusterioctl](#adding-custom-commands-to-clusterioctl)
## Plugin Structure
The basic file structure of a plugin is the following.
plugin_name/
+- info.js
+- package.json
+- master.js
+- instance.js
+- control.js
+- test/
| +- plugin.js
+- module/
+- 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.
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:
```js
const { libLink } = require("@clusterio/lib"); // For messages
module.exports = {
name: "foo_frobber",
title: "Foo Frobber",
description: "Does advanced frobnication",
instanceEntrypoint: "instance",
masterEntrypoint: "master",
messages: {
/* See below */
},
};
```
The following properties are recognized:
**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.
**description**:
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.
**InstanceConfigGroup**:
Subclass of `PluginConfigGroup` for defining the per instance configuration fields for this plugin.
See [Plugin Configuration](#plugin-configuration)
**masterEntrypoint**:
Path to a Node.js module relative to the plugin directory which contains the MasterPlugin class definiton for this plugin.
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)
**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.
**messages**:
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 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).
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
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:
```js
const libPlugin = require("@clusterio/lib/plugin");
class MasterPlugin extends libPlugin.BaseMasterPlugin {
async init() {
this.foo = 42;
await this.startFrobnication();
}
// ...
}
module.exports = {
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).
It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class.
E.g.:
```js
constructor(...args) {
super(...args);
// 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.
## 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:
```js
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.
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.
For guarding unexpected errors the best option is to log a short description along with the`stack` property of the Error:
```js
try {
// Operation that should not throw but may end up throwing
} catch (err) {
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.
## 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:
```js
const libConfig = require("@clusterio/lib/config");
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
MasterConfigGroup.groupName = "foo_frobber";
MasterConfigGroup.define({
name: "level",
description: "Level of frobnication done",
type: "number",
initial_value: 2,
});
MasterConfigGroup.finalize();
module.exports = {
...
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:
```js
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.
### 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:
```js
async init() {
if (!this.instance.config.get("factorio.enable_save_patching")) {
throw new Error("foo_frobber plugin requires save patching.");
}
}
```
## 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.
This is done by calling the `sendRcon` method on the plugin object.
For example:
```js
async onStart() {
let response = await this.sendRcon(
"/sc rcon.print('data')"
);
// Do stuff with response.
}
```
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:
```js
async init() {
this.instance.server.on("ipc-my_plugin_foo", content =>
this.handleFoo(content).catch(err => this.logger.error(
`Error handling foo:\n${err.stack}`
))
);
}
async handleFoo(content) {
// Do stuff with content
}
```
And then in the module for the plugin:
```lua
local clusterio_api = require("modules/clusterio/api")
-- 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.
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.
## 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).
[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.
### 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`:
```js
messages: {
startFrobnication: new libLink.Event({
type: "foo_frobber:start_frobnication",
links: ["master-slave", "slave-instance"],
forwardTo: "instance",
eventRequired: ["frobnication_type"],
eventProperties: {
"frobnication_type": { type: "string" },
"urgent": { type: "boolean" },
},
}),
},
```
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.
#### 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.
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.
#### 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 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.
#### 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.
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.
### 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`:
```js
messages: {
reportFrobnication: new libLink.Request({
type: "foo_frobber:report_frobnication",
links: ["master-slave", "slave-instance"],
forwardTo: "instance",
requestRequired: ["verbosity"],
requestProperties: {
"verbosity": { type: "integer" },
"special": { type: "boolean" },
},
responseProperties: {
"report": {
type: "array",
items: { type: "string" },
},
},
}),
},
```
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.
#### 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.
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.
#### 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.
#### 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
#### responseRequired
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.
## 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:
```js
// 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.
### 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.
After the prepare disconnect the connection will be closed, which will result in pending requests and events being dropped.
Plugins must respond to the prepare disconnect by stopping any processess it does that send events or requests over the link in question.
This can be accomplished either through listening for the prepare disconnect hook, or by checking the `connected` property of the `SlaveConnection` class and `Slave` class on the master and slave respectively.
For example the sending of an event from an `InstancePlugin` class can be stopped while the connection is not connected, not in the dropped state, and not in the process of discunnecting by using the following code:
```js
if (this.slave.connected) {
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.
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 either re-established or the session times out.
This means that if your plugin sends a lot of events or requests, they can end up being queued up in a buffer and sent out all at once the connection is re-estabished.
To avoid this you should be throtteling and/or stopping your requests/events after `drop` has been raised, and continue back as normal when `resume` is raised.
## 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.
In its simplest form collecting data from plugins consists of defining the metric and updating it somewhere in the plugin code.
For example:
```js
const { Counter } = require("@clusterio/lib/prometheus");
const fooMetric = new Counter(
"clusterio_foo_frobber_foo_metric", "Measures the level of foo",
);
// 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.
For metrics that are per-instance, you must define an `instance_id` label and set it accordingly, for example:
```js
const { Counter } = require("@clusterio/lib/prometheus");
const barMetric = new Gauge(
"clusterio_foo_frobber_bar_metric", "Bar instance level",
{ labels: ["instance_id"] }
);
// 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.
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
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:
```js
const { Command, CommandTree } = require("@clusterio/lib/command");
const fooFrobberCommands = new CommandTree({
name: "foo-frobber", description: "Foo Frobber Plugin commands"
});
```
Then commands are added to the the plugin's command tree:
```js
const info = require("./info");
fooFrobberCommands.add(new Command({
definition: ["frobnicate <type>", "Do frobnications", (yargs) => {
yargs.positional("level", {
describe: "type of frobnication", type: "string"
});
}],
handler: async function(args, control) {
await info.messages.frobnicate.send(control, {
instance_name: "Console",
content: args.message,
});
},
}));
```
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.
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:
```js
const libPlugin = require("@clusterio/lib/plugin");
class ControlPlugin extends libPlugin.BaseControlPlugin {
async addCommands(rootCommand) {
rootCommand.add(fooFrobberCommands);
}
}
module.exports = {
ControlPlugin,
}
```
-10
View File
@@ -1,10 +0,0 @@
toc:
- name: Master Server API
- clusterioMaster
- name: Type declarations
- itemStack
- name: Internal modules
description: |
Here are some helper functions for navigation.
- modManager
- getFactorioLocale
+8
View File
@@ -0,0 +1,8 @@
{
"packages": [
"packages/*",
"plugins/*",
"test"
],
"version": "2.0.0-alpha.10"
}
-15
View File
@@ -1,15 +0,0 @@
const jwt = require('jsonwebtoken');
const config = require("./../config");
var middleware = function (req, res, next) {
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
next();
});
}
module.exports = {middleware};
-49
View File
@@ -1,49 +0,0 @@
var forge = require("node-forge");
forge.options.usePureJavaScript = true;
var fs = require("fs");
var mkdirp = require("mkdirp");
var pki = forge.pki;
var rsa = forge.pki.rsa;
var startTime, endTime;
module.exports = function({
bits = 2048,
certificatePath = "database/certificate",
doLogging = false,
} = {}){
function log(t){
if(doLogging) console.log("lib/generateSSLcert.js | "+t);
}
log("Generating SSL certificate with "+bits+" bits at "+certificatePath);
startTime = Date.now();
var keypair = rsa.generateKeyPair({bits: bits, e: 0x10001});
endTime = Date.now();
log("Generated 2048bit keypair in: "+(endTime-startTime)+" ms");
startTime = Date.now();
var cert = pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date(Date.now()+(1000*60*60*24*365)); // valid for one year
// self-sign certificate
cert.sign(keypair.privateKey);
// Convert certificate to pem type and write it to a file
if(certificatePath[certificatePath.length-1] != "/") certificatePath += "/";
if(!fs.existsSync(certificatePath)) {
mkdirp.sync(certificatePath);
console.log("Created directory: "+certificatePath);
}
fs.writeFileSync(certificatePath+ "cert.key", pki.privateKeyToPem(keypair.privateKey));
fs.writeFileSync(certificatePath+ "cert.crt", pki.certificateToPem(cert));
endTime = Date.now();
log("Generated certificate in: "+(endTime-startTime)+" ms");
}
-25
View File
@@ -1,25 +0,0 @@
var assert = require("assert");
var fs = require("fs");
var fileOps = require("_app/fileOps");
var generateSSLcert = require("./generateSSLcert.js")
describe("generateSSLcert.js(options)", ()=>{
it("Creates a folder with a .crt and .key file in it", ()=>{
let certPath = "lib/certTestPlsDelete";
assert(!fs.existsSync(certPath)); // if it already exsits, the test might pass because of leftover data (which is bad)
generateSSLcert({
// bits:512, // supported, but I just use the default of 2048 for this test
certificatePath:certPath,
doLogging: false,
});
assert(fs.existsSync(certPath));
assert(fs.existsSync(certPath+"/cert.key")); // private key
assert(fs.existsSync(certPath+"/cert.crt")); // certificate file
// remove certificates and data from the test
fileOps.deleteFolderRecursiveSync(certPath);
}).timeout(10000);
});
-53
View File
@@ -1,53 +0,0 @@
/**
Module for grabbing factorio locale from a local factorio install
@module getFactorioLocale
*/
const fs = require("fs");
const ini = require("ini");
const cache = {};
/**
Gets raw factorio locale from the .ini file.
Not available outside of module through exports.
@memberof getFactorioLocale
@inner
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
function getLocale(factorioDirectory, languageCode, callback){
var localeFile = factorioDirectory + '/data/base/locale/' + languageCode + '/base.cfg';
fs.readFile(localeFile, "utf8", (err, rawLocale) => {
callback(err, rawLocale);
});
}
/**
Gets factorios locale as an object, does not respect mods.
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
throw new Error("Error: wrong parameters provided");
}
if(!cache[languageCode]){
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
if(err){
callback(err);
} else {
let processedLocale = ini.parse(rawLocale);
cache[languageCode] = processedLocale;
callback(undefined, processedLocale);
}
});
} else {
callback(undefined, cache[languageCode]);
}
}
-74
View File
@@ -1,74 +0,0 @@
const assert = require("assert");
const getFactorioLocale = require("./getFactorioLocale");
describe("getFactorioLocale.js", () => {
describe("asObject()", () => {
it("requires 2 strings and a function as parameters", () => {
let validate = (err) => {
return err == "Error: wrong parameters provided"
}
let message = "getFactorioLocale is responding incorrectly to invalid arguments"
let callback = (err,object) => true;
assert.throws(() => getFactorioLocale.asObject(), validate, message);
assert.throws(() => getFactorioLocale.asObject("hello", "I forgot my callback"), validate, message);
assert.throws(() => getFactorioLocale.asObject(undefined, undefined, callback), validate, message);
// the proper way to do it
assert.doesNotThrow(() => getFactorioLocale.asObject("factorio_0.15.27", "en", callback), validate, message);
});
it("returns a nested object with base game locale information", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
Object.keys(object).forEach(key => {
// first level of the nested object is always an object
assert.equal(typeof object[key], "object");
Object.keys(object[key]).forEach(key2 => {
// second level of the nested object is always a string, nearly always truthy
if(key2 != "so-long-and-thanks-for-all-the-fish"){
assert.ok(object[key][key2]);
assert.equal(typeof object[key][key2], "string");
}
});
});
done();
}
});
it("has some duplicate keys", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
let arrayOfKeys = [];
// collect all keys in a single array
Object.keys(object).forEach(key => {
arrayOfKeys.push(key);
Object.keys(object[key]).forEach(key2 => {
arrayOfKeys.push(key2);
});
});
// check for duplicates
let duplicateKeys = [];
let i = 0;
arrayOfKeys.forEach(key => {
let o = 0;
arrayOfKeys.forEach(key2 => {
if(i != o && key == key2){
duplicateKeys.push(key);
}
o++
});
i++;
});
// we got 704 but this is a very unexact number
assert(duplicateKeys.length > 10)
done();
}
});
});
});
-15
View File
@@ -1,15 +0,0 @@
const fileOps = require("_app/fileOps");
const config = require("./../config");
module.exports = {
sync: function(){
let instances;
try {
instances = fileOps.getDirectoriesSync(config.instanceDirectory);
} catch (e) {
// there are no instances created yet, probably missing the folder
instances = [];
}
return instances;
}
}
-48
View File
@@ -1,48 +0,0 @@
/**
Provides tools for managing both masters and slaves config files.
@module configManager
*/
// require modules
const asTable = require("as-table").configure({delimiter: ' | '});
const fs = require("fs");
// internal requires
var config = require("./../../config");
const objectOps = require("./../objectOps.js");
/**
console.log()s each entry in the config
@param {string} instance Either undefined, "shared" or the instance to show the config for.
*/
module.exports.displayConfig = function(instance){
if(!instance || instance == "shared"){
let configArray = [];
Object.keys(config).forEach(key=>{
if(key != "__comment"){
if(typeof config[key] == "object"){
configArray.push({entry: key, value: JSON.stringify(config[key])});
} else configArray.push({entry: key, value: config[key]});
}
});
console.log(asTable(configArray));
}
}
module.exports.editConfig = function(instance, entry, newValue){
if(!instance || instance == "shared"){
if(config[entry] && newValue){
if(objectOps.isJSON(newValue)){
config[entry] = JSON.parse(newValue);
console.log("Changed config entry "+entry+" from "+JSON.stringify(config[entry])+" to "+JSON.stringify(newValue));
console.log("new config file:");
console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
} else {
config[entry] = newValue;
console.log("Changed config entry "+entry+" from "+config[entry]+" to '"+newValue+"'");
console.log("new config file:");
console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
}
}
}
};
-273
View File
@@ -1,273 +0,0 @@
/**
Provides tools to manage mods in a clusterio install
@module modManager
*/
const rightpad = require("right-pad");
const asTable = require("as-table").configure({delimiter: ' | '});
const moment = require("moment");
const syncRequest = require("sync-request");
const fs = require("fs");
const https = require('follow-redirects').https;
const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const query = require('cli-interact').getYesNo;
const fileOps = require("_app/fileOps");
const config = require("./../../config");
const instances = require("./../getInstances.js").sync();
function ellipse(str, max){
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
}
// https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
function formatBytes(a,b){if(0==a)return"0 Bytes";var c=1024,d=b||2,e=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],f=Math.floor(Math.log(a)/Math.log(c));return parseFloat((a/Math.pow(c,f)).toFixed(d))+" "+e[f]};
/**
Lists mods for an instance as a table. Warning, handles printing internally.
@param {string} instance - Name of the instance or "shared"
@example
var modManager = require("modManager");
modManager.listMods("testInstance");
// console.logs table of mods installed on instance
modManager.listMods("shared");
// console.logs mods located in /sharedMods as a nice table
*/
module.exports.listMods = function(instance){
return new Promise((resolve, reject) => {
if(instance && instances.includes(instance)){
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
console.log(asTable(list.map(mod => {
return {
name:(mod.title || mod.name),
version:mod.version,
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
})));
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
resolve();
});
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
resolve();
});
}).catch(e => {
reject(new Error(e));
});
}
// get JSON data about GH releases
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
*/
});
}
/**
Searches mods.factorio.com for mods matching a search term and prints
the results to console as a neat looking table
@param {string} searchTerm What to search for
*/
module.exports.findMods = function(searchTerm){
return new Promise((resolve, reject) => {
factorioAPI.init(false);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
let result = asTable(mods.reverse().map(mod => {
return {
title:ellipse(mod.title, 50),
name:ellipse(mod.name, 30),
downloads:mod.downloads_count,
updated: moment(mod.updated_at).format("DD.MM.YY"),
latest_version: mod.latest_release.version,
owner:mod.owner,
//license: mod.license_name,
}
}));
resolve(result);
// console.log(result);
});
}).catch(e => {
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
reject(new Error(e));
});
});
}
module.exports.addMod = function(term, instance){
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
if(term && term.length > 0){
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
// treat it as a link download
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log("Downloading " + url);
let file = fs.createWriteStream(modDirectory + name);
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
let downloadTimeElapsed = Date.now() - downloadStartTime;
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
resolve();
});
});
}
} else {
// treat it as a search and download the top result
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log(body)
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
}
} else {
reject(new Error("Invalid search term"));
}
});
}
module.exports.removeMod = function removeMod(term, instance, force = false){
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
if(term && typeof term == "string" && term.length > 0){
let mods = fs.readdirSync(modDirectory);
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
if(modsMatchingFilter.length < 1){
console.log("No mods found matching search");
} else if(modsMatchingFilter.length == 1){
console.log("Deleting "+modsMatchingFilter[0]);
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
console.log("Deleted.");
} else if(modsMatchingFilter.length > 1){
console.log("Found multiple mods");
modsMatchingFilter.forEach(mod => console.log(mod));
if(query("Delete all mods matching filter?")){
let modsToDelete = modsMatchingFilter.length;
for(let i = 0; i < modsToDelete; i++){
fs.unlinkSync(modDirectory+modsMatchingFilter[i]);
console.log("Deleted "+modsMatchingFilter[i]);
}
} else console.log("Did not delete mods.")
}
resolve();
} else {
reject(new Error("Invalid search term"));
}
});
}
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
/**
Download mod from github repo by looking for the first binary download in
the latest release, saving it to /sharedMods
@param {string} url a github repo URL, for example https://api.github.com/repos/Danielv123/factorioClusterioMod/releases. Must link to /releases
*/
module.exports.downloadFromGitURL = function(url){
console.log("Downloading mod from "+url);
// get JSON data about releases
let res = syncRequest('GET', url, {"headers":{"User-Agent":"Please don't require setting a user agent"}});
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
if(downloadUrl) {
console.log(downloadUrl);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(downloadUrl, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
});
}
}
/*
module.exports.findUpdates = function(){
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
}*/
-136
View File
@@ -1,136 +0,0 @@
var mkdirp = require("mkdirp");
var http = require('http');
var https = require('https');
var fs = require('fs');
var modManager = require("./manager/modManager.js");
// modManager.addMod("clusterio", "shared");
function makeDirectory(path){
return new Promise((resolve, reject) => {
mkdirp(path, function(err){
if(err){
console.error("Unable to mkdirp "+path);
console.error(err);
resolve(false);
} else {
console.log("mkdirp "+path);
resolve(true);
}
});
});
}
function download(url, file, callback){
return new Promise((resolve, reject) => {
let pathArray = file.split("/");
pathArray.pop(); // remove filename from the end
if(pathArray.length >= 1){
// this might be an invalid path!
fs.stat(pathArray.join("/"), (err, stat) => {
if(err){
makeDirectory(pathArray.join("/")).then(()=>{
doDownload(url, file, callback);
});
} else {
doDownload(url, file, callback);
}
});
} else {
doDownload(url, file, callback);
}
function doDownload(url, file, callback){
var writeStream = fs.createWriteStream(file);
let protocol = http;
if(url.split(":")[0] == "https") protocol = https;
var request = protocol.get(url, function(response) {
response.pipe(writeStream);
response.on("end", () => {
if(callback && typeof callback == "function") callback(file);
console.log("Downloaded "+file+" from "+url);
resolve();
});
});
}
});
}
var directories = [
"database/linvodb",
"sharedMods",
"instances",
];
let downloadList = [
{
url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
file: "static/pictures/spritesheet.js",
callback: function(file){
fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
},
},
{
url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
file: "static/pictures/spritesheet.png",
},
{
url: "https://code.jquery.com/jquery.js",
file: "static/external/jquery.js",
},
{
url: "https://code.jquery.com/ui/1.12.1/jquery-ui.js",
file: "static/external/jquery-ui.js",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js",
file: "static/external/canvasjs.js",
},
{
url: "https://momentjs.com/downloads/moment.min.js",
file: "static/external/moment.min.js",
},
{
url: "https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js",
file: "static/external/mousetrap.min.js",
},
{
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js",
file: "static/external/socket.io.js",
},
]
// initiate folder creations and downloads
processMkdirRecursive(processDownloadRecursive);
function processMkdirRecursive(callback){
let item = directories.shift();
makeDirectory(item).then(()=>{
if(directories.length == 0){
if(callback) callback();
} else {
processMkdirRecursive(callback);
}
});
}
function processDownloadRecursive(callback){
let item = downloadList.shift();
download(item.url, item.file, item.callback).then(()=>{
if(downloadList.length == 0){
if(callback) callback();
} else {
processDownloadRecursive(callback);
}
});
}
// generate SSL certificates if they are missing
if(!fs.existsSync("database/certificates") || !fs.existsSync("database/certificates/cert.crt") || !fs.existsSync("database/certificates/cert.key")){
var generateSSLcert = require("./generateSSLcert.js");
generateSSLcert({
certificatePath: "database/certificates",
doLogging: true,
});
}
-44
View File
@@ -1,44 +0,0 @@
/**
Helper functions related to objects
@module objectOps
*/
/**
Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
@param {object} obj to clone
@returns {object}
@example
a = {"thingy":"with a value"};
b = a;
a == b; // true
c = objectOps.deepclone(a);
a == c; // false
*/
module.exports.deepclone = function(obj){
return JSON.parse(JSON.stringify(obj));
}
/**
Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
@param {string} string to verify
@returns {boolean}
@example
objectOps.isJSON("99 bottles of beer"); // false
objectOps.isJSON("99"); // true
objectOps.isJSON({"thingy":"that is an object"}); // true
objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
*/
module.exports.isJSON = function(string){
let stringIsJson = false;
let x
try {
x = JSON.parse(string);
} catch (e){
return false;
}
if(typeof x == "object" || typeof string == "object"){
stringIsJson = true;
}
return stringIsJson;
}
-40
View File
@@ -1,40 +0,0 @@
var assert = require('assert');
var objectOps = require('./objectOps');
describe("objectOps.js", function(){
/*describe("objectOps.clone()", function(){
it("clones javascript objects", function(){
var object1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var object2 = {};
object2 = objectOps.clone(object1);
assert.equal(object2.cat.name, object1.cat.name)
assert.equal(object1.hello, object2.hello)
});
it("does not deep clone objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.clone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.notEqual(obj2.cat.colors[1], "yellow");
});
});*/
describe("objectOps.deepclone()", function(){
it("deep clones javascript objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.deepclone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.equal(obj2.cat.colors[1], "yellow");
});
it("throws on non JSON parameters", function(){
assert.throws(function(){
let y = objectOps.deepclone(objectOps.deepclone);
});
});
});
});
-14
View File
@@ -1,14 +0,0 @@
function hashCode(string){
if(typeof string !== "string") throw "ERROR stringUtils: Not a string";
var hash = 0;
if (string.length == 0) return hash;
for (let i = 0; i < string.length; i++) {
char = string.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
module.exports = {
hashCode: hashCode,
}
-48
View File
@@ -1,48 +0,0 @@
const assert = require("assert");
const stringUtils = require("./stringUtils");
describe("stringUtils.js", function(){
describe("hashCode", function(){
it("Produces a 32bit signed int hash from a string", function(){
let x = stringUtils.hashCode("whatever");
let y = stringUtils.hashCode(" a much longer string to prove this works for long strings as well and also special characters &!¤(#)¤");
assert(typeof x == "number", "hashCode should be returning a 32 bit signed int");
assert(x.toString() == "1934383586");
assert(y.toString() == "1648886132");
});
let permutations = 1000;
it("Rarely produces same hash for different strings ("+permutations+" hashes with collision check)", function(){
let keyHashPairs = [];
for(let i = 0; i < permutations; i++){
let string = Math.random().toString();
keyHashPairs.push({
key:string,
hash:stringUtils.hashCode(string),
});
}
for(let i = 0; i < permutations; i++){
for(let o = 0; o < permutations; o++){
assert(i == o || keyHashPairs[o].key != keyHashPairs[i].key, "Woha, Math.random() generated 2 equal numbers???");
assert(i == o || keyHashPairs[o].hash != keyHashPairs[i].hash, "Hash collision, as long at it doesn't happen again its fine.");
}
}
});
it("Returns 0 for 0 length strings", function(){
let x = stringUtils.hashCode("");
assert.deepEqual(x, 0);
});
it("Throws when supplied with anything other than a string", function(){
assert.throws(function(){
stringUtils.hashCode([]);
}, "ERROR: Not a string")
assert.throws(function(){
stringUtils.hashCode({});
}, "ERROR: Not a string")
assert.throws(function(){
stringUtils.hashCode(0);
}, "ERROR: Not a string")
});
});
});
-11
View File
@@ -1,11 +0,0 @@
/**
An item with a count specified
@typedef {object} itemStack
@property {string} name The name of the item in the stack
@property {(number|string)} count The amount of items in the stack
@example
itemStack = {name:"iron-plate", count:"12"}
itemStack = {name:"copper-plate", count:972}
*/
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 154 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M105.009,19.309l-3.12,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68l0,6.2l-0.96,0.96l-7.596,-0.003l0,4.816l-0.96,0.953l0,0.007Zm6.876,0l-3.12,0l0,-3.944l0.96,-0.96l4.8,0l0,2.212l-2.64,2.692Zm-63.405,0l-9.917,0l-1.68,-1.68l0,-10.293l2.64,-2.64l3.118,0l0.002,9.827l2.718,0l0.002,-8.867l0.96,-0.96l3.117,0l1.68,1.68l0,10.293l-2.64,2.64Zm-15.652,0l-9.918,0l-1.68,-1.68l0,-10.293l2.64,-2.64l3.118,0l0.002,9.827l8.478,0.009l0,2.137l-2.64,2.64Zm-15.712,0l-9.996,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-0.96,0.942l0,0.018l-7.596,0l0,5.04l8.556,0l0,2.146l-2.64,2.64Zm47.185,0l-9.997,0l-1.68,-1.68l0,-2.142l0.96,-0.96l7.597,-0.002l0,-0.993l-8.557,0l0,-6.196l1.68,-1.68l0,0l0.96,-0.96c2.029,0 7.282,0 9.997,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-0.96,0.96l-7.597,0.002l0,0.957l8.557,0l0,6.227l-0.96,0.96l0,0l-1.68,1.68Zm12.444,0l-3.12,0l-1.68,-1.68l0,-6.216l0.96,-0.96l6.48,0l0,6.216l-2.64,2.64Zm19.325,0l-9.996,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-1.086,0.96l-7.47,0l0,0.959l5.841,0l0,2.13l-0.96,0.96l-4.881,0l0,0.991l8.556,0l0,2.146l-2.64,2.64Zm20.065,-7.896l0.96,-0.96l6.481,0l0,5.953l-2.64,2.903l-3.121,0l-1.68,-1.943l0,-5.953Zm21.411,7.896l-2.872,0l0,-1.178c-1.382,-0.123 -2.749,-0.431 -4.028,-0.977l-0.056,-0.024l-1.307,0.804l-2.03,-1.25l1.277,-0.786c-0.81,-0.666 -1.487,-1.522 -1.761,-2.534l-1.749,0l0,-1.768l0.96,-0.96l1.141,0c0.332,-0.616 0.809,-1.156 1.35,-1.61l-1.198,-0.78l0.94,-0.93l2.03,-1.25l1.227,0.755c0.949,-0.424 2.033,-0.741 3.204,-0.922l0,-0.248l0.013,0l0.947,-0.96l2.872,0l0,1.116c1.505,0.156 2.889,0.535 4.059,1.079l1.332,-0.82l2.03,1.25l-1.12,1.059c0.701,0.671 1.181,1.437 1.377,2.261l1.976,0l0,1.768l-0.96,0.962l-0.002,-0.002l-1.327,0c-0.228,0.624 -0.547,1.236 -1.053,1.681l1.109,0.679l-0.968,0.978l-0.015,-0.004l-2.007,1.236l-1.411,-0.868c-0.958,0.432 -1.978,0.728 -3.02,0.908l0,0.375l-0.96,0.93l0,0.03Zm-15.17,-7.656l-6.001,0l0,5.624l1.55,1.792l2.901,0l1.55,-1.792l0,-5.624Zm-44.191,0l-6,0l0,5.877l1.54,1.539l2.921,0l1.539,-1.539l0,-5.877Zm35.14,3.952l-4.32,0l0,3.464l2.778,0l1.542,-1.59l0,-1.874Zm-96.308,-9.709l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l9.798,0l1.539,-1.539l0,-1.807l-8.556,0l0,-6.48l8.556,0l0,-1.807l-1.539,-1.54Zm8.771,0l-2.778,0l-1.54,1.54l0,10.094l1.54,1.539l9.719,0l1.539,-1.539l0,-1.799l-8.478,-0.008l-0.002,-9.827Zm15.653,0l-2.779,0l-1.539,1.54l0,10.094l1.539,1.539l9.719,0l1.539,-1.539l0,-10.094l-1.539,-1.54l-2.778,0l-0.003,9.827l-4.157,0l-0.002,-9.827Zm54.53,0l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l9.798,0l1.539,-1.539l0,-1.807l-8.556,0l0,-2.431l5.841,0l0,-1.65l-5.841,0l0,-2.399l8.556,0l0,-1.807l-1.539,-1.54Zm-31.77,0l-9.797,0l-0.82,0.82l-0.72,0.72l0,5.856l8.557,0l0,2.432l-8.557,0.003l0,1.803l1.54,1.539l9.797,0l1.54,-1.539l0,-5.888l-8.557,0l0,-2.396l8.557,-0.003l0,-1.807l-1.54,-1.54Zm47.585,0l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l2.781,0l0,-5.777l8.556,0.004l0,-5.86l-1.539,-1.54Zm28.324,7.453c0.327,-0.363 0.421,-0.95 0.212,-1.53c-0.459,-1.279 -2.339,-2.021 -4.278,-2.122c-0.058,0 -0.058,0 -0.115,0c-2.792,0.201 -3.386,1.31 -3.433,1.781c-0.151,1.505 1.994,2.74 4.45,2.74c1.269,0 2.7,-0.357 3.164,-0.869Zm-31.101,-1.707l0,-2.398l-4.24,0.002l0,2.396l4.24,0Zm-3.04,-2.157l0,0.957l2.8,0l0,-0.958l-2.8,0.001Zm-24.048,0.012l-13.471,0l0,-2.161l2.64,-2.64l10.111,0l1.68,1.68c0,1.034 0,2.161 0,2.161l-0.96,0.96Zm40.606,-0.048l-6.392,0l0,-2.113l2.64,-2.64l4.801,-0.005l0,3.798l-1.049,0.96Zm-42.386,-3.553l-9.912,0l-1.539,1.54l0,1.821l12.991,0l0,-1.821l-1.54,-1.54Zm37.774,0l-1.54,1.54l0,1.773l6.001,0l0,-3.318c-1.487,0.002 -2.974,0.003 -4.461,0.005Z" style="fill:#141414;"/><path d="M73.625,19.309l3.12,0l-3.12,0Zm47.201,-0.24l-2.901,0l-1.55,-1.792l0,-5.624l6.001,0l0,5.624l-1.55,1.792Zm-44.18,0l-2.921,0l-1.54,-1.539l0,-5.877l6,0l0,5.877l-1.539,1.539Zm60.66,0l-2.392,0l0,-1.157l-0.288,-0.026c-1.392,-0.131 -2.761,-0.465 -4.055,-1.026l-1.288,0.792l-1.572,-0.968l1.175,-0.724l0.005,-0.076l-0.25,-0.205c-0.598,-0.5 -1.12,-1.098 -1.438,-1.808c-0.159,-0.355 -0.249,-0.743 -0.25,-0.747l-1.693,0l0,-1.288l1.676,0l0.056,-0.226c0.27,-1.047 1.011,-1.878 1.84,-2.574l-0.005,-0.072l-1.116,-0.688l1.572,-0.968l1.208,0.743l0.228,-0.1c1.335,-0.574 2.76,-0.86 4.195,-0.988l0,-1.072l2.392,0l0,1.092l0.284,0.03c1.394,0.155 2.76,0.518 4.031,1.105l1.316,-0.81l1.572,0.968l-1.259,0.776l0.236,0.276c0.559,0.497 1.039,1.085 1.324,1.77c0.148,0.356 0.225,0.734 0.226,0.738l1.924,0l0,1.288l-1.941,0l-0.059,0.219c-0.367,1.3 -1.768,2.53 -1.768,2.53l1.317,0.811l-1.572,0.968l-1.394,-0.858lLine truncated

After

Width:  |  Height:  |  Size: 6.3 KiB

-1201
View File
File diff suppressed because it is too large. Load diff
-184
View File
@@ -1,184 +0,0 @@
var app = require('./master.js'),
assert = require('assert'),
request = require('supertest');
const validateHTML = require('html5-validator');
var parallel = require('mocha.parallel');
describe('Master server endpoint testing', function() {
describe('#GET /api/getFactorioLocale', function() {
it('should get the basegame factorio locale', function(done) {
request(app).get('/api/getFactorioLocale').end(function(err, res) {
assert.equal(res.statusCode, 200);
let object = res.body;
// test that it is actual factorio locale, copied from getFactorioLocale.spec.js
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
Object.keys(object).forEach(key => {
// first level of the nested object is always an object
assert.equal(typeof object[key], "object");
Object.keys(object[key]).forEach(key2 => {
// second level of the nested object is always a string, nearly always truthy
if(key2 != "so-long-and-thanks-for-all-the-fish"){
assert.ok(object[key][key2]);
assert.equal(typeof object[key][key2], "string");
}
});
});
done();
});
});
});
// describe("#GET /api/")
parallel("#GET static website data", function() {
this.timeout(6000);
it("sends some HTML when accessing /", function(done){
this.timeout(6000);
request(app).get("/").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /nodes",function(done){
this.timeout(6000);
request(app).get("/nodes").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /settings",function(done){
this.timeout(6000);
request(app).get("/settings").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /nodeDetails",function(done){
this.timeout(6000);
request(app).get("/nodeDetails").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
assert(result.messages.length === 1, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
it("sends some HTML when accessing /remoteMap",function(done){
this.timeout(6000);
request(app).get("/remoteMap").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
// there should be 1 error regarding complaining about me using ES6 modules before they are fully supported
assert(result.messages.length === 2, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
});
let persistentMaster = request(app);
describe("#POST /api/place", function(){
it("adds an itemStack to the masters inventory", function(done){
persistentMaster.post("/api/place")
.send({
name:"steel-plate",
count:20,
instanceName:"unitTest"
})
.end(function(err,res){
assert(!err)
assert.equal(res.text, "success", "something went wrong with the request")
done();
});
});
});
describe("#GET /api/inventory", function(){
it("returns the masters current inventory", function(done){
persistentMaster.get("/api/inventory").end(function(err,res){
assert.equal(res.statusCode, 200);
let inventory = JSON.parse(res.text);
assert.equal(typeof inventory, "object", "Inventory should be an object");
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
done();
});
});
});
parallel("#POST /api/remove", function(){
it("returns an itemStack of how many items were removed", function(done){
persistentMaster.post("/api/remove")
.send({
name:"steel-plate",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.body.count, 10, "Something is wrong with the response, maybe the format changed slightly or you didn't have enough steel?");
done();
});
});
it("returns an empty itemStack if you try to request addItem or removeItem", function(done){
let casesRan = 0;
persistentMaster.post("/api/remove")
.send({
name:"addItem",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.text, '{"name":"addItem","count":0}', "When there are none of the item, you should get 0 back. addItem and removeItem should always return 0");
isDone();
});
persistentMaster.post("/api/remove")
.send({
name:"removeItem",
count:10
})
.end(function(err,res){
assert(!err);
assert.equal(res.text, '{"name":"removeItem","count":0}', "When there are none of the item, you should get 0 back. addItem and removeItem should always return 0");
isDone();
});
function isDone(){
if(++casesRan == 2) done();
}
});
it("returns an empty itemStack if you don't have any of the item you request", function(done){
persistentMaster.post("/api/remove")
.send({
name:"imaginaryItem",
count:999999
})
.end(function(err,res){
assert(!err);//{"name":"imaginaryItem","count":0}
assert.equal(res.body.name, "imaginaryItem", "Make sure body.name is the item we asked for");
assert.equal(res.body.count, 0, "Count should be 0 since we are asking for something that does not exist anywhere");
assert.equal(Object.keys(res.body).length, 2, "name and count should be the only keys on this object");
done();
});
});
});
});
-168
View File
@@ -1,168 +0,0 @@
/**
class factorioServer
This class has the responsiblity of :
* spawning a factorio server
* Handling input via stdin or RCON
*/
const fs = require("fs");
const child_process = require("child_process");
const path = require("path");
const mkdirp = require("mkdirp");
const appRootPath = require("app-root-path");
const fileOps = require("_app/fileOps");
module.exports = class factorioServer {
constructor({
factorioDirectory = "factorio",
instanceDirectory = "instances/data",
instanceName = "Default server name",
factorioPort = Math.floor(Math.random() * 65535),
serversettings = {},
onProgress = function(type, a, b, c){
if(type == "progress") console.log("Progress: ",a," of ",b," unit: ",c);
if(type == "log") console.log(a);
},
} = {}){
this.factorioDirectory = factorioDirectory;
this.instanceDirectory = instanceDirectory;
this.instanceName = instanceName;
this.factorioPort = factorioPort;
this.serversettings = Object.assign({
"name": "Clusterio instance: " + this.instanceName,
"description": "Public factorio server running clusterio",
"tags": ["clusterio"],
"max_players": "20",
"visibility": "public",
"username": "",
"token": "",
"game_password": "",
"verify_user_identity": true,
"admins": ["Danielv123"],
"allow_commands": true,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": false,
}, serversettings);
this.onProgress = (type, a, b, c) => {
if(typeof onProgress == "function") onProgress(type, a, b, c);
};
}
async initialize(){
if(!await this.isValidInstance()){
await this.createInstance();
}
return true;
}
async createInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
throw new Error("Instance already exists!");
}
// create folders
let folders = [
"/script-output/",
"/saves/",
"/mods/",
"/instanceMods/",
];
mkdirp.sync(path);
folders.forEach(folder => mkdirp.sync(path + folder));
// create files
let files = [
{name: "/script-output/output.txt", content: ""},
{name: "/script-output/orders.txt", content: ""},
{name: "/script-output/txbuffer.txt", content: ""},
{name: "/config.ini", content: "[path]\r\n\ read-data=__PATH__executable__/../../data\r\n\ write-data=__PATH__executable__/../../../" + this.instanceDirectory + "\r\n\ "},
{name: "/config.json", content: JSON.stringify({
factorioPort: this.factorioPort,
clientPassword: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
}, null, 4)},
{name: "/server-settings.json", content: JSON.stringify(this.serversettings, null, 4)},
];
files.forEach(file => {
fs.writeFile(path + file.name, file.content, doneWritingHandler);
// console.log("Created: ",path+file.name);
});
let filesToWrite = files.length;
function doneWritingHandler(){
if(!--filesToWrite){
return path;
}
}
}
async deleteInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
await fileOps.deleteFolderRecursive(path);
return true;
} else {
return false;
}
}
isValidInstance(path = this.instanceDirectory){
return new Promise((resolve, reject) => {
fs.stat(path, (err, stat) => {
if(err){
// console.log(err)
resolve(false);
} else {
// the instance folder exists, do some more checks
fs.readdir(path, (err, files) => {
if(files && Array.isArray(files)){
let foldersThatShouldBeThere = [
"script-output",
"saves",
"mods",
"instanceMods",
];
let instanceValidity = true;
foldersThatShouldBeThere.forEach(folder => {
if(!files.includes(folder)){
instanceValidity = false;
}
});
resolve(instanceValidity);
}
});
}
});
});
}
createMap(name = "map.zip"){
return new Promise((resolve, reject) => {
this.isValidInstance().then(isValid => {
if(!isValid){
return createInstance();
}
}).then(()=>{
this.onProgress("log", "Starting map creation");
// console.log(this.factorioDirectory + '/bin/x64/factorio')
let proc = child_process.spawn(
path.join(this.factorioDirectory, 'bin', 'x64', 'factorio'), [
'-c', path.join(this.instanceDirectory,'config.ini'),
'--create', path.join(this.instanceDirectory, 'saves',name),
'--server-settings', path.join(this.instanceDirectory, 'server-settings.json'),
],{
cwd:process.cwd(),
}
);
// console.log(proc.stdout.toString("utf8"));
proc.stdout.on("data", data => this.onProgress("log", data.toString("utf8")));
proc.on("close", code => {
this.onProgress("log", "Map created! "+code);
resolve();
});
});
});
}
}
-114
View File
@@ -1,114 +0,0 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const appRootPath = require("app-root-path");
const parallel = require("mocha.parallel");
const mkdirp = require("mkdirp");
const factorioServer = require("./index.js");
describe("class factorioServer/index.js", ()=>{
it("is a class (or at least a function)", ()=>{
assert.equal(typeof factorioServer, "function");
});
describe(".deleteInstance(instanceName)", ()=>{
it("deletes an instance, defaulting to the classes instanceDirectory", async () => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_deleteInstance"),
instanceName: "Unit testing instance",
});
await server.initialize();
await server.deleteInstance();
assert.equal(await server.isValidInstance(), false);
return true;
});
});
describe(".isValidInstance(instanceName)", ()=>{
it("tells us whether an instance exists or not", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance"),
instanceName: "Unit testing instance",
});
await server.deleteInstance();
let status = await server.isValidInstance();
assert.equal(status, false);
let x = await server.initialize(); // creates our instance
let status2 = await server.isValidInstance();
assert.equal(status2, true);
await server.deleteInstance(); // clean up folder
return true;
});
it("does not get tricked by an empty folder", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance_2"),
instanceName: "Unit testing instance",
});
assert.equal(await server.isValidInstance(), false);
mkdirp.sync(path.join(__dirname, "_testData_isValidInstance_2")); // create empty folder simulating instance
assert.equal(await server.isValidInstance(), false);
await server.initialize(); // create a proper instance
assert.equal(await server.isValidInstance(), true);
await server.deleteInstance(); // delete the instance and ensure we see that it is in fact deleted
assert.equal(await server.isValidInstance(), false);
return true;
}).timeout(5000);
});
describe(".initialize()", ()=>{
it("creates an instance", done => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_initialize"),
instanceName: "Unit testing instance",
});
server.initialize().then(()=>{
return server.deleteInstance();
}).then(() => {
done();
}).catch(e => console.log(e));
});
});
describe(".createMap(mapName.zip)", ()=>{
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert.equal(maps.length, 0);
await server.createMap();
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert(maps2.includes("map.zip"));
await server.deleteInstance();
return true;
});
it("supports custom filenames", async function(){
this.timeout(20000);
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert.equal(maps.length, 0);
await server.createMap("mapWithCustomName.zip");
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert(maps2.includes("mapWithCustomName.zip"));
await server.deleteInstance();
return true;
});
});
});
-120
View File
@@ -1,120 +0,0 @@
const fs = require("fs");
const path = require("path");
// gets newest file in a directory
// dir is directory
// files is array of filenames
// callback(err, filename string);
var getNewestFile = function(dir, files, callback) {
if (!callback) throw new Error("Missing callback");
if (!files || (files && files.length === 0)) {
callback(new Error("No files"));
return;
}
// we are appending the file name to the directory, without the
// "/" we would get paths like /folderfile.txt instead of /folder/file.txt
if(dir[dir.length-1] != "/"){
dir += "/";
}
var newest = { file: files[0] };
var checked = 0;
fs.stat(dir + newest.file, function(err, stats) {
if(err) {
callback(err);
} else {
newest.mtime = stats.mtime;
for (var i = 0; i < files.length; i++) {
var file = files[i];
(function(file) {
fs.stat(file, function(err, stats) {
++checked;
if (stats.mtime.getTime() > newest.mtime.getTime()) {
newest = { file : file, mtime : stats.mtime };
}
if (checked == files.length) {
callback(undefined,newest);
}
});
})(dir + file);
}
}
});
}
// get names of all directories in directory
var getDirectoriesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// get names of all files in a directory
var getFileNamesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return !fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// does what it says on the tin
var deleteFolderRecursiveSync = function(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursiveSync(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
return true;
} else {
return false;
}
};
var deleteFolderRecursive = function(path){
return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => {
if(err){
reject(err);
} else {
let thingsToDelete = files.length;
files.forEach(file => {
let curPath = path + "/" + file;
fs.stat(curPath, (err, stat) => {
if(!err){
if(stat.isDirectory()){
// recurse
deleteFolderRecursive(curPath).then(() => {
whenFilesDeleted();
}).catch(e => console.log(e));
} else {
fs.unlink(curPath, () => {
// console.log("Deleted "+curPath);
whenFilesDeleted();
});
}
} else {
resolve(false);
}
});
});
whenFilesDeleted();
function whenFilesDeleted(){
if(!thingsToDelete--){// var-- instead of --var because we are calling the function once too much and need the delay
fs.rmdir(path, () => {
resolve();
// console.log("Deleted "+path);
});
}
}
}
});
});
}
module.exports = {
getNewestFile,
getDirectoriesSync,
deleteFolderRecursiveSync,
deleteFolderRecursive,
getFileNamesSync,
}
-135
View File
@@ -1,135 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const fileOps = require('./fileOps');
// create folders for testing
function setupTestingEnv(){
if (!fs.existsSync(path.resolve(__dirname, "database"))){
fs.mkdirSync(path.resolve(__dirname, "database"));
}
if (!fs.existsSync(path.resolve(__dirname, "test"))){
fs.mkdirSync(path.resolve(__dirname, "test"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/another folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/another folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/file.txt"), "contents");
}
if (!fs.existsSync(path.resolve(__dirname, "test/another file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/another file.txt"), "more contents");
}
// we need this xfile to test a branch in fileOps.getNewestFile()s propagation
if (!fs.existsSync(path.resolve(__dirname, "test/xfile.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/xfile.txt"), "contents");
}
}
setupTestingEnv();
setupTestingEnv(); // run twice to get branch coverage
describe("fileOps.js", function(){
describe("fileOps.getDirectoriesSync()", function(){
it("gets an array of directory names from a path", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// because there are no arrays for some reason
assert(typeof directories == "object");
// they are sorted alphabetically because thats the way it is
assert(directories[0] == "another folder" && directories[1] == "folder");
});
it("does not return any filenames", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// this is a function to return directories, not files. So yeah.
assert(!directories.includes("file.txt") && !directories.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getDirectoriesSync();
}, Error);
});
});
describe("fileOps.getFileNamesSync(path)", function(){
it("gets names of files in directory", function(){
let files = fileOps.getFileNamesSync(path.resolve(__dirname, "test"));
assert(files.length > 0);
assert(files.includes("file.txt") && files.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getFileNamesSync();
}, Error);
});
});
describe("fileOps.getNewestFile()", function(){
it("gets the newest file in a directory async", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test/"), fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(newest && typeof newest.file == "string");
done();
});
});
it("throws if callback is missing", function(){
assert.throws(function(){
fileOps.getNewestFile("test", fileOps.getFileNamesSync(path.resolve(__dirname, "test")));
}, Error);
});
it("executes callback(err) if no files are provided", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test"), [], function(err, newest){
assert(err && typeof err == "object");
done();
});
});
it("executes callback(err) if launched without a directory", function(done){
fileOps.getNewestFile("", fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(err);
done();
});
});
it("executes callback(err) when you do stupid shit", function(done){
fileOps.getNewestFile({thisWontWork: "but not crash either"}, {lolz:"yeah..."}, function(err, newest){
assert(err);
// ENOENT: no such file or directory, stat 'C:\**\factorioClusterio\lib\test\undefined'
done();
});
});
});
describe("fileOps.deleteFolderRecursiveSync(path)", function(){
it("deletes a folder and all files and folders in it", function(){
let x = fileOps.deleteFolderRecursiveSync(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x);
// give it some extra time because its sometimes very slow
}).timeout(5000);
it("returns false if you give it an invalid path", function(){
let x = fileOps.deleteFolderRecursiveSync("This isn't a real path");
assert(!x);
});
});
describe("fileOps.deleteFolderRecursive(path)", function(){
it("deletes a folder and all files and folders in it, just like sync version", async function(){
setupTestingEnv();
assert(fs.statSync(path.resolve(__dirname, "test")));
let x = await fileOps.deleteFolderRecursive(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x === undefined, "Promise version of this function does not have a return value upon success");
return true;
}).timeout(5000);
it("throws like a promise does if you give it an invalid path", async function(){
let x = await fileOps.deleteFolderRecursive("Look this path is fake!")
.catch(e => {
assert(e);
assert.equal(e.code, "ENOENT");
return true;
});
});
});
});
-2
View File
@@ -1,2 +0,0 @@
const fileOps = require("./fileOps");
module.exports = fileOps;
-1
View File
@@ -1 +0,0 @@
module.exports = require("./isFactorioCommand.js");
-23
View File
@@ -1,23 +0,0 @@
const luamin = require("luamin");
function isFactorioCommand(command){
if(typeof command != "string"){
return false;
} else if(!command.includes("/silent-command ") && !command.includes("/c ")){
return false;
} else if(command[0] != "/"){
return false;
} else {
// check that what follows /c is valid LUA
let x = command.replace("/silent-command ", "").replace("/c ","");
let y
try{
y = luamin.minify(x);
} catch (e) {
return false;
}
// by this point it is surely a LUA command
return true;
}
}
module.exports = isFactorioCommand;
-18
View File
@@ -1,18 +0,0 @@
const isFactorioCommand = require("./isFactorioCommand");
const assert = require("assert");
describe("inventoryImports/isFactorioCommand.js()", function(){
it("Returns false if input is not a factorio command", function(){
assert(isFactorioCommand({}) == false, "This is not even a string so should return false");
assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false");
assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false");
assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command");
});
it("Returns true if it is a valid command", function(){
assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false")
});
it("Fails to understand that leading spaces are valid", function(){
assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?")
});
});
-9638
View File
File diff suppressed because it is too large. Load diff
+24 -70
View File
@@ -1,77 +1,31 @@
{
"name": "clusterio",
"version": "1.7.0",
"description": "Interserver communication for factorio",
"repository": "https://github.com/Danielv123/factorioClusterio",
"main": "master.js",
"name": "root",
"private": true,
"scripts": {
"test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo .",
"start": "echo Run node client.js start [instance] to start slaves && node master.js",
"cover": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo . && codecov -t f7b38085-36eb-4d72-84e6-64721910171e",
"docs": "documentation build index.js --config documentation.yml -f html -o docs",
"livedocs": "documentation serve --watch *.js lib/** --config documentation.yml",
"install": "node lib/npmPostinstall.js"
"test": "mocha --check-leaks -R spec --exclude \"**/node_modules/**\" --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",
"ci-cover": "nyc --reporter=lcovonly npm run-script test",
"lint": "eslint \"*.js\" packages plugins test",
"docs": "jsdoc --configure docs/jsdoc.json",
"bundle-dependencies": "bundle-dependencies"
},
"keywords": [
"factorio"
],
"author": "Danielv123",
"license": "ISC",
"dependencies": {
"app-root-path": "^2.0.1",
"as-table": "^1.0.31",
"averaged-timeseries": "^1.4.0",
"base64url": "^3.0.0",
"bcrypt": "^2.0.1",
"bcrypt-promise": "^2.0.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.2",
"cache-require-paths": "^0.3.0",
"cli-interact": "^0.1.9",
"codecov": "^3.0.1",
"compression": "^1.7.2",
"deepmerge": "^1.5.2",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-fileupload": "0.0.6",
"follow-redirects": "^1.5.0",
"fs-extra": "^6.0.1",
"getmac": "^1.4.0",
"hash-files": "^1.1.1",
"html5-validator": "^1.0.7",
"ini": "^1.3.5",
"jsonwebtoken": "^8.2.1",
"leveldown": "^1.9.0",
"linvodb3": "^3.25.1",
"luamin": "^1.0.4",
"mkdirp": "^0.5.1",
"mkdirp-promise": "^5.0.1",
"moment": "^2.20.1",
"ncp": "^2.0.0",
"nedb": "^1.8.0",
"needle": "^1.1.0",
"node-factorio-api": "^0.3.1-alpha",
"node-forge": "^0.7.1",
"prom-client": "^10.2.3",
"rcon-client": "^3.0.2",
"request": "^2.83.0",
"right-pad": "^1.0.1",
"rmdir-sync": "^1.0.1",
"sanitizer": "^0.1.3",
"single-line-log": "^1.1.2",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
"sync-request": "^4.0.1",
"underscore": "^1.8.3"
"nyc": {
"exclude": [
"test/**",
"plugins/*/test/**"
]
},
"engines": {
"node": ">=12"
},
"devDependencies": {
"documentation": "^5.4.0",
"istanbul": "^0.4.5",
"mocha": "^3.5.3",
"mocha-sinon": "^2.0.0",
"mocha.parallel": "^0.15.5",
"nock": "^9.1.6",
"sinon": "^2.4.1",
"supertest": "^3.0.0"
"eslint": "^7.25.0",
"eslint-plugin-node": "^11.1.0",
"jsdoc": "^3.6.6",
"lerna": "^4.0.0",
"mocha": "^8.4.0",
"nyc": "^15.1.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
# Clusterio installer
Installer for Clusterio.
See [the Clusterio README.md](https://github.com/clusterio/clusterio#readme) the for installation guide.
+635
View File
@@ -0,0 +1,635 @@
#!/usr/bin/env node
"use strict";
const child_process = require("child_process");
const events = require("events");
const fs = require("fs-extra");
const inquirer = require("inquirer");
const os = require("os");
const path = require("path");
const phin = require("phin");
const stream = require("stream");
const util = require("util");
const yargs = require("yargs");
const { levels, logger, setLogLevel } = require("./logging");
let dev = false;
const scriptExt = process.platform === "win32" ? ".cmd" : "";
const availablePlugins = [
{ name: "Global Chat", value: "@clusterio/plugin-global_chat" },
{ name: "Inventory Sync", value: "@clusterio/plugin-inventory_sync" },
{ name: "Player Auth", value: "@clusterio/plugin-player_auth" },
{ name: "Research Sync", value: "@clusterio/plugin-research_sync" },
{ name: "Statistics Exporter", value: "@clusterio/plugin-statistics_exporter" },
{ name: "Subspace Storage", value: "@clusterio/plugin-subspace_storage" },
// Comunity plugins
{ name: "Discord Bridge", value: "@hornwitser/discord_bridge" },
{ name: "Server Select", value: "@hornwitser/server_select" },
];
const factorioLocations = {
win32: [
"C:\\Program Files\\Factorio",
"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Factorio",
],
darwin: [
"/Applications/factorio.app/Contents",
],
};
class LineSplitter extends stream.Transform {
constructor(options) {
super(options);
this._partial = null;
}
_transform(chunk, encoding, callback) {
if (this._partial) {
chunk = Buffer.concat([this._partial, chunk]);
this._partial = null;
}
while (chunk.length) {
let end = chunk.indexOf("\n");
if (end === -1) {
this._partial = chunk;
break;
}
let next = end + 1;
// Eat carriage return as well if present
if (end >= 1 && chunk[end-1] === "\r".charCodeAt(0)) {
end -= 1;
}
let line = chunk.slice(0, end);
chunk = chunk.slice(next);
this.push(line);
}
callback();
}
_flush(callback) {
if (this._partial) {
this.push(this._partial);
this._partial = null;
}
callback();
}
}
class InstallError extends Error { }
async function safeOutputFile(file, data, options={}) {
let temporary = `${file}.tmp`;
await fs.outputFile(temporary, data, options);
await fs.rename(temporary, file);
}
async function execFile(cmd, args) {
logger.verbose(`executing ${cmd} ${args.join(" ")}`);
const asyncExec = util.promisify(child_process.execFile);
return new Promise((resolve, reject) => {
let child = child_process.execFile(cmd, args, (err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve({ stdout, stderr });
}
});
let stdout = new LineSplitter({ readableObjectMode: true });
stdout.on("data", line => { logger.verbose(line.toString()); });
child.stdout.pipe(stdout);
let stderr = new LineSplitter({ readableObjectMode: true });
stderr.on("data", line => { logger.verbose(`err: ${line.toString()}`); });
child.stderr.pipe(stderr);
});
}
async function execMaster(args) {
if (dev) {
return await execFile("node", [path.join("packages", "master"), ...args]);
}
return await execFile(path.join("node_modules", ".bin", `clusteriomaster${scriptExt}`), args);
}
async function execSlave(args) {
if (dev) {
return await execFile("node", [path.join("packages", "slave"), ...args]);
}
return await execFile(path.join("node_modules", ".bin", `clusterioslave${scriptExt}`), args);
}
async function execCtl(args) {
if (dev) {
return await execFile("node", [path.join("packages", "ctl"), ...args]);
}
return await execFile(path.join("node_modules", ".bin", `clusterioctl${scriptExt}`), args);
}
function validateSlaveToken(token) {
let parts = token.split(".");
if (parts.length !== 3) {
throw new InstallError("Invalid token");
}
let parsed;
try {
parsed = JSON.parse(Buffer.from(parts[1], "base64"));
} catch (err) {
throw new InstallError("Invalid token");
}
if (parsed.aud !== "slave" || !Number.isInteger(parsed.slave)) {
throw new InstallError("Invalid token");
}
}
async function validateInstallDir() {
let entries = new Set(await fs.readdir("."));
if (entries.size) {
if (!entries.has("package.json")) {
throw new InstallError("Refusing to install to non-empty directory");
}
let packageData;
try {
packageData = JSON.parse(await fs.readFile("package.json"));
} catch (err) {
throw new InstallError(`Failed to read package.json: ${err.message}`);
}
if (packageData.name !== "clusterio-install" || !packageData.private) {
throw new InstallError("Refusing to run on non-clusterio installation");
}
}
}
async function installClusterio(mode, plugins) {
try {
await safeOutputFile("package.json", JSON.stringify({
name: "clusterio-install",
private: true,
}, null, 2), { flag: "wx" });
} catch (err) {
if (err.code !== "EEXIST") {
throw new InstallError(`Failed to write package.json: ${err.message}`);
}
}
let components = [];
if (["standalone", "master"].includes(mode)) {
components.push("@clusterio/master");
}
if (["standalone", "slave"].includes(mode)) {
components.push("@clusterio/slave");
}
if (mode === "ctl") {
components.push("@clusterio/ctl");
}
logger.info(`Please wait, installing ${mode}`);
try {
await execFile(`npm${scriptExt}`, ["install", ...components, ...plugins]);
} catch (err) {
throw new InstallError(`Failed to install: ${err.message}`);
}
if (plugins.length) {
logger.info("Setting up plugins");
let pluginList;
try {
pluginList = new Map(JSON.parse(await fs.readFile("plugin-list.json")));
} catch (err) {
if (err.code === "ENOENT") {
pluginList = new Map();
} else {
throw new InstallError(`Error loading plugin-list.json: ${err.message}`);
}
}
for (let plugin of plugins) {
if (!pluginList.has(plugin)) {
// eslint-disable-next-line node/global-require
let pluginInfo = require(require.resolve(path.posix.join(plugin, "info"), { paths: [process.cwd()] }));
pluginList.set(pluginInfo.name, plugin);
}
}
try {
await safeOutputFile("plugin-list.json", JSON.stringify([...pluginList], null, 4));
} catch (err) {
throw new InstallError(`Error writing plugin-list.json: ${err.message}`);
}
}
}
async function writeScripts(mode) {
if (["standalone", "master"].includes(mode)) {
if (process.platform === "win32") {
await safeOutputFile(
"run-master.cmd",
"@echo off\n.\\node_modules\\.bin\\clusteriomaster.cmd run\n"
);
} else {
await safeOutputFile(
"run-master.sh",
"#!/bin/sh\nexec ./node_modules/.bin/clusteriomaster run\n",
{ mode: 0o755 },
);
await safeOutputFile(
"systemd/clusteriomaster.service",
`[Unit]
Description=Clusterio Master
[Service]
User=${os.userInfo().username}
Group=nogroup
WorkingDirectory=${process.cwd()}
KillMode=mixed
KillSignal=SIGINT
ExecStart=${process.cwd()}/node_modules/.bin/clusteriomaster run --log-level=warn
[Install]
WantedBy=multi-user.target
`);
}
}
if (["standalone", "slave"].includes(mode)) {
if (process.platform === "win32") {
await safeOutputFile(
"run-slave.cmd",
"@echo off\n.\\node_modules\\.bin\\clusterioslave.cmd run\n"
);
} else {
await safeOutputFile(
"run-slave.sh",
"#!/bin/sh\nexec ./node_modules/.bin/clusterioslave run\n",
{ mode: 0o755 },
);
await safeOutputFile(
"systemd/clusterioslave.service",
`[Unit]
Description=Clusterio Slave
[Service]
User=${os.userInfo().username}
Group=nogroup
WorkingDirectory=${process.cwd()}
KillMode=mixed
KillSignal=SIGINT
ExecStart=${process.cwd()}/node_modules/.bin/clusterioslave run --log-level=warn
[Install]
WantedBy=multi-user.target
`);
}
}
}
// eslint-disable-next-line complexity
async function inquirerMissingArgs(args) {
let answers = {};
if (args.mode) { answers.mode = args.mode; }
answers = await inquirer.prompt([
{
type: "list",
name: "mode",
message: "Operating mode to install",
default: "standalone",
choices: [
{ name: "Standalone (install both master and slave on this computer)", value: "standalone" },
{ name: "Master only", value: "master" },
{ name: "Slave only", value: "slave" },
{ name: "Ctl only", value: "ctl" },
{ name: "Plugins only", value: "plugins" },
],
},
], answers);
if (["standalone", "master"].includes(answers.mode)) {
if (args.admin) { answers.admin = args.admin; }
answers = await inquirer.prompt([
{
type: "input",
name: "admin",
message: "Admin account name",
validate: input => {
if (!input) {
return "May not be empty";
}
return true;
},
},
], answers);
}
if (answers.mode === "slave") {
if (args.slaveName) { answers.slaveName = args.slaveName; }
answers = await inquirer.prompt([
{
type: "input",
name: "slaveName",
message: "Name of slave",
},
], answers);
}
if (["slave", "ctl"].includes(answers.mode)) {
if (args.masterUrl) { answers.masterUrl = args.masterUrl; }
answers = await inquirer.prompt([
{
type: "input",
name: "masterUrl",
message: "Master server URL",
},
], answers);
if (args.masterToken) { answers.masterToken = args.masterToken; }
answers = await inquirer.prompt([
{
type: "input",
name: "masterToken",
message: "Master authentication Token",
},
], answers);
}
if (answers.mode === "slave") {
validateSlaveToken(answers.masterToken);
}
if (["standalone", "slave"].includes(answers.mode)) {
let myIp = "localhost";
if (args.publicAddress) {
answers.publicAddress = args.publicAddress;
} else {
try {
let result = await phin("https://api.ipify.org/");
myIp = result.body.toString();
} catch (err) { /* ignore */ }
}
answers = await inquirer.prompt([
{
type: "input",
name: "publicAddress",
message: "Public DNS/IP address of this server",
default: myIp,
},
], answers);
if (args.factorioDir) { answers.factorioDir = args.factorioDir; }
let locations = factorioLocations[process.platform] || [];
let foundLocations = [];
for (let location of locations) {
if (await fs.pathExists(path.join(location, "data", "changelog.txt"))) {
foundLocations.push(location);
}
}
if (process.platform === "linux") {
if (args.hasOwnProperty("downloadHeadless")) { answers.downloadHeadless = args.downloadHeadless; }
answers = await inquirer.prompt([
{
type: "confirm",
name: "downloadHeadless",
message: "(Linux only) Automatically download latest factorio release?",
default: true,
},
], answers);
if (answers.downloadHeadless) {
answers.factorioDir = "factorio";
}
}
answers = await inquirer.prompt([
{
type: "list",
name: "factorioDir",
message: "Path to Factorio installation",
choices: [
...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })),
{ name: "Use local factorio directory, you must copy an installation to it", value: "factorio" },
{ name: "Provide path manually", value: null },
],
},
], answers);
if (answers.factorioDir === null) {
answers = await inquirer.prompt([
{
type: "input",
name: "factorioDir",
message: "Path to Factorio installation",
askAnswered: true,
},
], answers);
} else if (answers.factorioDir === "factorio") {
await fs.ensureDir("factorio");
}
}
if (dev) {
answers.plugins = [];
} else if (args.plugins) {
answers.plugins = args.plugins;
}
answers = await inquirer.prompt([
{
type: "checkbox",
name: "plugins",
message: "Plugins to install",
choices: availablePlugins,
pageSize: 20,
},
], answers);
return answers;
}
async function downloadLinuxServer() {
let res = await phin("https://factorio.com/get-download/stable/headless/linux64");
const url = new URL(res.headers.location);
// get the filename of the latest factorio archive from redirected url
const filename = path.posix.basename(url.pathname);
const version = filename.match(/(?<=factorio_headless_x64_).*(?=\.tar\.xz)/)[0];
const tmpDir = "temp/create-temp/";
const archivePath = tmpDir + filename;
const tmpArchivePath = `${archivePath}.tmp`;
const factorioDir = `factorio/${version}/`;
const tmpFactorioDir = tmpDir + version;
if (await fs.pathExists(factorioDir)) {
logger.warn(`setting downloadDir to ${factorioDir}, but not downloading because already existing`);
} else {
await fs.ensureDir(tmpDir);
// follow the redirect
res = await phin({
url: url.href,
stream: true,
});
logger.info("Downloading latest Factorio server release. This may take a while.");
const writeStream = fs.createWriteStream(tmpArchivePath);
res.pipe(writeStream);
await events.once(res, "end");
await fs.rename(tmpArchivePath, archivePath);
try {
await fs.ensureDir(tmpFactorioDir);
await execFile("tar", [
"xf", archivePath, "-C", tmpFactorioDir, "--strip-components", "1",
]);
} catch (e) {
logger.error("error executing command- do you have 'xz-utils' installed?");
throw e;
}
await fs.unlink(archivePath);
await fs.rename(tmpFactorioDir, factorioDir);
}
}
async function main() {
let args = yargs
.option("log-level", {
nargs: 1, describe: "Log level to print to stdout", default: "info",
choices: ["none"].concat(Object.keys(levels)), type: "string",
})
.option("dev", {
nargs: 0, describe: "Initialize development repository", hidden: true, default: false, type: "boolean",
})
.option("mode", {
nargs: 1, describe: "Operating mode to install",
choices: ["standalone", "master", "slave", "ctl", "plugins"],
})
.option("admin", {
nargs: 1, describe: "Admin account name [standalone/master]", type: "string",
})
.option("slave-name", {
nargs: 1, describe: "Slave name [slave]", type: "string",
})
.option("master-url", {
nargs: 1, describe: "Master URL [slave/ctl]", type: "string",
})
.option("master-token", {
nargs: 1, describe: "Master authentication token [slave/ctl]", type: "string",
})
.option("public-address", {
nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/slave]", type: "string",
})
.option("factorio-dir", {
nargs: 1, describe: "Path to Factorio installation [standalone/slave]", type: "string",
})
.option("plugins", {
array: true, describe: "Plugins to install", type: "string",
})
.strict()
;
if (process.platform === "linux") {
args = args
.option("download-headless", {
nargs: 0,
describe: "(Linux only) Automatically download and unpack the latest factorio release. " +
"Can be set to false using --no-download-headless.",
type: "boolean",
})
.conflicts("factorio-dir", "download-headless");
}
args = args.argv;
setLogLevel(args.logLevel === "none" ? -1 : levels[args.logLevel]);
dev = args.dev;
if (!dev) {
await validateInstallDir();
}
let answers = await inquirerMissingArgs(args);
if (answers.downloadHeadless) {
await downloadLinuxServer();
}
logger.verbose(JSON.stringify(answers));
if (!dev) {
await installClusterio(answers.mode, answers.plugins);
}
let adminToken = null;
if (["standalone", "master"].includes(answers.mode)) {
logger.info("Setting up master");
await execMaster(["bootstrap", "create-admin", answers.admin]);
let result = await execMaster(["bootstrap", "generate-user-token", answers.admin]);
adminToken = result.stdout.split("\n").slice(-2)[0];
}
if (answers.mode === "standalone") {
logger.info("Setting up slave");
await execSlave(["config", "set", "slave.name", "local"]);
let result = await execSlave(["config", "show", "slave.id"]);
let slaveId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10);
result = await execMaster(["bootstrap", "generate-slave-token", slaveId]);
let slaveToken = result.stdout.split("\n").slice(-2)[0];
await execSlave(["config", "set", "slave.master_token", slaveToken]);
await execSlave(["config", "set", "slave.public_address", answers.publicAddress]);
await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]);
}
if (answers.mode === "slave") {
logger.info("Setting up slave");
let slaveId = JSON.parse(Buffer.from(answers.masterToken.split(".")[1], "base64")).slave;
await execSlave(["config", "set", "slave.id", slaveId]);
await execSlave(["config", "set", "slave.name", answers.slaveName]);
await execSlave(["config", "set", "slave.master_url", answers.masterUrl]);
await execSlave(["config", "set", "slave.master_token", answers.masterToken]);
await execSlave(["config", "set", "slave.public_address", answers.publicAddress]);
await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]);
}
if (!dev && ["standalone", "master", "slave"].includes(answers.mode)) {
logger.info("Writing run scripts");
await writeScripts(answers.mode);
}
if (answers.mode === "ctl") {
await execCtl(["control-config", "set", "control.master_url", answers.masterUrl]);
await execCtl(["control-config", "set", "control.master_token", answers.masterToken]);
}
/* eslint-disable no-console */
console.log(`Successfully installed ${answers.mode}`);
if (adminToken) {
console.log(`Admin authentication token: ${adminToken}`);
}
/* eslint-enable no-console */
}
if (module === require.main) {
main().catch(err => {
if (err instanceof InstallError) {
logger.error(err.message);
} else {
logger.fatal(`
+------------------------------------------------------------+
| Unexpected error occured installing clusterio, please |
| report it to https://github.com/clusterio/clusterio/issues |
+------------------------------------------------------------+
${err.stack}`
);
}
});
}
+51
View File
@@ -0,0 +1,51 @@
/* eslint-disable no-console */
"use strict";
const chalk = require("chalk");
const levels = Object.freeze({
fatal: 0,
error: 1,
warn: 2,
audit: 3,
info: 4,
verbose: 6,
});
const colors = {
fatal: "inverse red",
error: "bold redBright",
warn: "bold yellowBright",
audit: "bold greenBright",
info: "bold blueBright",
verbose: "bold grey",
};
let maxLogLevel = levels.info;
const logger = {};
for (let [name, level] of Object.entries(levels)) {
// eslint-disable-next-line no-loop-func
logger[name] = message => {
if (level > maxLogLevel) {
return;
}
let style = chalk;
for (let part of colors[name].split(" ")) {
style = style[part];
}
if (["fatal", "error"].includes(name)) {
console.error(`${style(name)} ${message}`);
} else if (["warn"].includes(name)) {
console.warn(`${style(name)} ${message}`);
} else {
console.log(`${style(name)} ${message}`);
}
};
}
module.exports = {
colors,
levels,
setLogLevel: (level) => { maxLogLevel = level; },
logger,
};
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@clusterio/create",
"description": "Installer for clusterio",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.10.c",
"main": "create.js",
"bin": {
"clusteriocreate": "./create.js"
},
"keywords": [
"factorio",
"clusterio"
],
"engines": {
"node": ">=12"
},
"dependencies": {
"chalk": "^4.1.0",
"fs-extra": "^8.1.0",
"inquirer": "^8.1.0",
"phin": "^3.5.1",
"yargs": "^14.2.0"
},
"publishConfig": {
"access": "public"
}
}
+91
View File
@@ -0,0 +1,91 @@
# 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.
## 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.
* `--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.
#### `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.
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.
#### `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.
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.
#### `plugin list`
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.
#### `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.
See docs/configuration.md in the main repositiory for the available configuration.
#### `config-control show <config-entry>`
Shows the value for a single config entry.
#### `config-control list`
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.
[The Clusterio repository](https://github.com/clusterio/clusterio) for instructions on how to set up a cluster.
+1402
View File
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@clusterio/ctl",
"description": "Clusterio command line management tool",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.10",
"main": "ctl.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"clusterioctl": "./ctl.js"
},
"keywords": [
"factorio"
],
"engines": {
"node": ">=12"
},
"dependencies": {
"@clusterio/lib": "^2.0.0-alpha.10",
"as-table": "^1.0.31",
"fs-extra": "^8.1.0",
"phin": "^3.5.1",
"set-blocking": "^2.0.0",
"winston": "^3.3.3",
"yargs": "^14.2.0"
},
"publishConfig": {
"access": "public"
}
}
+85
View File
@@ -0,0 +1,85 @@
"use strict";
/**
* Helper class for implementing the exponential backoff algorithm
*
* Provides a convenient interface for implementing an exponential backoff
* when reconnecting or resending in a distributed network.
*
* Once instantiated the delay method will return the next interval to wait
* for in milliseconds before making another attempt. For each attempt the
* delay chosen is a random value between 0 and the current maximum delay
* wwhich starts at a value of 2 * `base` and doubles for ever attempt up to
* the passed value of `max`. The value for the delay will reset back to
* `base` if more time than the value for `reset` has passed between two
* invocations of delay.
*
* @alias module:lib/ExponentialBackoff
*/
class ExponentialBackoff {
/**
* Construct ExponentialBackoff helper
*
* The options passed may also be modified on the instance at runtime.
*
* @param {object=} options -
* Options for this exponential backoff helper
* @param {number=} [options.base=1] -
* Base delay in seconds. The first delay will on average be this
* long, but can be anywhere between 0 and 2 times this value.
* @param {number=} [options.max=60] -
* Maximum delay in seconds. A delay will not take more than this
* value.
* @param {number=} [options.reset=2*max] -
* Time in seconds between two invocations of delay which will cause
* it to start over with `base` instead of doubling.
*/
constructor(options = {}) {
/**
* Base delay in seconds. The first delay will on average be this
* long, but can be anywhere between 0 and 2 times this value.
* @type {number}
*/
this.base = options.base || 1;
/**
* Maximum delay in seconds. A delay will not take more than this
* value.
* @type {number}
*/
this.max = options.max || 60;
/**
* Time in seconds between two invocations of delay which will cause
* it to start over with `base` instead of doubling.
* @type {number}
*/
this.reset = options.reset || 2 * this.max;
this._exp = 0;
this._lastInvocationTime = Date.now();
}
/**
* Compute the next delay to wait
*
* Returns the next delay to wait according to the exponential backoff
* algorithm. This is a value between 0 and min(2 * options.base *
* 2<sup>retries</sup>, optinos.max).
*
* @returns {number} time in milliseconds to wait.
*/
delay() {
let invocationTime = Date.now();
let interval = (invocationTime - this._lastInvocationTime) / 1000;
this._lastInvocationTime = invocationTime;
if (interval > this.reset) {
this._exp = 0;
}
this._exp = Math.min(this._exp + 1, Math.log2(this.max));
return Math.random() * this.base * 2 ** this._exp * 1000;
}
}
module.exports = ExponentialBackoff;
+5
View File
@@ -0,0 +1,5 @@
# Common Library for Clusterio
This package contains functionality shared between master, slave, ctl and/or plugins of Clusterio.
See [the Clusterio repository](https://github.com/clusterio/clusterio) for more information on Clusterio.
+104
View File
@@ -0,0 +1,104 @@
"use strict";
/**
* Helper class for rate limiting actions
*
* Provides a simple interface for implementing actions that are rate
* limited on invocations.
*
* @example
* const rateLimiter = RateLimiter({
* action: () => { console.log("action"); },
* maxRate: 1,
* });
*
* for (let i = 0; i < 100; i++) {
* rateLimiter.activate();
* // Prints action twice to the console, once for the first activation
* // and one more time 1 second later for 99 other activations that were
* // exceeding the rate limit.
* }
* @alias module:lib/RateLimiter
*/
class RateLimiter {
/**
* Construct RateLimiter helper
*
* The options passed may also be modified on the instance at runtime.
*
* @param {object=} options - Options for this rate limiter
* @param {number=} options.maxRate -
* Maximum rate of activations per second.
* @param {function()=} options.action -
* Callback invoked at maste `maxRate` per second on activation.
*/
constructor(options = {}) {
/**
* Maximum rate of activations per second.
* @type {number}
*/
this.maxRate = options.maxRate || 1;
/**
* Callback invoked at most `maxRate` per second on activation.
* @type {function()}
*/
this.action = options.action || null;
this._lastRun = 0;
this._runTimeout = null;
this.lastProgressBroadcast = Date.now();
}
/**
* Activate the rate limited action
*
* Checks if the invocation of the rate limited action now would fall
* within the rate limit. If it is within the limit then the action is
* invoked immediatly, otherwise a timeout is registered to invoke the
* action at next point in time where it will be within the rate limit.
*
* @returns {boolean} true if activation was within the rate limit.
*/
activate() {
let now = Date.now();
if (now < this._lastRun + 1000 / this.maxRate) {
if (!this._runTimeout && this.action !== null) {
this._runTimeout = setTimeout(() => {
this._runTimeout = null;
this._lastRun = Date.now();
this.action();
}, this._lastRun + 1000 / this.maxRate - now);
}
return false;
}
// We may end up activating before the timeout does.
if (this._runTimeout) {
clearTimeout(this._runTimeout);
this._runTimeout = null;
}
this._lastRun = now;
if (this.action !== null) {
this.action();
}
return true;
}
/**
* Cancel pending action
*
* Cancel pending action invocation. This is necessary when shutting
* down the use of the rate limit as a timeout is registered to invoke
* the next action when activated faster than the rate limit allows.
*/
cancel() {
if (this._runTimeout) {
clearTimeout(this._runTimeout);
}
}
}
module.exports = RateLimiter;
+12
View File
@@ -0,0 +1,12 @@
export { default as libConfig } from "./config";
export { default as libErrors } from "./errors";
export { default as libFactorio } from "./factorio/exchange_string";
export { default as libHelpers } from "./helpers";
export { default as libLink } from "./link";
export { default as libLogging } from "./logging";
export { default as libPlugin } from "./plugin";
export { default as libSchema } from "./schema";
export { default as libUsers } from "./users";
export { default as ExponentialBackoff } from "./ExponentialBackoff";
export { default as RateLimiter } from "./RateLimiter";
+131
View File
@@ -0,0 +1,131 @@
/* eslint-disable no-console */
"use strict";
const events = require("events");
const fs = require("fs-extra");
const JSZip = require("jszip");
const klaw = require("klaw");
const path = require("path");
const yargs = require("yargs");
async function buildMod(args, info) {
if (args.build) {
await fs.ensureDir(args.outputDir);
let modName = `${info.name}_${info.version}`;
if (args.pack) {
let zip = new JSZip();
let walker = klaw(args.sourceDir)
.on("data", item => {
if (item.stats.isFile()) {
// On Windows the path created uses backslashes as the directory sepparator
// but the zip file needs to use forward slashes. We can't use the posix
// version of relative here as it doesn't work with Windows style paths.
let basePath = path.relative(args.sourceDir, item.path).replace(/\\/g, "/");
zip.file(path.posix.join(modName, basePath), fs.createReadStream(item.path));
}
})
;
await events.once(walker, "end");
for (let [fileName, pathParts] of Object.entries(info.additional_files || {})) {
let filePath = path.join(args.sourceDir, ...pathParts);
if (!await fs.pathExists(filePath)) {
throw new Error(`Additional file ${filePath} does not exist`);
}
zip.file(path.posix.join(modName, fileName), fs.createReadStream(filePath));
}
delete info.additional_files;
zip.file(path.posix.join(modName, "info.json"), JSON.stringify(info, null, 4));
let modPath = path.join(args.outputDir, `${modName}.zip`);
console.log(`Writing ${modPath}`);
let writeStream = zip.generateNodeStream().pipe(fs.createWriteStream(modPath));
await events.once(writeStream, "finish");
} else {
let modDir = path.join(args.outputDir, modName);
if (await fs.exists(modDir)) {
console.log(`Removing existing build ${modDir}`);
await fs.remove(modDir);
}
console.log(`Building ${modDir}`);
await fs.copy(args.sourceDir, modDir);
for (let [fileName, pathParts] of Object.entries(info.additional_files) || []) {
let filePath = path.join(...pathParts);
await fs.copy(filePath, path.join(modDir, fileName));
}
delete info.additional_files;
await fs.writeFile(path.join(modDir, "info.json"), JSON.stringify(info, null, 4));
}
}
}
async function build(args) {
let info = JSON.parse(await fs.readFile(path.join(args.sourceDir, "info.json")));
if (args.factorioVersion) {
info.factorio_version = args.factorioVersion;
}
if (args.dependencies) {
info.dependencies = args.dependencies;
}
if (args.clean) {
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/;
for (let entry of await fs.readdir(args.outputDir)) {
let match = splitter.exec(entry);
if (match) {
let [, name, version] = match;
if (name === info.name) {
let modPath = path.join(args.outputDir, entry);
console.log(`Removing ${modPath}`);
await fs.remove(modPath);
}
}
}
}
if (info.variants) {
for (let [variant, variantOverrides] of Object.entries(info.variants)) {
let variantInfo = {
...info,
...variantOverrides,
};
delete variantInfo.variants;
await buildMod(args, variantInfo);
}
} else {
await buildMod(args, info);
}
}
async function main() {
const args = yargs
.scriptName("build")
.options({
"clean": { describe: "Remove previous builds", type: "boolean", default: false },
"build": { describe: "Build mod(s)", type: "boolean", default: true },
"pack": { describe: "Pack into zip file", type: "boolean", default: true },
"source-dir": { describe: "Path to mod source files", nargs: 1, type: "string", demandOption: true },
"output-dir": { describe: "Path to output built mod(s)", nargs: 1, type: "string", default: "dist" },
"factorio-version": { describe: "Override factorio_version", type: "string" },
})
.strict()
.argv
;
await build(args);
}
if (module === require.main) {
main().catch(err => { console.log(err); });
}
module.exports = {
build,
};
+248
View File
@@ -0,0 +1,248 @@
/**
* Command library
*
* Data types and utilites for commands to clusterioctl's CLI.
*
* @author Hornwitser
* @module lib/command
*/
"use strict";
const libLink = require("./link");
const libErrors = require("./errors");
/**
* Represents a command that can be runned by clusterioctl
* @static
*/
class Command {
/**
* Define an executable command.
*
* @param {Object} cmd - Command definiton.
* @param {Array} cmd.definiton -
* Arguments to pass to yargs .command method to define this
* command.
* @param {function(args, control)} handler -
* Async function invoked when the command is executed. Is given
* the parsed args and a reference to the {@link
* module:ctl/ctl.Control} instance.
*/
constructor({ definition, handler }) {
if (definition[0] instanceof Array) {
this.name = definition[0][0].split(" ")[0];
this.alias = definition[0].slice(1);
} else {
this.name = definition[0].split(" ")[0];
this.alias = [];
}
this._definition = definition;
this._handler = handler;
}
register(yargs) {
yargs.command(...this._definition);
}
async run(args, control) {
await this._handler(args, control);
}
}
/**
* A node in the command tree that can hold commands and sub trees
*
* Container which can contain {@link module:lib/command.Command}s and other
* CommandTrees. This is used by clusterioctl to hold the full tree of
* available commands, you may extend this tree by using a control plugin,
* see {@link module:lib/plugin.BaseControlPlugin#addCommands}
*
* @static
*/
class CommandTree {
/**
* Define a command containing sub commands.
*
* @param {Object} cmd - Command definition
* @param {string} cmd.name - Name of the command.
* @param {Array<string>} cmd.alias - Aliases for this tree.
* @param {description} cmd.description -
* Descripton to provide for this command tree node.
*/
constructor({ name, alias, description }) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
this.name = name;
this.alias = alias || [];
this.description = description;
this.subCommands = new Map();
}
/**
* Add a command or command tree to this tree
*
* @param {module:lib/command.Command|module:lib/command.CommandTree} command -
* The command to add to this command tree.
*/
add(command) {
if (this.subCommands.has(command.name)) {
throw new Error(`Command ${command.name} already exists`);
}
this.subCommands.set(command.name, command);
for (let alias of command.alias) {
if (this.subCommands.has(alias)) {
throw new Error(`Alias ${alias} for command ${command.name} already exists`);
}
this.subCommands.set(alias, command);
}
}
/**
* Get a command or command tree from this tree
*
* @param {string} name -
* The name of the command or command tree to retrieve.
* @return {?module:lib/command.Command|module:lib/command.CommandTree}
* The command to add to this command tree.
*/
get(name) {
return this.subCommands.get(name) || null;
}
/**
* Insert the registered commands into the given yargs parser
*
* Traverses the the tree of sub-commands recursively and adds all of
* them to the given yargs parser.
*
* @param {Object} yargs - yargs parser to register with.
*/
register(yargs) {
yargs.command([this.name].concat(this.alias), this.description, (yargs) => {
for (let [name, command] of this.subCommands) {
// Check if the entry is not an alias.
if (name === command.name) {
command.register(yargs);
}
}
}, () => {
yargs.showHelp();
yargs.exit();
});
}
}
/**
* Resolve a string into a slave ID
*
* Resolves a string with either an slave name or an id into an integer with
* the slave ID.
*
* @param {module:lib/link.Link} client - link to master server to query slave on.
* @param {string} slaveName - string with name or id of slave.
* @returns {number} slave ID.
* @static
*/
async function resolveSlave(client, slaveName) {
let slaveId;
if (/^-?\d+$/.test(slaveName)) {
slaveId = parseInt(slaveName, 10);
} else {
let response = await libLink.messages.listSlaves.send(client);
for (let slave of response.list) {
if (slave.name === slaveName) {
slaveId = slave.id;
break;
}
}
if (slaveId === undefined) {
throw new libErrors.CommandError(`No slave named ${slaveName}`);
}
}
return slaveId;
}
/**
* Resolve a string to an instance ID
*
* Resolves a string with either an instance name or an id into an integer
* with the instance ID.
*
* @param {module:lib/link.Link} client - link to master server to query instance on.
* @param {string} instanceName - string with name or id of instance.
* @returns {number} instance ID.
* @static
*/
async function resolveInstance(client, instanceName) {
let instanceId;
if (/^-?\d+$/.test(instanceName)) {
instanceId = parseInt(instanceName, 10);
} else {
let response = await libLink.messages.listInstances.send(client);
for (let instance of response.list) {
if (instance.name === instanceName) {
instanceId = instance.id;
break;
}
}
if (instanceId === undefined) {
throw new libErrors.CommandError(`No instance named ${instanceName}`);
}
}
return instanceId;
}
/**
* Retrieve role object from string
*
* Resolves a string with either a role name or an id into an object
* representing the role.
*
* @param {module:lib/link.Link} client - link to master server to query role on.
* @param {string} roleName - string with name or id of role.
* @returns {Object} Role info.
* @static
*/
async function retrieveRole(client, roleName) {
let response = await libLink.messages.listRoles.send(client);
let resolvedRole;
if (/^-?\d+$/.test(roleName)) {
let roleId = parseInt(roleName, 10);
for (let role of response.list) {
if (role.id === roleId) {
resolvedRole = role;
break;
}
}
} else {
for (let role of response.list) {
if (role.name === roleName) {
resolvedRole = role;
break;
}
}
}
if (!resolvedRole) {
throw new libErrors.CommandError(`No role named ${roleName}`);
}
return resolvedRole;
}
module.exports = {
Command,
CommandTree,
resolveSlave,
resolveInstance,
retrieveRole,
};
+834
View File
@@ -0,0 +1,834 @@
// Configuration classes
"use strict";
const events = require("events");
const isDeepStrictEqual = require("../is_deep_strict_equal");
const { basicType } = require("../helpers");
/**
* Invalid Access exception
*
* Exception class for when an access attempt is made to a field that
* does not permitt this access is done.
*
* @extends Error
* @memberof module:lib/config
*/
class InvalidAccess extends Error { };
/**
* Invalid Value exception
*
* Exception class for when invalid values are attempted to be set on a
* field.
*
* @extends Error
* @memberof module:lib/config
*/
class InvalidValue extends Error { };
/**
* Invalid Field exception
*
* Exception class for when a group or field that does not exist is
* attempted to be accessed.
*
* @extends Error
* @memberof module:lib/config
*/
class InvalidField extends Error { };
/**
* Split the given string on the first instance of separator
*
* Splits `string` on the first instance of `separator` and returns an
* array consisting of the string up to the separator and the string
* after the separator. Returns an array with the string and an empty
* string if the separator is not present.
*
* @param {string} separator - Separator to split string by.
* @param {string} string - String to split
* @returns {Array} string split on separator.
* @memberof module:lib/config
* @private
* @inner
*/
function splitOn(separator, string) {
let index = string.indexOf(separator);
if (index === -1) {
return [string, ""];
}
return [string.slice(0, index), string.slice(index + separator.length)];
}
/**
* Collection of config groups
*
* Represents a collection of ConfigGroup instances that can be
* managed as one.
*
* This is an EventEmitter with the following events:
* - fieldChanged -
* invoked after a field has been updated in one of the config groups
* belonging to this config. Takes three parameters: group, field,
* prev.
*
* @extends events.EventEmitter
*
* @memberof module:lib/config
*/
class Config extends events.EventEmitter {
/**
* Create a new instance of the given config
*
* @param {string} location -
* Location to evaluate access for this instance from
*/
constructor(location) {
super();
if (!this.constructor._finalized) {
throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`);
}
if (typeof location !== "string") {
throw new Error("location must be a string");
}
this.location = location;
this._groups = new Map();
this._unknownGroups = new Map();
}
async init() {
for (let [name, GroupClass] of this.constructor._groups) {
if (!this._groups.has(name)) {
let group = new GroupClass(this);
await group.init();
this._groups.set(name, group);
}
}
this._initialized = true;
}
_validate(serializedConfig) {
if (basicType(serializedConfig) !== "object") {
throw new Error(`Expected object, not ${basicType(serializedConfig)} for config`);
}
if (basicType(serializedConfig.groups) !== "array") {
throw new Error(`Expected groups to be an array, not ${basicType(serializedConfig.groups)}`);
}
}
/**
* Load config from a serialized object
*
* @param {object} serializedConfig - Serialized config to load.
* @param {string=} location - Location used for access control.
*/
async load(serializedConfig, location = this.location) {
this._validate(serializedConfig);
for (let serializedGroup of serializedConfig.groups) {
let GroupClass = this.constructor._groups.get(serializedGroup.name);
if (!GroupClass) {
this._unknownGroups.set(serializedGroup.name, serializedGroup);
continue;
}
let group = new GroupClass(this);
await group.load(serializedGroup, location);
this._groups.set(group.name, group);
}
await this.init();
}
/**
* Serialize the config to a plain JavaScript object
*
* @param {string=} location - Location used for access control.
* @returns {Object} JSON serializable representation of the config.
*/
serialize(location = this.location) {
let groups = [...this._unknownGroups.values()];
for (let group of this._groups.values()) {
groups.push(group.serialize(location));
}
return { groups };
}
/**
* Update the configuration based on a serialized config
*
* Updates all groups in the config with groups in the serialized
* config passed.
*
* @param {Object} serializedConfig - Output from .serialize().
* @param {boolean} notify - Invoke fieldChanged events if true.
* @param {string=} location - Location used for access control.
*/
update(serializedConfig, notify, location = this.location) {
this._validate(serializedConfig);
for (let serializedGroup of serializedConfig.groups) {
let group = this._groups.get(serializedGroup.name);
if (group) {
group.update(serializedGroup, notify, location);
} else {
this._unknownGroups.set(serializedGroup.name, serializedGroup);
}
}
}
/**
* Check if field can be accessed from the given location
*
* @param {string} name - Name of the field to check.
* @param {string=} location - Location used for access control.
* @returns {boolean} true if field is accessible
*/
canAccess(name, location = this.location) {
let [group, field] = splitOn(".", name);
return this.group(group).canAccess(field, location);
}
/**
* Get the value for a config field
*
* @param {string} name - Name of field to get.
* @param {string=} location - Location used for access control.
* @returns {*} the value for the field.
*/
get(name, location = this.location) {
let [group, field] = splitOn(".", name);
return this.group(group).get(field, location);
}
/**
* Set the value for a config field
*
* @param {string} name - Name of field to set.
* @param {*} value - the value to set on field
* @param {string=} location - Location used for access control.
*/
set(name, value, location = this.location) {
let [group, field] = splitOn(".", name);
this.group(group).set(field, value, location);
}
/**
* Set property of an object value for a config field
*
* @param {string} name - Name of field to set property on.
* @param {string} prop - Name of property to set on field.
* @param {*} value - the value to set the property to.
* @param {string=} location - Location used for access control.
*/
setProp(name, prop, value, location = this.location) {
let [group, field] = splitOn(".", name);
this.group(group).setProp(field, prop, value, location);
}
/**
* Get the config group instance with the given name
*
* @param {string} name - Name of group to get.
* @returns {module:lib/config.ConfigGroup} config group.
*/
group(name) {
if (!this._initialized) {
throw new Error(`${this.constructor.name} instance is uninitialized`);
}
let group = this._groups.get(name);
if (!group) {
throw new InvalidField(`No config group named '${name}'`);
}
return group;
}
static _initSubclass() {
// These properties are initialized here in order to ensure that
// they end up on the sub-class.
if (!this._initialized) {
this._initialized = true;
this._finalized = false;
this._groups = new Map();
}
}
/**
* Mapping of group name to group class
*
* Returns the internal mapping of group names to group classes that
* mave been registered with this config. Note that mutating this
* object will lead to unexpected behaviour.
*
* @returns {Map<string, GroupClass>} config groups
*/
static get groups() {
this._initSubclass();
return this._groups;
}
/**
* Locks this Config class from adding more groups.
*/
static finalize() {
this._initSubclass();
this._finalized = true;
}
/**
* Register a ConfigGroup with this Config
*
* Adds the ConfigGroup to this config as a group that will get
* initialized, loaded and serialized with this config class.
*
* @param {module:lib/config.ConfigGroup} group - class to register.
*/
static registerGroup(group) {
this._initSubclass();
if (this._finalized) {
throw new Error("Cannot register group on finalized config");
}
if (!group._finalized) {
throw new Error("Group must be finalized before it can be registered");
}
if (this._groups.has(group.groupName)) {
throw new Error(`${group.groupName} has already been registered`);
}
this._groups.set(group.groupName, group);
}
}
/**
* Collection of related config entries
*
* Type checks and stores a collection of related config entries. Attempt
* to set invalid values for fields will result in an error thrown while
* loading a config with invalid values will result in those being replaced
* by the innitial value for those fields.
*
* @memberof module:lib/config
*/
class ConfigGroup {
/**
* Creates a new config group.
*
* After the creation of the new config group you have to call
* either .init() or .load() on it in order to fully initialize it.
*
* @param {module:lib/config.Config} config -
* Parent config for this group instance.
*/
constructor(config) {
if (!this.constructor._finalized) {
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`);
}
this._fields = new Map();
this._unknownFields = new Map();
this.config = config;
}
/**
* Check access for a field
*
* @param {object} def - definition for for the field to check.
* @param {string} remote - location of remote access to check for.
* @param {boolean} error - throw on failure to pass check.
* @throws {module:lib/config.InvalidAccess}
* if access is not granted and error is true.
* @returns {boolean} true if access is granted
* @private
*/
_checkAccess(def, remote, error) {
for (let loc of [remote, this.config.location]) {
if (!def.access.includes(loc)) {
if (error) {
throw new InvalidAccess(`Field '${def.name}' is not accessible from ${loc}`);
} else {
return false;
}
}
}
return true;
}
/**
* Initialize new config group
*
* Computes and assigns the initial values for all fields of the
* config group.
*/
async init() {
for (let [name, def] of this.constructor._definitions) {
if (this._fields.has(name)) {
continue;
}
if (!def.access.includes(this.config.location)) {
continue;
}
let value = null;
if (typeof def.initial_value === "function") {
value = await def.initial_value();
} else if (def.initial_value !== undefined) {
value = def.initial_value;
}
this._fields.set(name, value);
}
}
/**
* Load from serialized group
*
* Loads all the values from the passed serialized version of the
* group and then initializes any possible missing fields from it to
* their default values.
*
* @param {Object} serializedGroup -
* Result from a previous call to .serialize().
* @param {string=} location - Location used for access control.
*/
async load(serializedGroup, location = this.config.location) {
this.update(serializedGroup, false, location);
await this.init();
}
/**
* Name of the group
*
* Shortcut for `group.constructor.groupName`
*/
get name() {
return this.constructor.groupName;
}
/**
* Check if field can be accessed from the given location
*
* @param {string} name - Name of field to check.
* @param {string=} location - Location used for access control.
* @returns {boolean} true if field is accessible
*/
canAccess(name, location = this.config.location) {
let def = this.constructor._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
return this._checkAccess(def, location, false);
}
/**
* Get value for field
*
* @param {string} name - Name of field to get.
* @param {string=} location - Location used for access control.
* @returns {*} Value stored for the field.
*/
get(name, location = this.config.location) {
let def = this.constructor._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
this._checkAccess(def, location, true);
return this._fields.get(name);
}
/**
* Set value of field
*
* The field must be defined for the config group and the value is
* type checked against the field definition.
*
* @param {string} name - Name of field to set.
* @param {*} value - Value to set for field.
* @param {string=} location - Location used for access control.
* @throws {module:lib/config.InvalidField} if field is not defined.
* @throws {module:lib/config.InvalidValue} if value is not allowed for the field.
*/
set(name, value, location = this.config.location) {
let def = this.constructor._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
this._checkAccess(def, location, true);
// Empty strings are treates as null
if (value === "") {
value = null;
}
if (def.enum && !def.enum.includes(value)) {
throw new InvalidValue(`Expected one of [${def.enum.join(", ")}], not ${value}`);
}
if (basicType(value) === "string") {
if (def.type === "boolean") {
if (value === "true") {
value = true;
} else if (value === "false") {
value = false;
}
} else if (def.type === "number") {
let numberRegExp = /^[+-]?(Infinity|\d+\.(\d+)?([eE][+-]?\d+)?|\.?\d+([eE][+-]?\d+)?)$/;
if (numberRegExp.test(value.trim())) {
value = Number.parseFloat(value);
}
} else if (def.type === "object") {
try {
value = JSON.parse(value);
} catch (err) {
throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`);
}
}
}
if (value === null) {
if (!def.optional) {
throw new InvalidValue(`Field ${name} cannot be null`);
}
} else if (basicType(value) !== def.type) {
throw new InvalidValue(`Expected type of ${name} to be ${def.type}, not ${basicType(value)}`);
}
let prev = this._fields.get(name);
this._fields.set(name, value);
if (this.config && !isDeepStrictEqual(value, prev)) {
this.config.emit("fieldChanged", this, name, prev);
}
}
/**
* Set property of object field
*
* Update value of stored object field by setting the specified property
* on it. The field must be defined as an object for the config group.
*
* @param {string} name - Name of field to set property on.
* @param {string} prop - Name of property to set on field.
* @param {*} value - the value to set the property to.
* @param {string=} location - Location used for access control.
* @throws {module:lib/config.InvalidField} if field is not defined.
* @throws {module:lib/config.InvalidValue} if field is not an object.
*/
setProp(name, prop, value, location = this.config.location) {
let def = this.constructor._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
this._checkAccess(def, location, true);
if (def.type !== "object") {
throw new InvalidValue(`Cannot set property on non-object field '${name}'`);
}
let prev = this._fields.get(name);
let updated = {...prev || {}};
if (value !== undefined) {
updated[prop] = value;
} else {
delete updated[prop];
}
this._fields.set(name, updated);
if (this.config && !isDeepStrictEqual(updated, prev)) {
this.config.emit("fieldChanged", this, name, prev);
}
}
/**
* Update a config group from a serialized group
*
* Overwrites the values of all the fields in this config group that
* has a valid value in the passed serialized group with that value.
*
* @param {Object} serializedGroup -
* Result from a previous call to .serialize().
* @param {boolean} notify - Invoke fieldChanged events if true.
* @param {string=} location - Location used for access control.
*/
update(serializedGroup, notify, location = this.config.location) {
if (basicType(serializedGroup) !== "object") {
throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`);
}
if (serializedGroup.name !== this.name) {
throw new Error(`Expected group name ${this.name}, not ${serializedGroup.name}`);
}
if (basicType(serializedGroup.fields) !== "object") {
throw new Error(`Expected fields to be an object, not ${basicType(serializedGroup.fields)}`);
}
for (let [name, value] of Object.entries(serializedGroup.fields)) {
let def = this.constructor._definitions.get(name);
if (!def) {
this._unknownFields.set(name, value);
continue;
}
if (!this._checkAccess(def, location, false)) {
continue;
}
if (def.optional) {
if (value !== null && basicType(value) !== def.type) {
continue;
}
} else if (basicType(value) !== def.type) {
continue;
}
if (def.enum && !def.enum.includes(value)) {
continue;
}
let prev = this._fields.get(name);
this._fields.set(name, value);
if (notify && this.config && !isDeepStrictEqual(value, prev)) {
this.config.emit("fieldChanged", this, name, prev);
}
}
}
/**
* Serialize the config to a plain JavaScript object
*
* @param {string=} location - Location used for access control.
* @returns {Object} JSON serializable representation of the group.
*/
serialize(location = this.config.location) {
let fields = {};
for (let [name, value] of this._fields) {
let def = this.constructor._definitions.get(name);
if (!this._checkAccess(def, location, false)) {
continue;
}
fields[name] = value;
}
for (let [name, value] of this._unknownFields) {
fields[name] = value;
}
return {
name: this.name,
fields,
};
}
static _initSubclass() {
// These properties are initialized here in order to ensure that
// they end up on the sub-class.
if (!this._initialized) {
this._initialized = true;
this._definitions = new Map();
}
}
/**
* Mapping of config name to field meta data
*
* Returns the internal meta data for the fields of this class.
* Note that mutating this object will lead to unexpected behaviour.
*
* @returns {Map<string, Object>} field meta data
*/
static get definitions() {
this._initSubclass();
return this._definitions;
}
/**
* Define a new configuration field
*
* @param {Object} item - Field definition.
* @param {Array<string>=} item.access -
* Array of strings declaring where this config entry can be read
* and modified from.
* @param {string} item.type -
* string declaring the type of the config value, supports
* boolean, string, number and object.
* @param {string} item.name -
* Name of the configuration field. Should follow the
* lower_case_underscore style.
* @param {string} item.title -
* Text used to identify the config field in user interfaces.
* @param {string} item.description -
* Text used to describe the config field in user interfaces.
* @param {Array=} item.enum -
* Array of all values that are acceptible. Useful for making
* enumerated values.
* @param {boolean=} item.optional -
* True if this field can be set to null. Defaults to false.
* @param {*=} item.initial_value -
* Value this config field should take in a newly initialized
* config. This can also be an async function returning the
* value to use.
*/
static define(item) {
if (this._finalized) {
throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`);
}
if (basicType(this.groupName) !== "string") {
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`);
}
this._initSubclass();
let itemKeys = Object.keys(item);
// Check for required properties
["name", "type"].forEach(prop => {
if (!itemKeys.includes(prop)) {
throw new Error(`${prop} is required when defining an field`);
}
});
// Validate properties
let validTypes = ["boolean", "string", "number", "object"];
Object.keys(item).forEach(key => {
let value = item[key];
let valueType = basicType(value);
switch (key) {
case "type":
if (!validTypes.includes(value)) {
throw new Error(`${value} is not a valid type`);
}
break;
case "access":
if (valueType !== "array") {
throw new Error("access must be an array");
}
break;
case "enum":
if (valueType !== "array") {
throw new Error("enum must be an array");
}
break;
case "name":
if (valueType !== "string") {
throw new Error("name must be a string");
}
break;
case "title":
if (valueType !== "string") {
throw new Error("title must be a string");
}
break;
case "description":
if (valueType !== "string") {
throw new Error("description must be a string");
}
break;
case "optional":
if (valueType !== "boolean") {
throw new Error("optional must be a boolean");
}
break;
case "initial_value":
if (valueType !== item.type && valueType !== "function") {
throw new Error("initial_value must match the type or be a function");
}
break;
default:
throw new Error(`Unknown property ${key}`);
}
});
if (this._definitions.has(item.name)) {
throw new Error(`Config field ${item.name} has already been defined`);
}
let computed = {
access: this.defaultAccess,
optional: false,
fullName: `${this.groupName}.${item.name}`,
...item,
};
if (!computed.access) {
throw new Error(`access not set and no defaultAccess defined for ${this.name}`);
}
if (!computed.optional && computed.initial_value === undefined) {
throw new Error(`Non-optional field ${item.name} needs an initial_value`);
}
this._definitions.set(item.name, computed);
}
/**
* Locks this ConfigGroup class from adding more definitons.
*/
static finalize() {
if (basicType(this.groupName) !== "string") {
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`);
}
this._initSubclass();
this._finalized = true;
}
}
/**
* Config group for plugins
*
* Grouping that includes per plugin fields clusterio uses to manage
* plugins. This is currently only the enabled field. It is otherwise
* identical to the ConfigGroup class.
*
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class PluginConfigGroup extends ConfigGroup {
static _initSubclass() {
if (!this._initialized) {
super._initSubclass();
this.define({
name: "load_plugin",
title: "Load Plugin",
type: "boolean",
initial_value: true,
});
}
}
}
module.exports = {
InvalidAccess,
InvalidField,
InvalidValue,
Config,
ConfigGroup,
PluginConfigGroup,
};
+454
View File
@@ -0,0 +1,454 @@
// Core definitions for the configuration system
"use strict";
const classes = require("./classes");
/**
* Master config group for {@link module:lib/config.MasterConfig}
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class MasterGroup extends classes.ConfigGroup { }
MasterGroup.defaultAccess = ["master", "slave", "control"];
MasterGroup.groupName = "master";
MasterGroup.define({
name: "name",
title: "Name",
description: "Name of the cluster.",
type: "string",
initial_value: "Your Cluster",
});
MasterGroup.define({
name: "database_directory",
title: "Database directory",
description: "Directory where item and configuration data is stored.",
type: "string",
initial_value: "database",
});
MasterGroup.define({
name: "http_port",
title: "HTTP Port",
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
type: "number",
optional: true,
initial_value: 8080,
});
MasterGroup.define({
name: "https_port",
title: "HTTPS Port",
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
type: "number",
optional: true,
});
MasterGroup.define({
name: "bind_address",
title: "Bind Address",
description: "IP address to bind the HTTP and HTTPS ports on.",
type: "string",
optional: true,
});
MasterGroup.define({
name: "external_address",
title: "External Address",
description: "Public facing address the master server is hosted on.",
type: "string",
optional: true,
});
MasterGroup.define({
name: "tls_certificate",
title: "TLS Certificate",
description: "Path to the certificate to use for HTTPS.",
type: "string",
optional: true,
});
MasterGroup.define({
name: "tls_private_key",
title: "TLS Private Key",
description: "Path to the private key to use for HTTPS.",
type: "string",
optional: true,
});
MasterGroup.define({
access: ["master"],
name: "auth_secret",
title: "Master Authentication Secret",
description:
"Secret used to generate and verify authentication tokens. " +
"Should be a long string of random letters and numbers. " +
"Do not share this.",
type: "string",
optional: true,
});
MasterGroup.define({
name: "heartbeat_interval",
title: "Heartbeat Interval",
description: "Interval heartbeats are sent out on WebSocket connections.",
type: "number",
initial_value: 15,
});
MasterGroup.define({
name: "session_timeout",
title: "Session Timeout",
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
type: "number",
initial_value: 60,
});
MasterGroup.define({
name: "metrics_timeout",
title: "Metrics Timeout",
description: "Timeout in seconds for metrics gathering from slaves.",
type: "number",
initial_value: 8,
});
MasterGroup.define({
name: "proxy_stream_timeout",
title: "Proxy Stream Timeout",
description: "Timeout in seconds for proxy streams to start flowing.",
type: "number",
initial_value: 15,
});
MasterGroup.define({
name: "default_role_id",
title: "Default role",
description: "ID of role assigned by default to new users.",
type: "number",
optional: true,
initial_value: 1,
});
MasterGroup.finalize();
/**
* Master Config class
* @extends module:lib/config.Config
* @memberof module:lib/config
*/
class MasterConfig extends classes.Config { }
MasterConfig.registerGroup(MasterGroup);
/**
* Slave config group for {@link module:lib/config.SlaveConfig}
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class SlaveGroup extends classes.ConfigGroup {}
SlaveGroup.defaultAccess = ["master", "slave", "control"];
SlaveGroup.groupName = "slave";
SlaveGroup.define({
name: "name",
description: "Name of the slave",
type: "string",
initial_value: "New Slave",
});
SlaveGroup.define({
name: "id",
description: "ID of the slave",
type: "number",
initial_value: () => Math.random() * 2**31 | 0,
});
SlaveGroup.define({
name: "factorio_directory",
description: "Path to directory to look for factorio installs",
type: "string",
initial_value: "factorio",
});
SlaveGroup.define({
name: "instances_directory",
description: "Path to directory to store instances in.",
type: "string",
initial_value: "instances",
});
SlaveGroup.define({
name: "master_url",
description: "URL to connect to the master server at",
type: "string",
initial_value: "http://localhost:8080/",
});
SlaveGroup.define({
name: "master_token",
description: "Token to authenticate to master server with.",
type: "string",
initial_value: "enter token here",
});
SlaveGroup.define({
name: "tls_ca",
description: "Path to Certificate Authority to validate TLS connection to master against.",
type: "string",
optional: true,
});
SlaveGroup.define({
name: "public_address",
description: "Public facing address players should connect to in order to join instances on this slave",
type: "string",
initial_value: "localhost",
});
SlaveGroup.define({
name: "max_reconnect_delay",
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
type: "number",
initial_value: 60,
});
SlaveGroup.finalize();
/**
* Slave Config class
* @extends module:lib/config.Config
* @memberof module:lib/config
*/
class SlaveConfig extends classes.Config { }
SlaveConfig.registerGroup(SlaveGroup);
/**
* Instance config group for {@link module:lib/config.InstanceConfig}
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class InstanceGroup extends classes.ConfigGroup { }
InstanceGroup.defaultAccess = ["master", "slave", "control"];
InstanceGroup.groupName = "instance";
InstanceGroup.define({
name: "name",
type: "string",
initial_value: "New Instance",
});
InstanceGroup.define({
name: "id",
description: "ID of the slave",
type: "number",
initial_value: () => Math.random() * 2**31 | 0,
});
InstanceGroup.define({
name: "assigned_slave",
type: "number",
optional: true,
});
InstanceGroup.define({
name: "auto_start",
description: "Automatically start this instance when the slave hosting it is started up",
type: "boolean",
initial_value: false,
});
InstanceGroup.finalize();
/**
* Factorio config group for {@link module:lib/config.InstanceConfig}
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class FactorioGroup extends classes.ConfigGroup { }
FactorioGroup.defaultAccess = ["master", "slave", "control"];
FactorioGroup.groupName = "factorio";
FactorioGroup.define({
name: "version",
description: "Version of the game to run, use latest to run the latest installed version",
type: "string",
initial_value: "latest",
});
FactorioGroup.define({
name: "game_port",
description: "UDP port to run game on, uses a random port if null",
type: "number",
optional: true,
});
FactorioGroup.define({
name: "rcon_port",
description: "TCP port to run RCON on, uses a random port if null",
type: "number",
optional: true,
});
FactorioGroup.define({
name: "rcon_password",
description: "Password for RCON, randomly generated if null",
type: "string",
optional: true,
});
FactorioGroup.define({
name: "enable_save_patching",
description: "Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.",
type: "boolean",
initial_value: true,
});
FactorioGroup.define({
name: "enable_whitelist",
description: "Turn on whitelist for joining the server.",
type: "boolean",
initial_value: false,
});
FactorioGroup.define({
name: "settings",
description: "Settings overridden in server-settings.json",
type: "object",
initial_value: {}, // See create instance handler in master.
});
FactorioGroup.define({
name: "verbose_logging",
description: "Enable verbose logging on the Factorio server",
type: "boolean",
initial_value: false,
});
FactorioGroup.define({
name: "strip_paths",
description: "Strip down instance paths in the log",
type: "boolean",
initial_value: true,
});
FactorioGroup.define({
name: "sync_adminlist",
description: "Synchronize adminlist with master server",
type: "boolean",
initial_value: true,
});
FactorioGroup.define({
name: "sync_whitelist",
description: "Synchronize whitelist with master server",
type: "boolean",
initial_value: true,
});
FactorioGroup.define({
name: "sync_banlist",
description: "Synchronize banlist with master server",
type: "boolean",
initial_value: true,
});
FactorioGroup.define({
name: "max_concurrent_commands",
description: "Maximum number of RCON commands trasmitted in parallel",
type: "number",
initial_value: 5,
});
FactorioGroup.finalize();
/**
* Instance config class
* @extends module:lib/config.Config
* @memberof module:lib/config
*/
class InstanceConfig extends classes.Config { }
InstanceConfig.registerGroup(InstanceGroup);
InstanceConfig.registerGroup(FactorioGroup);
/**
* Control config group for {@link module:lib/config.ControlConfig}
* @extends module:lib/config.ConfigGroup
* @memberof module:lib/config
*/
class ControlGroup extends classes.ConfigGroup {}
ControlGroup.defaultAccess = ["control"];
ControlGroup.groupName = "control";
ControlGroup.define({
name: "master_url",
description: "URL to connect to the master server at",
type: "string",
optional: true,
});
ControlGroup.define({
name: "master_token",
description: "Token to authenticate to master server with.",
type: "string",
optional: true,
});
ControlGroup.define({
name: "tls_ca",
description: "Path to Certificate Authority to validate TLS connection to master against.",
type: "string",
optional: true,
});
ControlGroup.define({
name: "max_reconnect_delay",
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
type: "number",
initial_value: 60,
});
ControlGroup.finalize();
/**
* Control config class
* @extends module:lib/config.Config
* @memberof module:lib/config
*/
class ControlConfig extends classes.Config { }
ControlConfig.registerGroup(ControlGroup);
function validateGroup(pluginInfo, groupName) {
if (!pluginInfo[groupName] instanceof classes.PluginConfigGroup) {
throw new Error(
`Expected ${groupName} for ${pluginInfo.name} to be an instance of PluginConfigGroup`
);
}
if (pluginInfo[groupName].groupName !== pluginInfo.name) {
throw new Error(
`Expected ${groupName} for ${pluginInfo.name} to be named after the plugin`
);
}
}
/**
* Registers the config groups for the provided plugin infos
*
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
* @memberof module:lib/config
*/
function registerPluginConfigGroups(pluginInfos) {
for (let pluginInfo of pluginInfos) {
if (pluginInfo.MasterConfigGroup) {
validateGroup(pluginInfo, "MasterConfigGroup");
MasterConfig.registerGroup(pluginInfo.MasterConfigGroup);
} else {
class MasterConfigGroup extends classes.PluginConfigGroup { }
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
MasterConfigGroup.groupName = pluginInfo.name;
MasterConfigGroup.finalize();
MasterConfig.registerGroup(MasterConfigGroup);
}
if (pluginInfo.instanceEntrypoint) {
if (pluginInfo.InstanceConfigGroup) {
validateGroup(pluginInfo, "InstanceConfigGroup");
InstanceConfig.registerGroup(pluginInfo.InstanceConfigGroup);
} else {
class InstanceConfigGroup extends classes.PluginConfigGroup { }
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
InstanceConfigGroup.groupName = pluginInfo.name;
InstanceConfigGroup.finalize();
InstanceConfig.registerGroup(InstanceConfigGroup);
}
}
}
}
/**
* Lock configs from adding more groups and make them usable
*
* @memberof module:lib/config
*/
function finalizeConfigs() {
MasterConfig.finalize();
SlaveConfig.finalize();
InstanceConfig.finalize();
ControlConfig.finalize();
}
module.exports = {
MasterGroup,
SlaveGroup,
InstanceGroup,
FactorioGroup,
ControlGroup,
MasterConfig,
SlaveConfig,
InstanceConfig,
ControlConfig,
registerPluginConfigGroups,
finalizeConfigs,
};
+13
View File
@@ -0,0 +1,13 @@
/**
* Configuration System
* @author Hornwitser
* @module lib/config
*/
"use strict";
const classes = require("./classes");
const definitions = require("./definitions");
module.exports = {
...classes,
...definitions,
};
+288
View File
@@ -0,0 +1,288 @@
/**
* Implementation of master databases
*
* @module lib/database
* @author Hornwitser
*/
"use strict";
const fs = require("fs-extra");
const libFileOps = require("./file_ops");
const { basicType } = require("./helpers");
/**
* Converts a Map with only string keys to a JavaScript object.
*
* @param {Map} map - the Map to convert to an object.
* @return {object} - Object with the mapping's key-values as properties.
* @throws {Error} if there are non-string keys in map.
*/
function mapToObject(map) {
let obj = {};
for (let [key, value] of map) {
if (typeof key !== "string") {
throw new Error(
`Expected all keys to be string but got ${typeof key}`
);
}
obj[key] = value;
}
return obj;
}
/**
* Load JSON object file as a Map
*
* Loads the JSON file specified by path containing an object with
* properties as key value pairs and converts it into a JavaScript Map.
* Returns an empty Map if the file does not exist.
*
* @param {string} filePath - The path to the JSON file to load.
* @throws {Error} If JSON file did not contain an object.
* @throws {Error} If an error occured reading the file.
*/
async function loadJsonAsMap(filePath) {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
if (basicType(parsed) !== "object") {
throw new Error(`Expected object but got ${basicType(parsed)}`);
}
return new Map(Object.entries(parsed));
} catch (err) {
if (err.code === "ENOENT") {
// File does not exist, return empty map
return new Map();
}
throw err;
}
}
/**
* Save a Map as a JSON object file
*
* Save the content of a key-value Map with only string keys as a JSON file.
* If the directory the file is to be saved into doesn't exist it will be
* created.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map - Map with only string keys to save.
* @throws {Error} if there are non-string keys in map.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJson(filePath, map) {
let obj = mapToObject(map);
await libFileOps.safeOutputFile(filePath, JSON.stringify(obj, null, 4));
}
/**
* Load JSON array file as a Map
*
* Loads the JSON file specified by path containing an array of objects that
* each have an id property into a Map from the id to the object.
*
* @param {string} filePath - The path to the JSON file to load.
* @throws {Error} If JSON file did not contain an array.
* @throws {Error} If there are objects in the array without an id property.
* @throws {Error} If an error occured reading the file.
*/
async function loadJsonArrayAsMap(filePath) {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
if (basicType(parsed) !== "array") {
throw new Error(`Expected array but got ${basicType(parsed)}`);
}
let map = new Map();
for (let element of parsed) {
if (basicType(element) !== "object") {
throw new Error("Expected all elements to be objects");
}
if (element.id === undefined) {
throw new Error("Expected all elements to have an id property");
}
map.set(element.id, element);
}
return map;
} catch (err) {
if (err.code === "ENOENT") {
// File does not exist, return empty map
return new Map();
}
throw err;
}
}
/**
* Save a Map as a JSON array file
*
* Save the values of a Map with objects containing an id property that is
* equal to the key the objects are stored at into a JSON file.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map -
* Map with objects containing an id property equal to the key they are
* stored under.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJsonArray(filePath, map) {
await libFileOps.safeOutputFile(filePath, JSON.stringify([...map.values()], null, 4));
}
function checkName(name) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
}
function checkCount(count) {
if (typeof count !== "number" || isNaN(count)) {
throw new Error("count must be a number");
}
}
/**
* Item database
*
* Stores counts of items for the master server. Items that have not been
* stored in the database are treated as having a stored count of zero. When
* serialized the database discards entries with a zero counts, and when
* deserialized the content is verified.
*/
class ItemDatabase {
/**
* Create a new item database
*
* @param {object|undefined} serialized - An object from a previous call to
* {@link module:lib/database~ItemDatabase#serialize} to restore the
* database from. An empty database will be created if this parameter
* is left undefined.
*/
constructor(serialized) {
this._items = new Map();
// Verify the content of the serialized database
if (serialized !== undefined) {
for (let [name, count] of Object.entries(serialized)) {
checkName(name);
checkCount(count);
this._items.set(name, count);
}
}
}
/**
* Serialize item database
*
* Serialize the item database into a plain JavaScript object that can be
* turned inta a string with JSON.stringify().
*
* @returns {object} Serialized representation of the database
*/
serialize() {
let obj = {};
for (let [name, count] of this._items) {
if (count !== 0) {
obj[name] = count;
}
}
return obj;
}
/**
* Approximate size of the items database.
*
* Does not account for entries being zero in the item database.
*/
get size() {
return this._items.size;
}
/**
* Get the count of an item stored in the database
*
* Returns the count of a given item that has been stored in to the
* database. If the item has not been previously stored into the database
* then 0 is returned.
*
* @param {string} name - The name of the item to get the count of.
* @returns {number} The count of the item stored.
*/
getItemCount(name) {
checkName(name);
if (!this._items.has(name)) {
return 0;
}
return this._items.get(name);
}
/**
* Adds count of an item to the database
*
* Add count copies of the item specified by name to the item database.
*
* @param {string} name - The name of the item to add.
* @param {number} count - The count of item to remove.
*/
addItem(name, count) {
checkName(name);
checkCount(count);
if (this._items.has(name)) {
count += this._items.get(name);
}
this._items.set(name, count);
};
/**
* Removes count of an item from the database.
*
* Remove count copies of the item specified by name from the item database
* If count is greater than the currently stored count the stored item
* count will become negative.
*
* @param {string} name - The name of the item to remove.
* @param {number} count - The count of items to remove.
*/
removeItem(name, count) {
checkName(name);
checkCount(count);
count = -count;
if (this._items.has(name)) {
count += this._items.get(name);
}
this._items.set(name, count);
};
}
module.exports = {
mapToObject,
loadJsonAsMap,
saveMapAsJson,
loadJsonArrayAsMap,
saveMapAsJsonArray,
ItemDatabase,
};
+86
View File
@@ -0,0 +1,86 @@
/**
* Errors thrown in Clusterio
* @module lib/errors
*/
"use strict";
/**
* Thrown by commands when they fail
* @static
*/
class CommandError extends Error { }
/**
* Thrown from requests sent when an error occured handling it
* @static
*/
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
*/
class InvalidMessage extends Error {
constructor(msg, errors) {
super(msg);
this.errors = errors || null;
}
}
/**
* Thrown from requests when the session it was sent on was lost
* @static
*/
class SessionLost extends Error { }
/**
* Thrown when WebSocket authentication failed
* @static
*/
class AuthenticationFailed extends Error { }
/**
* Errror class for known errors occuring during startup
* @static
*/
class StartupError extends Error { }
/**
* Errors outside of our control
* @static
*/
class EnvironmentError extends Error { }
/**
* Errors caused by plugins
* @static
*/
class PluginError extends Error {
constructor(pluginName, original) {
super(`PluginError: ${original.message}`);
this.pluginName = pluginName;
this.original = original;
}
}
module.exports = {
CommandError,
RequestError,
PermissionError,
InvalidMessage,
SessionLost,
AuthenticationFailed,
StartupError,
EnvironmentError,
PluginError,
};
+393
View File
@@ -0,0 +1,393 @@
"use strict";
const zlib = require("zlib");
class MapReaderState {
constructor(buf) {
this.pos = 0;
this.buf = buf;
this.last_position = { x: 0, y: 0 };
}
}
function readUInt8(state) {
let value = state.buf.readUInt8(state.pos);
state.pos += 1;
return value;
}
function readBool(state) {
let value = readUInt8(state) !== 0;
return value;
}
function readInt16(state) {
let value = state.buf.readInt16LE(state.pos);
state.pos += 2;
return value;
}
function readUInt16(state) {
let value = state.buf.readUInt16LE(state.pos);
state.pos += 2;
return value;
}
function readInt32(state) {
let value = state.buf.readInt32LE(state.pos);
state.pos += 4;
return value;
}
function readUInt32(state) {
let value = state.buf.readUInt32LE(state.pos);
state.pos += 4;
return value;
}
function readUInt32so(state) {
let value = readUInt8(state);
if (value === 0xff) {
return readUInt32(state);
}
return value;
}
function readFloat(state) {
let value = state.buf.readFloatLE(state.pos);
state.pos += 4;
return value;
}
function readDouble(state) {
let value = state.buf.readDoubleLE(state.pos);
state.pos += 8;
return value;
}
function readString(state) {
let size = readUInt32so(state);
let data = state.buf.slice(state.pos, state.pos + size).toString("utf-8");
state.pos += size;
return data;
}
function readOptional(state, readValue) {
let load = readUInt8(state) !== 0;
if (!load) {
return null;
}
return readValue(state);
}
function readArray(state, readItem) {
let size = readUInt32so(state);
let array = [];
for (let i = 0; i < size; i++) {
let item = readItem(state);
array.push(item);
}
return array;
}
function readDict(state, readKey, readValue) {
let size = readUInt32so(state);
let mapping = new Map();
for (let i = 0; i < size; i++) {
let key = readKey(state);
let value = readValue(state);
mapping.set(key, value);
}
return mapping;
}
function readVersion(state) {
let major = readUInt16(state);
let minor = readUInt16(state);
let patch = readUInt16(state);
let developer = readUInt16(state);
return [major, minor, patch, developer];
}
function readFrequencySizeRichness(state) {
return {
frequency: readFloat(state),
size: readFloat(state),
richness: readFloat(state),
};
}
function readAutoplaceSetting(state) {
return {
treat_missing_as_default: readBool(state),
settings: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)),
};
}
function readMapPosition(state) {
let x, y;
let x_diff = readInt16(state) / 256;
if (x_diff === 0x7fff / 256) {
x = readInt32(state) / 256;
y = readInt32(state) / 256;
} else {
let y_diff = readInt16(state) / 256;
x = state.last_position.x + x_diff;
y = state.last_position.y + y_diff;
}
state.last_position.x = x;
state.last_position.x = y;
return { x, y };
}
function readBoundingBox(state) {
return {
left_top: readMapPosition(state),
right_bottom: readMapPosition(state),
orientation: {
x: readInt16(state),
y: readInt16(state),
},
};
}
function readCliffSettings(state) {
return {
name: readString(state),
elevation_0: readFloat(state),
elevation_interval: readFloat(state),
richness: readFloat(state),
};
}
function readMapGenSettings(state) {
return {
terrain_segmentation: readFloat(state),
water: readFloat(state),
autoplace_controls: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)),
autoplace_settings: Object.fromEntries(readDict(state, readString, readAutoplaceSetting)),
default_enable_all_autoplace_controls: readBool(state),
seed: readUInt32(state),
width: readUInt32(state),
height: readUInt32(state),
area_to_generate_at_start: readBoundingBox(state),
starting_area: readFloat(state),
peaceful_mode: readBool(state),
starting_points: readArray(state, readMapPosition),
property_expression_names: Object.fromEntries(readDict(state, readString, readString)),
cliff_settings: readCliffSettings(state),
};
}
function readPollution(state) {
return {
enabled: readOptional(state, readBool),
diffusion_ratio: readOptional(state, readDouble),
min_to_diffuse: readOptional(state, readDouble),
ageing: readOptional(state, readDouble),
expected_max_per_chunk: readOptional(state, readDouble),
min_to_show_per_chunk: readOptional(state, readDouble),
min_pollution_to_damage_trees: readOptional(state, readDouble),
pollution_with_max_forest_damage: readOptional(state, readDouble),
pollution_per_tree_damage: readOptional(state, readDouble),
pollution_restored_per_tree_damage: readOptional(state, readDouble),
max_pollution_to_restore_trees: readOptional(state, readDouble),
enemy_attack_pollution_consumption_modifier: readOptional(state, readDouble),
};
}
function readSteeringValue(state) {
return {
radius: readOptional(state, readDouble),
separation_factor: readOptional(state, readDouble),
separation_force: readOptional(state, readDouble),
force_unit_fuzzy_goto_behavior: readOptional(state, readBool),
};
}
function readSteering(state) {
return {
default: readSteeringValue(state),
moving: readSteeringValue(state),
};
}
function readEnemyEvolution(state) {
return {
enabled: readOptional(state, readBool),
time_factor: readOptional(state, readDouble),
destroy_factor: readOptional(state, readDouble),
pollution_factor: readOptional(state, readDouble),
};
}
function readEnemyExpansion(state) {
return {
enabled: readOptional(state, readBool),
max_expansion_distance: readOptional(state, readUInt32),
friendly_base_influence_radius: readOptional(state, readUInt32),
enemy_building_influence_radius: readOptional(state, readUInt32),
building_coefficient: readOptional(state, readDouble),
other_base_coefficient: readOptional(state, readDouble),
neighbouring_chunk_coefficient: readOptional(state, readDouble),
neighbouring_base_chunk_coefficient: readOptional(state, readDouble),
max_colliding_tiles_coefficient: readOptional(state, readDouble),
settler_group_min_size: readOptional(state, readUInt32),
settler_group_max_size: readOptional(state, readUInt32),
min_expansion_cooldown: readOptional(state, readUInt32),
max_expansion_cooldown: readOptional(state, readUInt32),
};
}
function readUnitGroup(state) {
return {
min_group_gathering_time: readOptional(state, readUInt32),
max_group_gathering_time: readOptional(state, readUInt32),
max_wait_time_for_late_members: readOptional(state, readUInt32),
max_group_radius: readOptional(state, readDouble),
min_group_radius: readOptional(state, readDouble),
max_member_speedup_when_behind: readOptional(state, readDouble),
max_member_slowdown_when_ahead: readOptional(state, readDouble),
max_group_slowdown_factor: readOptional(state, readDouble),
max_group_member_fallback_factor: readOptional(state, readDouble),
member_disown_distance: readOptional(state, readDouble),
tick_tolerance_when_member_arrives: readOptional(state, readUInt32),
max_gathering_unit_groups: readOptional(state, readUInt32),
max_unit_group_size: readOptional(state, readUInt32),
};
}
function readPathFinder(state) {
return {
fwd2bwd_ratio: readOptional(state, readInt32),
goal_pressure_ratio: readOptional(state, readDouble),
use_path_cache: readOptional(state, readBool),
max_steps_worked_per_tick: readOptional(state, readDouble),
max_work_done_per_tick: readOptional(state, readUInt32),
short_cache_size: readOptional(state, readUInt32),
long_cache_size: readOptional(state, readUInt32),
short_cache_min_cacheable_distance: readOptional(state, readDouble),
short_cache_min_algo_steps_to_cache: readOptional(state, readUInt32),
long_cache_min_cacheable_distance: readOptional(state, readDouble),
cache_max_connect_to_cache_steps_multiplier: readOptional(state, readUInt32),
cache_accept_path_start_distance_ratio: readOptional(state, readDouble),
cache_accept_path_end_distance_ratio: readOptional(state, readDouble),
negative_cache_accept_path_start_distance_ratio: readOptional(state, readDouble),
negative_cache_accept_path_end_distance_ratio: readOptional(state, readDouble),
cache_path_start_distance_rating_multiplier: readOptional(state, readDouble),
cache_path_end_distance_rating_multiplier: readOptional(state, readDouble),
stale_enemy_with_same_destination_collision_penalty: readOptional(state, readDouble),
ignore_moving_enemy_collision_distance: readOptional(state, readDouble),
enemy_with_different_destination_collision_penalty: readOptional(state, readDouble),
general_entity_collision_penalty: readOptional(state, readDouble),
general_entity_subsequent_collision_penalty: readOptional(state, readDouble),
extended_collision_penalty: readOptional(state, readDouble),
max_clients_to_accept_any_new_request: readOptional(state, readUInt32),
max_clients_to_accept_short_new_request: readOptional(state, readUInt32),
direct_distance_to_consider_short_request: readOptional(state, readUInt32),
short_request_max_steps: readOptional(state, readUInt32),
short_request_ratio: readOptional(state, readDouble),
min_steps_to_check_path_find_termination: readOptional(state, readUInt32),
start_to_goal_cost_multiplier_to_terminate_path_find: readOptional(state, readDouble),
overload_levels: readOptional(state, (p) => readArray(p, readUInt32)),
overload_multipliers: readOptional(state, (p) => readArray(p, readDouble)),
negative_path_cache_delay_interval: readOptional(state, readUInt32),
};
}
function readDifficultySettings(state) {
return {
recipe_difficulty: readUInt8(state),
technology_difficulty: readUInt8(state),
technology_price_multiplier: readDouble(state),
research_queue_setting: ["always", "after-victory", "never"][readUInt8(state)],
};
}
function readMapSettings(state) {
return {
pollution: readPollution(state),
steering: readSteering(state),
enemy_evolution: readEnemyEvolution(state),
enemy_expansion: readEnemyExpansion(state),
unit_group: readUnitGroup(state),
path_finder: readPathFinder(state),
max_failed_behavior_count: readUInt32(state),
difficulty_settings: readDifficultySettings(state),
};
}
/**
* @typedef {object} module:lib/factorio.MapExchangeResult
* @property {array} version -
* Version of Factorio the string was created with.
* @property {object} map_gen_settings -
* Decoded map generator settings in the format the --map-gen-settings
* command line option to Factorio expect.
* @property {object} map_settings -
* Decoded map settings in the format the --map-settings command line option
* to Factorio expects.
* @property {number} checksum - checksum for the exchange string.
*/
/**
* Parse a Map Exchange String
*
* Reads and decodes the data in the given map exchange string and returns
* data structures that can be fed into the Factorio server when creating a
* save to set the map gen settings and the map settings for the save.
*
* @param {string} exchangeString - Max Exchange String to parse.
* @returns {module:lib/factorio.MapExchangeResult} Parsed result.
* @memberof module:lib/factorio
*/
function readMapExchangeString(exchangeString) {
exchangeString = exchangeString.replace(/[ \t\n\r]+/g, "");
if (!/>>>[0-9a-zA-Z\/+]+={0,3}<<</.test(exchangeString)) {
throw new Error("Not a map exchange string");
}
let buf = Buffer.from(exchangeString.slice(3, -3), "base64");
try {
// eslint-disable-next-line node/no-sync
buf = zlib.inflateSync(buf);
} catch (err) {
if (err.code.startsWith("Z_")) {
throw new Error("Malformed map exchange string: zlib inflate failed");
}
}
let state = new MapReaderState(buf);
let data;
try {
data = {
version: readVersion(state),
unknown: readUInt8(state),
map_gen_settings: readMapGenSettings(state),
map_settings: readMapSettings(state),
checksum: readUInt32(state),
};
} catch (err) {
if (err.code === "ERR_OUT_OF_RANGE") {
throw new Error("Malformed map exchange string: reached end before finishing parsing");
}
throw err;
}
if (state.pos !== buf.length) {
throw new Error("Malformed map exchange string: data after end");
}
return data;
}
module.exports = {
readMapExchangeString,
};
+418
View File
@@ -0,0 +1,418 @@
// Export of item icons and locale
"use strict";
const fs = require("fs-extra");
const path = require("path");
const Jimp = require("jimp");
const JSZip = require("jszip");
const libIni = require("../ini");
const libBuildMod = require("../build_mod");
/**
* Generate the export mod needed for exportData
*
* Packs the lua export mod located in lua/export into the mods folder for
* the server given, with dependencies generated for all the mods present in
* the server's mods folder.
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to generate the export mod for.
* @memberof module:lib/factorio
*/
async function generateExportMod(server) {
let dependencies = [];
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/;
for (let entry of await fs.readdir(server.writePath("mods"))) {
let match = splitter.exec(entry);
if (match && match[1] !== "export") {
dependencies.push(`? ${match[1]}`);
}
}
await libBuildMod.build({
clean: false,
build: true,
pack: true,
// XXX fix path when moving lib/factorio to slave
sourceDir: path.join(__dirname, "..", "..", "slave", "lua", "export"),
outputDir: server.writePath("mods"),
bumpPatch: false,
factorioVersion: server.version.replace(/\.\d+$/, ""),
dependencies,
});
}
let zipCache = new Map();
async function loadZip(server, modVersions, mod) {
let modVersion = modVersions.get(mod);
if (!modVersion) {
throw new Error(`Got path for unknown mod ${mod}`);
}
let zipPath = server.writePath("mods", `${mod}_${modVersion}.zip`);
let zip = zipCache.get(zipPath);
if (!zip) {
zip = await JSZip.loadAsync(await fs.readFile(zipPath));
zipCache.set(zipPath, zip);
}
return zip.folder(`${mod}_${modVersion}`);
}
/**
* Load the given Factorio file path into a Buffer
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to load the file from.
* @param {Map<string, string>} modVersions - Mapping of mod to version used.
* @param {string} modPath - Factorio style path to the file to load.
* @returns {?Buffer} The content of the file or null if not found.
* @memberof module:lib/factorio
* @private
* @inner
*/
async function loadFile(server, modVersions, modPath) {
let match = /^__([^\/]+)__\/(.*)$/.exec(modPath);
if (!match) {
throw new Error(`Bad mod path ${match}`);
}
let [, mod, filePath] = match;
if (["core", "base"].includes(mod)) {
try {
return await fs.readFile(server.dataPath(mod, filePath));
} catch (err) {
if (err.code === "ENOENT") {
return null;
}
throw err;
}
}
let zip;
try {
zip = await loadZip(server, modVersions, mod);
} catch (err) {
if (err.code === "ENOENT") {
return null;
}
throw err;
}
let file = zip.file(filePath);
if (!file) {
return null;
}
return await file.async("nodebuffer");
}
async function loadIcon(server, modVersions, iconPath, iconSize, iconMipmaps, iconCache) {
let icon = iconCache.get(iconPath);
if (icon === undefined) {
let fileContent = await loadFile(server, modVersions, iconPath);
if (fileContent) {
icon = await Jimp.read(fileContent);
icon.crop(0, 0, iconSize, iconSize);
iconCache.set(iconPath, icon);
} else {
icon = null;
server._logger.warn(`${iconPath} not found`);
}
iconCache.set(iconPath, icon);
}
return icon;
}
async function loadSimpleIcon(server, modVersions, item, size, iconCache) {
let icon = await loadIcon(server, modVersions, item.icon, item.icon_size, 0, iconCache);
if (icon) {
let iconScale = size / item.icon_size;
if (iconScale !== 1) {
icon = icon.clone();
icon.scale(iconScale);
}
}
return icon;
}
async function loadLayeredIcon(server, modVersions, item, size, iconCache) {
let baseLayerSize = item.icons[0].icon_size || item.icon_size;
let icon = await Jimp.create(size, size);
// The scaling factor of the base layer
let baseLayerScale = item.icons[0].scale || 32 / baseLayerSize;
// The size in pixels of one unit
let baseUnit = size / (baseLayerSize * baseLayerScale);
for (let layer of item.icons) {
let layerSize = layer.icon_size || item.icon_size;
let iconLayer = await loadIcon(server, modVersions, layer.icon, layerSize, layer.icon_mipmaps || 0, iconCache);
if (!iconLayer) {
continue;
}
iconLayer = iconLayer.clone();
let tint;
if (layer.tint) {
let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1;
tint = {
r: (layer.tint.r || 0) / divisor,
g: (layer.tint.g || 0) / divisor,
b: (layer.tint.b || 0) / divisor,
a: layer.tint.a || 1,
};
} else {
tint = { r: 1, b: 1, g: 1, a: 1 };
}
let layerScale = layer.scale || 32 / layerSize;
let [xs, ys] = layer.shift || [0, 0];
let realScale = layerScale * baseUnit;
let sizeShift = (size - layerSize * realScale) / 2;
xs = xs * baseUnit + sizeShift;
ys = ys * baseUnit + sizeShift;
if (realScale !== 1) {
iconLayer.scale(realScale);
}
// eslint-disable-next-line no-loop-func
iconLayer.scan(0, 0, iconLayer.bitmap.width, iconLayer.bitmap.height, (x, y, sidx) => {
x += xs;
y += ys;
if (x < 0 || x >= size || y < 0 || y >= size) {
return;
}
let sdata = iconLayer.bitmap.data;
let ddata = icon.bitmap.data;
let didx = icon.getPixelIndex(x, y);
let sa = sdata[sidx + 3] / 255;
let da = ddata[didx + 3] / 255;
let db = da * (1 - sa * tint.a);
let cb = sa + da * (1 - sa);
ddata[didx + 0] = Math.min(255, (sdata[sidx + 0] * sa * tint.r + ddata[didx + 0] * db) / cb);
ddata[didx + 1] = Math.min(255, (sdata[sidx + 1] * sa * tint.g + ddata[didx + 1] * db) / cb);
ddata[didx + 2] = Math.min(255, (sdata[sidx + 2] * sa * tint.b + ddata[didx + 2] * db) / cb);
ddata[didx + 3] = Math.min(255, sdata[sidx + 3] + ddata[didx + 3] * (1 - sa));
});
}
return icon;
}
/**
* Export item icons and data
*
* Assembles and packs the icons for the item prototypes given into a single
* spritesheet and json file with meta data.
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to generate the export mod for.
* @param {Map<string, string>} modVersions -
* Mapping of mod name to versions to get icons from.
* @param {Array<Object>} items - Array of item prototypes.
* @return {{itemSheet: Jimp, itemData: Map<string,Object>}}
* Item spritesheet and metadata.
* @memberof module:lib/factorio
* @private
* @inner
*/
async function exportItems(server, modVersions, items) {
// Size to render icons at
let size = 32;
// Width of spritesheet
let width = 1024;
let rows = Math.ceil(items.length / (width / size));
let iconSheet = await Jimp.create(width, rows * size);
let itemData = new Map();
let pos = 0;
let iconCache = new Map();
let simpleIcons = new Map();
for (let item of items) {
let icon;
let iconPos;
if (item.icons) {
icon = await loadLayeredIcon(server, modVersions, item, size, iconCache);
iconPos = pos;
} else {
iconPos = simpleIcons.get(item.icon);
if (iconPos === undefined) {
icon = await loadSimpleIcon(server, modVersions, item, size, iconCache);
if (icon) {
iconPos = pos;
simpleIcons.set(item.icon, pos);
}
}
}
if (iconPos !== undefined) {
itemData.set(item.name, {
x: iconPos * size % width,
y: Math.floor(iconPos / (width / size)) * size,
size,
localised_name: item.localised_name,
localised_description: item.localised_description,
});
}
if (icon) {
iconSheet.composite(icon, pos * size % width, Math.floor(pos / (width / size)) * size);
pos += 1;
}
}
iconSheet.crop(0, 0, width, Math.ceil(pos / (width / size)) * size);
return { iconSheet, itemData };
}
/**
* Exports the locale files for the base game and the given mods
*
* Parses and merges all the locales for the all the mods given through
* `modVersions` and `modOrder`.
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to export the locale from.
* @param {Map<string, string>} modVersions -
* Mapping of mod name to version to export locale from.
* @param {Array<string>} modOrder - Load order of the mods.
* @param {string} languageCode - Language to export locale for.
* @returns {Map<string, string>} merged locale information
* @memberof module:lib/factorio
* @private
* @inner
*/
async function exportLocale(server, modVersions, modOrder, languageCode) {
let mergedLocales = new Map();
function mergeLocale(locale) {
for (let [category, entries] of Object.entries(locale)) {
if (typeof entries === "string") {
mergedLocales.set(category, entries);
} else {
for (let [key, value] of Object.entries(entries)) {
mergedLocales.set(`${category}.${key}`, value);
}
}
}
}
let baseLocaleFilePath = server.dataPath("base", "locale", languageCode, "base.cfg");
mergeLocale(libIni.parse(await fs.readFile(baseLocaleFilePath, "utf8")));
for (let mod of modOrder) {
if (["core", "base", "export"].includes(mod)) {
continue;
}
let zip;
try {
zip = await loadZip(server, modVersions, mod);
} catch (err) {
if (err.code === "ENOENT") {
continue;
}
throw err;
}
for (let file of zip.file(new RegExp(`locale\\/${languageCode}\\/.*\\.cfg`))) {
let content = await file.async("nodebuffer");
mergeLocale(libIni.parse(content.toString("utf8")));
}
}
return mergedLocales;
}
/**
* Export the locale and item icons for the given factorio server
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to export the data from.
* @returns {JSZip} zip file with exported data.
* @memberof module:lib/factorio
*/
async function exportData(server) {
await generateExportMod(server);
let items = [];
let modVersions = new Map();
let modOrder = [];
server.on("ipc-item_export", data => items.push(data));
server.on("ipc-mod_list", data => { modVersions = new Map(Object.entries(data)); });
server.on("output", parsed => {
if (parsed.format === "seconds" && parsed.type === "generic") {
let match = /^Checksum of (.*): \d+$/.exec(parsed.message);
if (match) {
modOrder.push(match[1]);
}
}
});
try {
await server.startScenario("base/freeplay");
await server.kill();
} finally {
await fs.unlink(server.writePath("mods", "export_0.0.0.zip"));
}
if (!items.length) {
throw new Error("No items got exported");
}
// Some mod authors put leading zeros into the versions of their zip files.
let splitter = /^(.*)_(\d+)\.(\d+)\.(\d+)\.zip?$/;
for (let entry of await fs.readdir(server.writePath("mods"))) {
let match = splitter.exec(entry);
if (!match) {
continue;
}
let modVersion = `${match[2]}.${match[3]}.${match[4]}`;
let normalizedVersion =
`${Number.parseInt(match[2], 10)}.${Number.parseInt(match[3], 10)}.${Number.parseInt(match[4], 10)}`
;
if (modVersion === normalizedVersion) {
continue;
}
if (modVersions.get(match[1]) === normalizedVersion) {
modVersions.set(match[1], modVersion);
}
}
let { iconSheet, itemData } = await exportItems(server, modVersions, items);
let locale = await exportLocale(server, modVersions, modOrder, "en");
// Free up the memory used by zip files loaded during the export.
zipCache.clear();
let zip = new JSZip();
zip.file("export/item-spritesheet.png", await iconSheet.getBufferAsync(Jimp.MIME_PNG));
zip.file("export/item-metadata.json", JSON.stringify([...itemData.entries()]));
zip.file("export/locale.json", JSON.stringify([...locale.entries()]));
return zip;
}
module.exports = {
exportData,
// For testing only
_exportLocale: exportLocale,
};
+17
View File
@@ -0,0 +1,17 @@
/**
* Library for interfacing with Factorio Servers and saves.
* @module lib/factorio
* @author Hornwitser
*/
"use strict";
const exchange_string = require("./exchange_string");
const export_ = require("./export");
const patch = require("./patch");
const server = require("./server");
module.exports = {
...exchange_string,
...export_,
...patch,
...server,
};
+301
View File
@@ -0,0 +1,301 @@
// Library for patching Factorio saves with scenario code.
"use strict";
const events = require("events");
const fs = require("fs-extra");
const JSZip = require("jszip");
const path = require("path");
const semver = require("semver");
const libHash = require("../hash");
const knownScenarios = {
// First seen in 0.17.63
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": {
"name": "freeplay",
"modules": [
"freeplay",
"silo-script",
],
},
};
/**
* Returns the root folder in the zip file
*
* Returns the name of the folder that all files in the zip file is
* contained in. Throws an error if there are multiple such folders.
*
* @param {JSZip} zip - Zip to search through.
* @returns {string} name of the root folder.
* @memberof module:lib/factorio
* @private
* @inner
*/
function findRoot(zip) {
let root = null;
zip.forEach((relativePath, file) => {
let index = relativePath.indexOf("/");
if (index === -1) {
throw new Error(`Zip contains file '${relativePath}' in root dir`);
}
let pathRoot = relativePath.slice(0, index);
if (root === null) {
root = pathRoot;
} else if (root !== pathRoot) {
throw new Error("Zip contains multiple root folders");
}
});
if (root === null) {
throw new Error("Empty zip file");
}
return root;
}
/**
* Generates control.lua code for loading the Clusterio modules
*
* @param {Object} patchInfo - The patch info files's json content
* @returns {string} Generated control.lua code.
* @memberof module:lib/factorio
* @private
* @inner
*/
function generateLoader(patchInfo) {
let lines = [
"-- Auto generated scenario module loader created by Clusterio",
"-- Modifications to this file will be lost when loaded in Clusterio",
`clusterio_patch_number = ${patchInfo.patch_number}`,
"",
'local event_handler = require("event_handler")',
"",
"-- Scenario modules",
];
for (let moduleName of patchInfo["scenario"]["modules"]) {
lines.push(`event_handler.add_lib(require("${moduleName}"))`);
}
lines.push(...[
"",
"-- Clusterio modules",
]);
for (let module of patchInfo["modules"]) {
for (let file of module["files"]) {
let requirePath = `modules/${module.name}/${file["path"].slice(0, -4)}`;
if (file["load"]) {
lines.push(`event_handler.add_lib(require("${requirePath}"))`);
}
if (file["require"]) {
lines.push(`require("${requirePath}")`);
}
}
}
// End last line with a newline
lines.push("");
return lines.join("\n");
}
/**
* Reorders modules to satisfy their dependencies
*
* Looks through and reorders the array of module definitions in order to
* satisfy the property that dependencies are earlier in the array than
* their dependents. Throws an error if this is not possible.
*
* @param {Array<Object>} modules - Array of modules to reorder
* @memberof module:lib/factorio
* @private
* @inner
*/
function reorderDependencies(modules) {
let index = 0;
let present = new Map();
let hold = new Map();
reorder: while (index < modules.length) {
let module = modules[index];
if (semver.valid(module.version) === null) {
throw new Error(`Invalid version '${module.version}' for module ${module.name}`);
}
for (let [dependency, requirement] of Object.entries(module.dependencies)) {
if (semver.validRange(requirement) === null) {
throw new Error(
`Invalid version range '${requirement}' for dependency ${dependency} on module ${module.name}`
);
}
if (present.has(dependency)) {
if (!semver.satisfies(present.get(dependency), requirement)) {
throw new Error(`Module ${module.name} requires ${dependency} ${requirement}`);
}
// We have an unmet dependency, take it out and continue
} else {
if (hold.has(dependency)) {
hold.get(dependency).push(module);
} else {
hold.set(dependency, [module]);
}
modules.splice(index, 1);
continue reorder;
}
}
// No unmet dependencies, record and continue
present.set(module.name, module.version);
index += 1;
if (hold.has(module.name)) {
modules.splice(index, 0, ...hold.get(module.name));
hold.delete(module.name);
}
}
if (!hold.size) {
return;
}
// There are three reasons for a module to end up being held: The module depends
// on a module that is missing, the module is part of a dependency loop, or the
// the module depends on a module that satisfy any of these conditions.
let remaining = new Map();
for (let heldModules of hold.values()) {
for (let module of heldModules) {
remaining.set(module.name, module);
}
}
// Start with a random module from the remaining modules
for (let module of remaining.values()) {
let cycle = [];
while (true) {
// Find an unmet dependency
let dependency = Object.keys(module.dependencies).find(name => !present.has(name));
if (!remaining.has(dependency)) {
// There's no module being held up by this dependency, the
// dependency is missing.
throw new Error(`Missing dependency ${dependency} for module ${module.name}`);
}
if (cycle.includes(module.name)) {
cycle.push(module.name);
cycle.splice(0, cycle.indexOf(module.name));
throw new Error(`Module dependency loop detected: ${cycle.join(" -> ")}`);
}
cycle.push(module.name);
module = remaining.get(dependency);
}
}
}
/**
* Patch a save with the given modules
*
* Adds the modules given by the modules parameter to the save located
* at savePath and rewrites the control.lua in the save to load the
* modules that were added. Will also remove any previous module
* located in the save.
*
* @param {string} savePath - Path to the Factorio save to patch.
* @param {Array<Object>} modules - Description of the modules to patch.
* @memberof module:lib/factorio
*/
async function patch(savePath, modules) {
let zip = await JSZip.loadAsync(await fs.readFile(savePath));
let root = zip.folder(findRoot(zip));
let patchInfoFile = root.file("clusterio.json");
let patchInfo;
if (patchInfoFile !== null) {
let content = await patchInfoFile.async("string");
patchInfo = JSON.parse(content);
// No info file present, try to detect if it's a known compatible scenario.
} else {
let controlStream = root.file("control.lua").nodeStream("nodebuffer");
let controlHash = await libHash.hashStream(controlStream);
if (controlHash in knownScenarios) {
patchInfo = {
"scenario": knownScenarios[controlHash],
};
} else {
throw new Error(`Unable to patch save, unknown scenario (${controlHash})`);
}
}
// Increment patch number
patchInfo["patch_number"] = (patchInfo["patch_number"] || 0) + 1;
// Remove any existing modules from the save
patchInfo["modules"] = [];
for (let file of root.file(/^modules\//)) {
zip.remove(file.name);
}
reorderDependencies(modules);
// Add the modules to the save.
for (let module of modules) {
let moduleDir = root.folder(`modules/${module.name}`);
let moduleInfo = {
"name": module.name,
"files": [],
};
let dirs = [[module.path, ""]];
while (dirs.length) {
let [dir, relativeDir] = dirs.pop();
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
let fsPath = path.join(dir, entry.name);
let relativePath = path.posix.join(relativeDir, entry.name);
if (entry.isFile()) {
moduleDir.file(relativePath, await fs.readFile(fsPath));
moduleInfo["files"].push({
"path": relativePath,
"load": module.load.includes(relativePath),
"require": module.require.includes(relativePath),
});
} else if (entry.isDirectory()) {
dirs.push([fsPath, relativePath]);
}
}
}
patchInfo["modules"].push(moduleInfo);
}
// Add loading code and patch info
root.file("control.lua", generateLoader(patchInfo));
root.file("clusterio.json", JSON.stringify(patchInfo, null, 4));
// Write back the save
let tempSavePath = `${savePath}.tmp`;
let stream = zip.generateNodeStream({ compression: "DEFLATE" });
let pipe = stream.pipe(fs.createWriteStream(tempSavePath));
await events.once(pipe, "finish");
await fs.rename(tempSavePath, savePath);
}
module.exports = {
patch,
// For testing only
_findRoot: findRoot,
_generateLoader: generateLoader,
_reorderDependencies: reorderDependencies,
};
File diff suppressed because it is too large. Load diff
+116
View File
@@ -0,0 +1,116 @@
/**
* @module lib/file_ops
*/
"use strict";
const fs = require("fs-extra");
const path = require("path");
const crypto = require("crypto"); // needed for getTempFile
/**
* Returns the newest file in a directory
*
* @param {string} directory - The directory to look for the newest file in.
* @param {function(string): boolean} filter -
* Optional function to filter out files to ignore. Should return true
* if the file is to be considered.
* @returns {?string}
* Name of the file with the newest timestamp or null if the
* directory contains no files.
*/
async function getNewestFile(directory, filter = (name) => true) {
let newestTime = new Date(0);
let newestFile = null;
for (let entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isFile()) {
let stat = await fs.stat(path.join(directory, entry.name));
if (filter(entry.name) && stat.mtime > newestTime) {
newestTime = stat.mtime;
newestFile = entry.name;
}
}
}
return newestFile;
}
/**
* Modifies name in case it already exisist at the given directory
*
* Checks the directory passed if it already contains a file or folder with
* the given name, if it does not returns name unmodified, otherwise it
* returns a modified name that does not exist in the directory.
*
* Warning: this function should not be relied upon to be accurate for
* security sensitive applications. The selection process is inherently
* racy and a file or folder may have been created in the folder by the time
* this function returns.
*
* @param {string} directory - directory to check in.
* @param {string} name - file name to check for, may have extension.
* @param {string} extension - dot extension used for the file name.
* @returns {string} modified name with extension that likely does not exist
* in the folder
*/
async function findUnusedName(directory, name, extension = "") {
if (extension && name.endsWith(extension)) {
name = name.slice(0, -extension.length);
}
while (true) {
if (!await fs.pathExists(path.join(directory, `${name}${extension}`))) {
return `${name}${extension}`;
}
let match = /^(.*?)(-(\d+))?$/.exec(name);
if (!match[2]) {
name = `${match[1]}-2`;
} else {
name = `${match[1]}-${Number.parseInt(match[3], 10) + 1}`;
}
}
}
/**
* Warning: this function should not be relied upon to be accurate for
* security sensitive applications. The selection process is inherently
* racy and a file or folder may have been created in the folder by the time
* this function returns.
*
* @param {string} prefix - Prefix for file
* @param {string} suffix - Suffix for file
* @param {string} tmpdir - Directory for temp file
*/
async function getTempFile(prefix, suffix, tmpdir) {
prefix = (typeof prefix !== "undefined") ? prefix : "tmp.";
suffix = (typeof suffix !== "undefined") ? suffix : "";
tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./";
let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix);
let freeFile = await findUnusedName(tmpdir, fileName);
let fullPath = path.join(tmpdir, freeFile);
return fullPath;
}
/**
* Safely write data to a file
*
* Same as fs-extra.outputFile except the data is written to a temporary
* file that's renamed over the target file. The name of the temporary file
* is the same as the target file with the suffix `.tmp` added.
*
* If the operation fails it may leave behind the temporary file. This
* should not be too much of an issue as the next time the same file is
* written the temporary will be overwritten and renamed to the target file.
*
* @param {string} file - Path to file to write.
* @param {string|Buffer} data - Content to write.
* @param {object|string} options - see fs.writeFile, `flag` must not be set.
*/
async function safeOutputFile(file, data, options={}) {
let temporary = `${file}.tmp`;
await fs.outputFile(temporary, data, options);
await fs.rename(temporary, file);
}
module.exports = {
getNewestFile,
findUnusedName,
getTempFile,
safeOutputFile,
};
+46
View File
@@ -0,0 +1,46 @@
/**
* Hashing functions
* @module lib/hash
*/
"use strict";
const crypto = require("crypto");
const fs = require("fs");
/**
* Returns a promise that resolves to the SHA1 hash of the stream given
*
* @param {Stream} stream - Node stream of the content to hash.
* @returns {Promise<string>} hash of the stream.
*/
function hashStream(stream) {
return new Promise((resolve, reject) => {
let hasher = crypto.createHash("sha1");
hasher.setEncoding("hex");
hasher.on("finish", () => {
resolve(hasher.read());
});
stream.on("error", (error) => {
// the docs doesn't say anything about what's passed on error
reject(error);
});
stream.pipe(hasher);
});
}
/**
* Returns a promise that resolves to the SHA1 hash of the file given by path
*
* @param {string} path - Path to the file to hash.
* @returns {Promise<string>} hash of the file given.
*/
function hashFile(path) {
return hashStream(fs.createReadStream(path));
}
module.exports = {
hashStream,
hashFile,
};
+100
View File
@@ -0,0 +1,100 @@
/**
* Collection of small utilites that are useful in multiple places.
* @module lib/helpers
*/
"use strict";
/**
* Return a string describing the type of the value passed
*
* Works the same as typeof, excpet that null and array types get their
* own string.
*
* @param {*} value - value to return the type of.
* @returns {string} basic type of the value passed.
*/
function basicType(value) {
if (value === null) { return "null"; }
if (value instanceof Array) { return "array"; }
return typeof value;
}
/**
* Asynchronously wait for the given duration
*
* @param {number} duration - Time to wait for in milliseconds.
*/
async function wait(duration) {
await new Promise(resolve => { setTimeout(resolve, duration); });
}
/**
* Resolve a promise with a timeout.
*
* @param {Promise} promise - Promise to wait for.
* @param {number} time - Maximum time im milliseconds to wait for.
* @param {*=} timeoutResult - Value to return if the operation timed out.
*/
async function timeout(promise, time, timeoutResult) {
let timer;
try {
return await Promise.race([
promise,
new Promise(resolve => {
timer = setTimeout(() => resolve(timeoutResult), time);
}),
]);
} finally {
clearTimeout(timer);
}
}
/**
* Read stream to the end and return its content
*
* Reads the stream given asynchronously until the end is reached and
* returns all the data which was read from the stream.
*
* @param {Readable} stream - byte stream to read to the end.
* @returns {Buffer} content of the stream.
*/
async function readStream(stream) {
let chunks = [];
for await (let chunk of stream) {
// Support using ^Z to end input on Windows
if (process.platform === "win32" && stream.isTTY && chunk.toString() === "\x1a\r\n") {
break;
}
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
/**
* Escapes text for inclusion in a RegExp
*
* Adds \ character in front of special meta characters in the passsed in
* text so that it can be embedded into a RegExp and only match the text.
*
* See https://stackoverflow.com/a/9310752
*
* @param {string} text - Text to escape RegExp meta chars in.
* @returns {string} escaped text.
*/
function escapeRegExp(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
module.exports = {
basicType,
wait,
timeout,
readStream,
escapeRegExp,
};
+49
View File
@@ -0,0 +1,49 @@
"use strict";
const libBuildMod = require("./build_mod");
const libCommand = require("./command");
const libConfig = require("./config");
const libDatabase = require("./database");
const libErrors = require("./errors");
const libFactorio = require("./factorio");
const libFileOps = require("./file_ops");
const libHash = require("./hash");
const libHelpers = require("./helpers");
const libLink = require("./link");
const libLogging = require("./logging");
const libLoggingUtils = require("./logging_utils");
const libLuaTools = require("./lua_tools");
const libPlugin = require("./plugin");
const libPluginLoader = require("./plugin_loader");
const libPrometheus = require("./prometheus");
const libSchema = require("./schema");
const libSharedCommands = require("./shared_commands");
const libUsers = require("./users");
const ExponentialBackoff = require("./ExponentialBackoff");
const RateLimiter = require("./RateLimiter");
module.exports = {
libBuildMod,
libCommand,
libConfig,
libDatabase,
libErrors,
libFactorio,
libFileOps,
libHash,
libHelpers,
libLink,
libLogging,
libLoggingUtils,
libLuaTools,
libPlugin,
libPluginLoader,
libPrometheus,
libSchema,
libSharedCommands,
libUsers,
ExponentialBackoff,
RateLimiter,
};
+94
View File
@@ -0,0 +1,94 @@
/**
* Factorio Compatible INI parser
*
* @module lib/ini
* @author Hornwitser
*/
"use strict";
/**
* Parse INI file into object of sections
* @param {string} input - content of INI file to parse.
* @returns {object<string, object<string, string>>}
* sections parsed from the ini file.
*/
function parse(input) {
const sections = {};
let currentSection = sections;
const lines = input.split(/\r\n|\n/);
for (let number = 1; number <= lines.length; number++) {
let line = lines[number - 1].replace(/^[\t ]+/, "");
if (!line || line[0] === "#" || line[0] === ";") {
continue;
}
// Read section header
if (line[0] === "[") {
line = line.replace(/[\t ]+$/, "");
if (line.slice(-1) !== "]") {
throw new Error(`Unterminated section header on line ${number}`);
}
const name = line.slice(1, -1);
if (Object.prototype.hasOwnProperty.call(sections, name)) {
throw new Error(`Duplicated section [${name}] on line ${number}`);
}
currentSection = {};
sections[name] = currentSection;
continue;
}
// Read key value
const equalIndex = line.indexOf("=");
if (equalIndex === -1) {
throw new Error(`Missing value for key ${line} on line ${number}`);
}
const key = line.slice(0, equalIndex);
if (Object.prototype.hasOwnProperty.call(currentSection, key)) {
throw new Error(`Duplicated key ${key} on line ${number}`);
}
const value = line.slice(equalIndex + 1);
currentSection[key] = value;
}
return sections;
}
/**
* Serialize an object of section definitions into an INI file
* @param {object<string, object<string, string>>} sections -
* Sections to encode into ini format
* @returns {string} sections serialized into INI format
*/
function stringify(sections) {
let serialized = [];
let first = true;
for (let [key, value] of Object.entries(sections)) {
if (typeof value === "string") {
serialized.push(`${key}=${value}`);
first = false;
}
}
for (let [section, content] of Object.entries(sections)) {
if (typeof content === "object") {
if (!first) {
serialized.push("");
}
first = false;
serialized.push(`[${section}]`);
for (let [key, value] of Object.entries(content)) {
serialized.push(`${key}=${value}`);
}
}
}
if (serialized.length) { serialized.push(""); }
return serialized.join("\n");
}
module.exports = {
parse,
stringify,
};
+9
View File
@@ -0,0 +1,9 @@
/* eslint-disable node/global-require */
"use strict";
// eslint-disable-next-line node/no-process-env
if (process.env.APP_ENV === "browser") {
// eslint-disable-next-line node/no-extraneous-require
module.exports = require("fast-deep-equal/es6");
} else {
module.exports = require("util").isDeepStrictEqual;
}
+561
View File
@@ -0,0 +1,561 @@
// Connection adapters for Links
"use strict";
const events = require("events");
const WebSocket = require("isomorphic-ws");
const libSchema = require("../schema");
const libErrors = require("../errors");
const { logger } = require("../logging");
const ExponentialBackoff = require("../ExponentialBackoff");
/**
* Base connector for links
*
* @extends events.EventEmitter
* @memberof module:lib/link
*/
class WebSocketBaseConnector extends events.EventEmitter {
constructor(sessionTimeout) {
super();
this._sessionTimeout = sessionTimeout;
// One of closed, connecting (client only), connected and resuming.
this._state = "closed";
this._closing = false;
this._socket = null;
this._seq = 1;
this._lastHeartbeat = null;
this._heartbeatId = null;
this._heartbeatInterval = null;
this._lastReceivedSeq = null;
this._sendBuffer = [];
}
_reset() {
this._state = "closed";
this._closing = false;
this._socket = null;
this._seq = 1;
this._lastHeartbeat = null;
this._heartbeatId = null;
this._heartbeatInterval = null;
this._lastReceivedSeq = null;
this._sendBuffer.length = 0;
}
_check(...expectedStates) {
if (!expectedStates.includes(this._state)) {
throw new Error(
`Expected state ${expectedStates} but state is ${this._state}`
);
}
}
_dropSendBufferSeq(seq) {
if (seq === null) {
return;
}
let dropCount = 0;
for (let index = 0; index < this._sendBuffer.length; index++) {
if (this._sendBuffer[index].seq <= seq) {
dropCount += 1;
} else {
break;
}
}
this._sendBuffer.splice(0, dropCount);
}
_doHeartbeat() {
this._check("connected");
if (Date.now() - this._lastHeartbeat > 2000 * this._heartbeatInterval) {
logger.verbose("Connector | closing after heartbeat timed out");
// eslint-disable-next-line node/no-process-env
if (process.env.APP_ENV === "browser") {
this._socket.close(4008, "Heartbeat timeout");
} else {
this._socket.terminate();
}
} else {
this._socket.send(JSON.stringify({
seq: null,
type: "heartbeat",
data: { seq: this._lastReceivedSeq },
}));
}
}
_processHeartbeat(message) {
if (!libSchema.heartbeat(message)) {
logger.warn("Connector | closing after received invalid heartbeat");
this._socket.close(1002, "Invalid heartbeat");
return;
}
this._lastHeartbeat = Date.now();
this._dropSendBufferSeq(message.data.seq);
}
/**
* Start sending heartbeats
*/
startHeartbeat() {
if (this._heartbeatId) {
throw new Error("heartbeat is already running");
}
this._lastHeartbeat = Date.now();
this._heartbeatId = setInterval(() => {
this._doHeartbeat();
}, this._heartbeatInterval * 1000);
}
/**
* Stop sending heartbeats
*/
stopHeartbeat() {
if (this._heartbeatId) {
clearInterval(this._heartbeatId);
this._heartbeatId = null;
this._lastHeartbeat = null;
}
}
/**
* Send a message over the socket
*
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
* @returns {number} the sequence number of the message sent
*/
send(type, data) {
if (!["connected", "resuming"].includes(this._state)) {
throw new libErrors.SessionLost("No session");
}
let seq = this._seq;
this._seq += 1;
let message = { seq, type, data };
this._sendBuffer.push(message);
if (this._state === "connected") {
this._socket.send(JSON.stringify(message));
}
return seq;
}
/**
* Set connection state to closing
*
* Signal the connection is about to close and that once the close
* frame is received it should be considered finished.
*/
setClosing() {
throw new Error("Abstract function");
}
/**
* True if the connection is established and active
*/
get connected() {
return this._state === "connected";
}
/**
* True if the connection is closing down or closed
*/
get closing() {
return this._closing || this._state === "closed";
}
/**
* True if the connector currently has a valid session that is either
* connected or in the process of resuming.
*/
get hasSession() {
return ["connected", "resuming"].includes(this._state);
}
}
/**
* Connector for master server clients
*
* @extends module:lib/link.WebSocketBaseConnector
* @memberof module:lib/link
*/
class WebSocketClientConnector extends WebSocketBaseConnector {
constructor(url, maxReconnectDelay, tlsCa = null) {
super(null);
this._url = url;
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
this._tlsCa = tlsCa;
// The following states are used in the client connector
// closed: Not connected
// connecting: Attempting to connect to server.
// resuming: Attempting to resume an existing connection to the server.
// connected: Connection is online
this._reconnectId = null;
this._sessionToken = null;
this._startedResuming = null;
}
_reset() {
clearTimeout(this._reconnectId);
this._reconnectId = null;
this._lastReceivedSeq = null;
this._sessionToken = null;
this._sessionTimeout = null;
this._sendBuffer.length = 0;
this._startedResuming = null;
super._reset();
}
/**
* Send a handshake message over the socket
*
* Used in the register function in order to send the register handshake
* message over the WebSocket.
*
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
*/
sendHandshake(type, data) {
this._check("connecting", "resuming");
this._socket.send(JSON.stringify({ seq: null, type, data }));
}
/**
* Close the connection with the given reason.
*
* Sends a close frame and disconnects the connector.
*
* @param {number} code - WebSocket close code.
* @param {string} reason - WebSocket close reason.
*/
async close(code, reason) {
if (this._state === "closed") {
return;
}
this._closing = true;
if (this._state === "connected" || this._socket) {
this._socket.close(code, reason);
await events.once(this, "close");
} else {
this._reset();
this.emit("close");
}
}
/**
* Notify the connection is expected to close
*
* Sets the internal flag to signal not to attempt to resume the
* connection if the server closes it. This will close the connection
* if it's currently attempting to connect or resume.
*/
setClosing() {
if (this._state === "closed") {
return;
}
this._closing = true;
if (this._state === "connected") {
return;
}
if (this._socket) {
this._socket.close(1000, "Connector closing");
} else {
this._reset();
this.emit("close");
}
}
/**
* Connect to the master server
*/
async connect() {
this._check("closed");
this._state = "connecting";
this._doConnect();
// Wait for the connection to be ready
await events.once(this, "connect");
}
_doConnect() {
let url = new URL(this._url);
url.pathname += "api/socket";
// Open WebSocket to master
logger.verbose(`Connector | connecting to ${url}`);
// eslint-disable-next-line node/no-process-env
if (process.env.APP_ENV === "browser") {
this._socket = new WebSocket(url);
} else {
let options = {};
if (this._tlsCa) { options.ca = this._tlsCa; }
this._socket = new WebSocket(url, options);
}
this._attachSocketHandlers();
}
/**
* Attempt re-establishing the connecting again
*/
reconnect() {
if (this._socket) {
throw new Error("Cannot reconnect while socket is open");
}
if (this._reconnectId !== null) {
logger.error("Unexpected double call to reconnect");
}
let delay = this._backoff.delay();
if (
this._state === "resuming"
&& this._startedResuming + this._sessionTimeout * 1000 < Date.now() + delay
) {
logger.error("Connector | Session timed out trying to resume");
this._reset();
this._state = "connecting";
this.emit("invalidate");
}
logger.verbose(
`Connector | waiting ${(Math.round(delay / 10) / 100)} seconds for reconnect`
);
this._reconnectId = setTimeout(() => {
this._reconnectId = null;
this._doConnect();
}, delay);
this._reconnectTime = Date.now() + delay;
}
_attachSocketHandlers() {
this._socket.onclose = event => {
const previousState = this._state;
// Authentication failed
if (event.code === 4003) {
this.emit("error", new libErrors.AuthenticationFailed(event.reason));
this._closing = true;
}
this._socket = null;
if (this._state === "connected") {
this.stopHeartbeat();
if (this._closing) {
this._reset();
this.emit("close");
} else {
this._state = "resuming";
this._startedResuming = Date.now();
this.reconnect();
this.emit("drop");
}
} else {
// eslint-disable-next-line no-lonely-if
if (this._closing) {
this._reset();
this.emit("close");
} else {
this.reconnect();
}
}
// Log must be sent after state change is complete
let message = `Connector | Close (code: ${event.code}, reason: ${event.reason})`;
if (previousState === "connected" && event.code !== 1000) {
logger.info(message);
} else {
logger.verbose(message);
}
};
this._socket.onerror = event => {
// It's assumed that close is always called by ws
let code = !event.error ? "" : `, code: ${event.error.code}`;
let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`;
if (this._state === "connected") {
logger.error(message);
} else {
// Don't log as error if not actually connected, prevents
// log spam during reconnecting attempts.
logger.verbose(message);
}
if (event.error) {
// Abort connection if certificate validation failed
if ([
"CERT_HAS_EXPIRED",
"DEPTH_ZERO_SELF_SIGNED_CERT",
"ERR_TLS_CERT_ALTNAME_INVALID",
].includes(event.error.code)) {
this.emit("error", new libErrors.AuthenticationFailed(event.error.message));
this._closing = true;
}
}
};
this._socket.onopen = () => {
logger.verbose("Connector | Open");
};
// Handle messages
this._socket.onmessage = event => {
let message = JSON.parse(event.data);
if (["connecting", "resuming"].includes(this._state)) {
this._processHandshake(message);
} else if (this._state === "connected") {
if (message.seq !== null) {
this._lastReceivedSeq = message.seq;
}
if (message.type === "heartbeat") {
this._processHeartbeat(message);
} else {
this.emit("message", message);
}
} else {
throw new Error(`Received message in unexpected state ${this._state}`);
}
};
}
_processHandshake(message) {
if (!libSchema.serverHandshake(message)) {
logger.warn("Connector | closing after received invalid handshake");
this._socket.close(1002, "Invalid handshake");
return;
}
let { seq, type, data } = message;
if (type === "hello") {
logger.verbose(`Connector | received hello from master version ${data.version}`);
this.emit("hello", data);
if (this._sessionToken) {
logger.verbose("Connector | Attempting resume");
this.sendHandshake("resume", {
session_token: this._sessionToken,
last_seq: this._lastReceivedSeq,
});
} else {
this.register();
}
} else if (type === "ready") {
logger.verbose("Connector | received ready from master");
this._state = "connected";
this._sessionToken = data.session_token;
this._sessionTimeout = data.session_timeout;
this._heartbeatInterval = data.heartbeat_interval;
this.startHeartbeat();
for (let bufferedMessage of this._sendBuffer) {
this._socket.send(JSON.stringify(bufferedMessage));
}
this.emit("connect", data);
} else if (type === "continue") {
logger.info("Connector | resuming existing session");
this._state = "connected";
this._heartbeatInterval = data.heartbeat_interval;
this._sessionTimeout = data.session_timeout;
this.startHeartbeat();
this._dropSendBufferSeq(data.last_seq);
for (let bufferedMessage of this._sendBuffer) {
this._socket.send(JSON.stringify(bufferedMessage));
}
this._startedResuming = null;
this.emit("resume");
} else if (type === "invalidate") {
logger.warn("Connector | session invalidated by master");
this._state = "connecting";
this._seq = 1;
this._lastReceivedSeq = null;
this._sessionToken = null;
this._sessionTimeout = null;
this._sendBuffer.length = 0;
this._startedResuming = null;
this.emit("invalidate");
this.register();
}
}
/**
* Register the link with the server
*
* This function is expected to be overriden by sub-classes and send the
* register message over the socket when invoked.
*/
register() {
throw Error("Abstract function");
}
}
/**
* Connector for in-memory local links
*
* @extends events.EventEmitter
* @memberof module:lib/link
*/
class VirtualConnector extends events.EventEmitter {
constructor() {
super();
this.other = this;
this._seq = 1;
}
/**
* Create a pair of virtual connector hooked into each other
*
* Creates two virtual connectors that are liked to each othes such that
* a message sent on one is received by the other.
*
* @returns {Array} two virtuarl connectors.
*/
static makePair() {
let first = new this();
let second = new this();
first.other = second;
second.other = first;
return [first, second];
}
/**
* Send a message to the other end of the connector
*
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
* @returns {number} the sequence number of the message sent
*/
send(type, data) {
let seq = this._seq;
this._seq += 1;
this.other.emit("message", { seq, type, data });
return seq;
}
}
module.exports = {
WebSocketBaseConnector,
WebSocketClientConnector,
VirtualConnector,
};
+15
View File
@@ -0,0 +1,15 @@
/**
* Library for message based communication between nodes
* @module lib/link
* @author Hornwitser
*/
"use strict";
const link = require("./link");
const messages = require("./messages");
const connectors = require("./connectors");
module.exports = {
...link,
...messages,
...connectors,
};
+264
View File
@@ -0,0 +1,264 @@
// Implementation of Link class
"use strict";
const libSchema = require("../schema");
const libErrors = require("../errors");
const { logger } = require("../logging");
const messages = require("./messages");
// Some definitions for the terminology used here:
// link: Either side of a master - client connection
// connector: Adapter between a link and a socket connection
// connection: the master server side of a link
// client: the side that is not the master server of a link
// message: the complete object sent using the 'message' event
// data: the data property of a message, essentially the payload.
// type: the type property of a message, identifies what to expect in data.
/**
* Common interface for server and client connections
*
* @memberof module:lib/link
*/
class Link {
constructor(source, target, connector) {
this.source = source;
this.target = target;
this.connector = connector;
this._waiters = new Map();
this._handlers = new Map();
this._validators = new Map();
// Process messages received by the connector
connector.on("message", payload => {
try {
this.processMessage(payload);
} catch (err) {
if (err instanceof libErrors.InvalidMessage) {
logger.error(`Invalid message on ${this.source}-${this.target} link: ${err.message}`);
if (err.errors) {
logger.error(JSON.stringify(err.errors, null, 4));
}
// Send back an error response if this was a request.
if (
typeof payload.type === "string"
&& payload.type.endsWith("_request")
&& Number.isInteger(payload.seq)
) {
this.connector.send(
`${payload.type.slice(0, -8)}_response`,
{ seq: payload.seq, error: err.message }
);
}
} else {
this.connector.emit("error", err);
}
}
});
connector.on("invalidate", () => {
for (let waiterType of this._waiters.values()) {
for (let waiter of waiterType) {
waiter.reject(new libErrors.SessionLost("Session Lost"));
}
}
this._waiters.clear();
});
connector.on("close", () => {
for (let waiterType of this._waiters.values()) {
for (let waiter of waiterType) {
waiter.reject(new libErrors.SessionLost("Session Closed"));
}
}
this._waiters.clear();
});
}
/**
* Process a received message on the link
*
* Validates and invokes the handler and/or waiters for a message that has
* been received. An unhandled message is considered to be an error.
*
* @param {Object} message - Message to process.
* @throws {module:lib/errors.InvalidMessage}
* if validation failed, is missing, the message is invalid, or no
* handlers/waiters were invoked to handle the message.
*/
processMessage(message) {
if (!libSchema.message(message)) {
throw new libErrors.InvalidMessage("Malformed", libSchema.message.errors);
}
let validator = this._validators.get(message.type);
if (!validator) {
throw new libErrors.InvalidMessage(`No validator for ${message.type} on ${this.source}-${this.target}`);
}
if (!validator(message)) {
throw new libErrors.InvalidMessage(`Validation failed for ${message.type}`, validator.errors);
}
let hadHandlers = this._processHandler(message);
let hadWaiters = this._processWaiters(message);
if (!hadWaiters && !hadHandlers) {
// XXX console.error(`Unhandled message: ${JSON.stringify(message, null, 4)}`);
throw new libErrors.InvalidMessage(`Unhandled message ${message.type}`);
}
}
/**
* Invoke the handler for this message if it exists
*
* @param {Object} message - Message to invoke handlers on.
* @returns {Boolean} true if a handler was invoked.
*/
_processHandler(message) {
let handler = this._handlers.get(message.type);
if (handler) {
handler(message);
return true;
}
return false;
}
/**
* Invokes the waiters for this message if any
*
* @param {Object} message - Message to invoke waiters on.
* @returns {Boolean} true if a waiter was triggered.
*/
_processWaiters(message) {
let { seq, type, data } = message;
let waiters = this._waiters.get(type);
if (!waiters || !waiters.length) {
return false;
}
let matched = [];
waitersLoop: for (let index = 0; index < waiters.length; index++) {
let waiter = waiters[index];
for (let [key, value] of Object.entries(waiter.data)) {
if (data[key] !== value) {
continue waitersLoop;
}
}
waiter.resolve(message);
matched.push(index);
}
matched.reverse();
for (let index of matched) {
waiters.splice(index, 1);
}
return matched.length > 0;
}
/**
* Set handler for a message type
*
* Set function called upon receiving a message of the given type.
*
* @param {string} type - Type of message to listen to
* @param {function} handler -
* Callack to invoke on messages of the given type.
* @param {function} validator -
* Function validating the message have the correct format.
*/
setHandler(type, handler, validator) {
if (this._handlers.has(type)) {
throw new Error(`${type} already has a handler`);
}
if (!validator) {
throw new Error("validator is required");
}
this.setValidator(type, validator);
this._handlers.set(type, handler);
}
/**
* Set validator for a message type
*
* Set function called upon receiving a message of the given type in
* order to validate the message. The function should return true if
* the message is valid and false otherwise.
*
* @param {string} type - Type of message to validate
* @param {function} validator - Validation function.
*/
setValidator(type, validator) {
if (this._validators.has(type)) {
throw new Error(`${type} already has a validator`);
}
this._validators.set(type, validator);
}
/**
* Wait for a message matching given type and data
*
* Waits for a message over the link that matches the type given as well
* as any properties specified in data. For example specifying { foo:
* 21 } as the data will cause it to wait until a message with the right
* type and a foo property value of 21 in the data payload.
*
* @param {string} type - Message type to wait for.
* @param {Object} data - Properties to match in the message data.
*/
async waitFor(type, data) {
if (!this._validators.has(type)) {
throw new Error(`No validator for ${type} on ${this.source}-${this.target}`);
}
let waiter = { data };
waiter.promise = new Promise((resolve, reject) => {
waiter.resolve = resolve;
waiter.reject = reject;
if (!this._waiters.has(type)) {
this._waiters.set(type, [waiter]);
} else {
this._waiters.get(type).push(waiter);
}
});
return waiter.promise;
}
/**
* Handle ping requests
*/
async pingRequestHandler() { }
/**
* Prepare connection for disconnect
*
* Waits for all pending requests on the connection to resolve.
* Sub-classes should implement this handler with code that prevents
* additional requests to be sent out before calling the super class
* method.
*/
async prepareDisconnectRequestHandler() {
let promises = [];
for (let waiterType of this._waiters.values()) {
for (let waiter of waiterType) {
promises.push(waiter.promise.then(() => {}, () => {}));
}
}
await Promise.all(promises);
}
}
module.exports = {
Link,
};
File diff suppressed because it is too large. Load diff
+94
View File
@@ -0,0 +1,94 @@
"use strict";
const winston = require("winston");
const Transport = require("winston-transport");
const { LEVEL, MESSAGE } = require("triple-beam");
const levels = Object.freeze({
fatal: 0,
error: 1,
warn: 2,
audit: 3,
info: 4,
server: 5,
verbose: 6,
});
const colors = {
fatal: "inverse red",
error: "bold brightRed",
warn: "bold brightYellow",
audit: "bold brightGreen",
info: "bold brightBlue",
server: "bold",
verbose: "bold grey",
};
winston.addColors(colors);
// The Console Transport is replicated here because it's overly complicated
// in winston, writes to stdout/err instead of console and doesn't work in
// the browser.
class ConsoleTransport extends Transport {
constructor(options = {}) {
super(options);
this.errorLevels = new Set(options.errorLevels || ["fatal", "error"]);
this.warnLevels = new Set(options.warnLevels || ["warn"]);
}
log(info, callback) {
/* eslint-disable no-console */
if (this.errorLevels.has(info[LEVEL])) {
console.error(info[MESSAGE]);
} else if (this.warnLevels.has(info[LEVEL])) {
console.warn(info[MESSAGE]);
} else {
console.log(info[MESSAGE]);
}
/* eslint-enable no-console */
callback();
}
}
/**
* Formats winston log messages for the web console
*/
class WebConsoleFormat {
constructor(options = {}) {
this.options = options;
}
transform(info, options) {
let src = " ";
if (info.slave_id) {
let slaveName = info.slave_id;
src += `s:${slaveName} - `;
}
if (info.instance_id) {
let instanceName = info.instance_id;
src += `i:${instanceName}`;
}
if (info.plugin) {
src += `${info.plugin}: `;
}
if (info.stack) {
info[MESSAGE] = `[${info.level}]${src}${info.stack}`;
} else {
info[MESSAGE] = `[${info.level}]${src}${info.message}`;
}
return info;
}
}
const logger = winston.createLogger({
level: "verbose",
levels,
format: winston.format.timestamp(),
});
module.exports = {
ConsoleTransport,
WebConsoleFormat,
colors,
levels,
logger,
};
+131
View File
@@ -0,0 +1,131 @@
"use strict";
const winston = require("winston");
const Transport = require("winston-transport");
const { LEVEL, MESSAGE } = require("triple-beam");
const chalk = require("chalk");
const libLink = require("./link");
const libErrors = require("./errors");
/**
* Format a parsed Factorio message with colors
*
* Formats a parsed Factorio output from lib/factorio into a readable
* colorized output using terminal escape codes that can be printed.
*
* @param {Object} parsed - Parsed Factorio server output.
* @returns {string} terminal colorized message.
* @private
*/
function formatServerOutput(parsed) {
let time = "";
if (parsed.format === "seconds") {
time = `${chalk.yellow(parsed.time.padStart(8))} `;
} else if (parsed.format === "date") {
time = `${chalk.yellow(parsed.time)} `;
}
let info = "";
if (parsed.type === "log") {
let level = parsed.level;
if (level === "Script") {
level = chalk.bold.greenBright(level);
} else if (level === "Verbose") {
level = chalk.bold.gray(level);
} else if (level === "Info") {
level = chalk.bold.blueBright(level);
} else if (parsed.level === "Warning") {
level = chalk.bold.yellowBright(level);
} else if (parsed.level === "Error") {
level = chalk.bold.redBright(level);
}
info = `${level} ${chalk.gray(parsed.file)}: `;
} else if (parsed.type === "action") {
info = `[${chalk.yellow(parsed.action)}] `;
}
return time + info + parsed.message;
}
// These are defined here to avoid circular dependencies and pulling them
// into the web interface code.
/**
* Formats winston log messages for a character terminal.
*/
class TerminalFormat {
constructor(options = {}) {
this.options = options;
this.colorize = winston.format.colorize(options);
}
transform(info, options) {
info = this.colorize.transform(info, this.colorize.options);
let ts = "";
if (options.showTimestamp && info.timestamp) {
ts = `${info.timestamp.replace("T", " ")} `;
}
let src = " ";
if (info.instance_name) {
src += `i:${info.instance_name} `;
} else if (info.slave_name) {
src += `s:${info.slave_name} `;
}
if (info.plugin) {
src += `${info.plugin}: `;
}
if (info[LEVEL] === "server" && info.parsed) {
info.message = formatServerOutput(info.parsed);
}
if (info.stack) {
info[MESSAGE] = `${ts}[${info.level}]${src}${info.stack}`;
} else {
info[MESSAGE] = `${ts}[${info.level}]${src}${info.message}`;
}
return info;
}
}
/**
* Sends logs over a lib/link connection.
*/
class LinkTransport extends Transport {
constructor(options) {
super(options);
this.link = options.link;
this.filter = options.filter || null;
}
log(info, callback) {
if (this.filter && !this.filter(info)) {
return callback();
}
try {
libLink.messages.logMessage.send(this.link, { info });
} catch (err) {
// Ignore session lost errors.
if (!(err instanceof libErrors.SessionLost)) {
throw err;
}
}
return callback();
}
}
module.exports = {
TerminalFormat,
LinkTransport,
// for testing only
_formatServerOutput: formatServerOutput,
};
+26
View File
@@ -0,0 +1,26 @@
/**
* Utilities for dealing with Lua
* @module lib/lua_tools
*/
"use strict";
/**
* Escapes a string for inclusion into a lua string
*
* @param {string} content - String to escape.
* @returns {string} Escaped string.
*/
function escapeString(content) {
return content
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/'/g, "\\'")
.replace(/\0/g, "\\0")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
;
}
module.exports = {
escapeString,
};
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@clusterio/lib",
"description": "Shared library for Clusterio",
"version": "2.0.0-alpha.10",
"repository": "https://github.com/clusterio/clusterio",
"license": "MIT",
"browser": "browser.js",
"keywords": [
"factorio"
],
"engines": {
"node": ">=12"
},
"dependencies": {
"ajv": "^6.12.6",
"chalk": "^4.1.0",
"fs-extra": "^8.1.0",
"isomorphic-ws": "^4.0.1",
"jimp": "^0.12.1",
"jsonwebtoken": "^8.2.1",
"jszip": "^3.2.2",
"klaw": "^3.0.0",
"rcon-client": "^4.2.0",
"semver": "^7.1.1",
"set-blocking": "^2.0.0",
"triple-beam": "^1.3.0",
"winston": "^3.3.3",
"winston-transport": "^4.4.0",
"ws": "^7.2.3",
"yargs": "^14.2.0"
},
"publishConfig": {
"access": "public"
}
}
+765
View File
@@ -0,0 +1,765 @@
/**
* Plugin interfaces and utilities.
* @module lib/plugin
*/
"use strict";
const helpers = require("./helpers");
/**
* Conceptual base for master and instance plugins.
* @typedef {
* module:lib/plugin.BaseMasterPlugin
* |module:lib/plugin.BaseInstancePlugin
* |module:lib/plugin.BaseControlPlugin
* } module:lib/plugin.BasePlugin
*/
/**
* Base class for instance plugins
*
* Instance plugins are subclasses of this class which get instantiated by
* the slave when it brings up an instance with the plugin enabled in the
* config. To be discovered the class must be exported under the name
* `InstancePlugin` in the module specified by the `instanceEntrypoint` in
* the plugin's info.js file.
*
* Instances may be started and stopped many times, and many instances may
* be running at the same time, each of which will have their own instance
* of the InstancePlugin class.
*
* @static
*/
class BaseInstancePlugin {
constructor(info, instance, slave) {
/**
* The plugin's own info module
*/
this.info = info;
/**
* Instance the plugin started for
* @type {module:slave/slave~Instance}
*/
this.instance = instance;
/**
* Slave running the instance
*
* With the exepction of accessing the slave's config you should
* avoid ineracting with the slave object directly.
*
* @type {module:slave/slave~Slave}
*/
this.slave = slave;
/**
* 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 = instance.logger.child({ plugin: this.info.name });
this._pendingRconMessages = [];
this._sendingRconMessages = false;
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called when the value of a config field changed.
*
* Invoked after the value of the config field given by `field` has
* changed.
*
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed on.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(group, field, prev) { }
/**
* Called before collecting Prometheus metrics
*
* Invoked before the default metrics of prometheus is collected. Note
* that since this is done while replying to /metrics this should not
* take a long time, otherwise the requst will time out.
*
* Although you can return the results of collecting your prometheus
* collectors directly, all gauges in the default registry is already
* automatically collected. It's therefore recommended to only update
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
/**
* Called after the Factorio server is started
*/
async onStart() { }
/**
* Called before the Factorio server is stopped
*
* This will not be called if for example the Factorio server crashes or
* is killed.
*/
async onStop() { }
/**
* Called when the instance exits
*
* Invoked when the instance is shut down. This may occur after init
* has been called if an error occurs during startup. Note that if
* the plugin's init() throws this method will still be invoked.
*/
onExit() { }
/**
* Called when the Factorio outputs a line
*
* Called when the Factorio server outputs a line on either stdout or
* stderr. The output is a parsed form of the line and contains the
* following properties:
*
* @param {Object} parsed - parsed server output.
* @param {string} parsed.source -
* Where the message came from, one of "stdout" and "stderr".
* @param {string} parsed.format -
* Timestamp format, one of "date", "seconds" and "none".
* @param {string=} parsed.time -
* Timestamp of the message. Not present if format is "none".
* @param {string} parsed.type -
* Type of message, one of "log", "action" and "generic".
* @param {string=} parsed.level -
* Log level for "log" type. i.e "Info" normally.
* @param {string=} parsed.file - File reported for "log" type.
* @param {string=} parsed.action -
* Kind of action for "action" type. i.e "CHAT" for chat.
* @param {string} parsed.message - Main content of the line.
* @param {string} line - raw line of server output.
*/
async onOutput(parsed, line) { }
/**
* 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 slave 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 on the slave 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) { }
/**
* Called when the master is preparing to disconnect from the slave
*
* Invoked when the master server has requested the disconnection from
* the slave. This typically happens when it is in the process of
* shutting down.
*
* Plugins must stop sending messages to the master, or are forwarded
* via the master server after the prepare disconnect has been handled.
*
* @param {module:master/src/SlaveConnection} connection -
* The connection to the slave preparing to disconnect.
*/
async onPrepareMasterDisconnect(connection) { }
/**
* Called when a player joins or leaves the game
*
* Invoked when a player either joins or leaves the instance.
*
* @param {Object} event - Information about the event.
* @param {string} event.type - Either "join" or "leave".
* @param {string} event.name - Name of the player that joined/left.
*/
async onPlayerEvent(event) { }
/**
* Send RCON message to instance
*
* Send a message or command to the server which can potentially be
* executed out of order in relation to other RCON commands.
*
* This should not be called before onStart or after onStop. A simple
* way to achieve this is to check that the instance status is running
* before sending commands. Note that the instance status is set to
* running some time after `onStart` and some time before `onStop` is
* invoked.
*
* @param {string} message - message to send to server over RCON.
* @param {boolean=} [expectEmpty=false] -
* if true throw if the response is not empty. Useful for detecting
* errors that might have been sent in response.
* @returns {Promise<string>} response from server.
*/
async sendRcon(message, expectEmpty=false) {
return await this.instance.sendRcon(message, expectEmpty, this.info.name);
}
/**
* Send serially ordered RCON message to instance
*
* Send a message or command to the server which will not be executed
* out of order in relation to other RCON commands sent with this
* method. The ordering applies per plugin, and two plugins sending
* commands with this method may execute out of order in relation to
* each other.
*
* This should not be called before onStart or after onStop. A simple
* way to achieve this is to check that the instance status is running
* before sending commands. Note that the instance status is set to
* running some time after `onStart` and some time before `onStop` is
* invoked.
*
* @param {string} message - message to send to server over RCON.
* @param {boolean=} [expectEmpty=false] -
* if true throw if the response is not empty. Useful for detecting
* errors that might have been sent in response.
* @returns {Promise<string>} response from server.
*/
async sendOrderedRcon(message, expectEmpty=false) {
let promise = new Promise((resolve, reject) => {
this._pendingRconMessages.push({resolve, reject, message, expectEmpty});
});
if (!this._sendingRconMessages) {
this._sendPendingRconMessages();
}
return await promise;
}
async _sendPendingRconMessages() {
this._sendingRconMessages = true;
while (this._pendingRconMessages.length) {
let task = this._pendingRconMessages.shift();
try {
let result = await this.sendRcon(task.message, task.expectEmpty);
task.resolve(result);
} catch (err) {
task.reject(err);
}
}
this._sendingRconMessages = false;
}
}
/**
* Base class for master plugins
*
* Master plugins are subclasses of this class which get instantiated by
* the master server on startup when the plugin is enabled in the config.
* To be discovered the class must be exported under the name `MasterPlugin`
* in the module specified by the `masterEntrypoint` in the plugin's info.js
* file.
*
* @static
*/
class BaseMasterPlugin {
constructor(info, master, metrics, logger) {
this.info = info;
/**
* Master server
* @type {module:master/src/Master}
*/
this.master = master;
this.metrics = metrics;
/**
* 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 });
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called when the status of an instance changes
*
* Invoked when the master server has changed the status of an instance
* or received notice from a slave that the status of an instance has
* changed. The possible statuses that can be notified about are:
* - `unassigned:`: Instance is no longer asssigned to a slave.
* - `unknown`: Slave assigned to instance is offline.
* - `stopped`: Instance is no longer running or was just assigned to a
* slave.
* - `starting`: Instance is in the process of starting up.
* - `running`: Instance startup completed and is now running.
* - `stopping`: Instance is in the processing stopping.
* - `creating_save`: Instance is in the process of creating a save.
* - `exporting_data`: Instance is in the process of exporting item
* icons and locale data.
* - `deleted`: Instance was deleted.
*
* On master startup all known instances gets the status `unknown` if
* they are assigned to a slave, when the slave then connects to the
* master and informs of the current status of its instances this hook
* is invoked for all those instances. You can detect this situation by
* checking if prev equals `unknown`. If the slave disconnects the
* instances will again get the `unknown` status.
*
* When instances are created on the master they will notify of a status
* change with prev set to null. While the status of new instances is
* in most cases `unassigned` it's possible for the created instance to
* start with any state in some slave connection corner cases.
*
* Note that it's possible for status change notification to get lost in
* the case of network outages if reconnect fails to re-establish the
* session between the master server and the slave.
*
* @param {Object} instance - the instance that changed.
* @param {?string} prev - the previous status of the instance.
*/
async onInstanceStatusChanged(instance, prev) { }
/**
* Called when the value of an instance config field changed.
*
* Invoked after the value of the config field given by `field` has
* changed on an instance.
*
* @param {Object} instance - The instance the config changed on.
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed on.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(instance, group, field, prev) { }
/**
* Called before collecting Prometheus metrics
*
* Invoked before the default metrics of prometheus is collected. Note
* that since this is done while replying to /metrics this should not
* take a long time, otherwise the requst will time out.
*
* Although you can return the results of collecting your prometheus
* collectors directly, all gauges in the default registry is already
* automatically collected. It's therefore recommended to only update
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
/**
* Called when the master server is shutting down
*/
async onShutdown() { }
/**
* Called when an event on a slave connection happens
*
* The event param may be one of connect, drop, resume and close and has
* the following meaning:
*
* ##### connect
*
* Invoked when a new connection to the slave has been established.
*
* ##### drop
*
* Invoked when a connection loss is detected between the master and a
* slave. Plugins should respond to this event by throtteling messages
* it is sending to the given slave connection to an absolute minimum.
*
* Messages sent over a dropped slave connection will get queued up in
* memory on the master and sent all in one go when the connection is
* re-established again.
*
* ##### resume
*
* Invoked when a connection that had previously been dropped is
* re-established
*
* ##### close
*
* Invoked when the connection to the slave has been closed.
*
* @param {module:master/src/SlaveConnection} connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume and close
*/
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/src/ControlConnection} connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume, and close.
*/
onControlConnectionEvent(connection, event) { }
/**
* Called when a slave is preparing to disconnect from the master
*
* Invoked when a slave has requested the disconnection from the master.
* This typically happens when it is in the process of shutting down.
*
* Plugins must stop sending messages to the slave in question after the
* prepare disconnect has been handled.
*
* @param {module:master/src/SlaveConnection} connection -
* The connection to the slave preparing to disconnect.
*/
async onPrepareSlaveDisconnect(connection) { }
/**
* Called when a player joins or leaves an instance
*
* Invoked when a player either joins or leaves an instance in the
* cluster.
*
* @param {Object} instance - the instance it occured on.
* @param {Object} event - Information about the event.
* @param {string} event.type - Either "join" or "leave".
* @param {string} event.name - Name of the player that joined/left.
*/
async onPlayerEvent(instance, event) { }
/**
* Broadcast event to all connected slaves
*
* Sends the given event to all slaves connected to the master server.
* This does not include slaves that are in the process of closing the
* connection, which typically happens when they are shutting down.
*
* @param {module:lib/link.Event} event - Event to send
* @param {Object} data - Data ta pass with the event.
*/
broadcastEventToSlaves(event, data={}) {
for (let slaveConnection of this.master.wsServer.slaveConnections.values()) {
if (
!slaveConnection.connector.closing
&& (!event.plugin || slaveConnection.plugins.has(event.plugin))
) {
event.send(slaveConnection, data);
}
}
}
}
/**
* Base class for clusterioctl plugins
*
* Control plugins are subclasses of this class which get instantiated by
* clusterioctl in order to extend its functionallity. To be discovered the
* class must be exported under the name `ControlPlugin` in the module
* specified by the `controlEntrypoint` in the plugin's info.js file.
*
* @static
*/
class BaseControlPlugin {
constructor(info, logger) {
/**
* The plugin's own info module
*/
this.info = info;
/**
* 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 });
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called to add commands to the command line interface.
*
* Invoked by clusterioctl to let plugins add commands. `rootCommand` is
* the top level command node which the plugin should add its own {@link
* module:lib/command.CommandTree} to.
*
* @param {module:lib/command.CommandTree} rootCommand -
* Root of the clusterioctl command tree.
*/
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
*
* Attaches all messages defined in the `.info.message` property of the
* `plugin` passed with handlers taken from the `pluin`.
*
* @param {module:lib/link.Link} link - Link to attach handlers to.
* @param {module:lib/plugin.BasePlugin} plugin -
* The instance of the plugin attach messages and handlers from.
*/
function attachPluginMessages(link, plugin) {
let messageDefinitions = plugin.info.messages || [];
for (let [name, messageFormat] of Object.entries(messageDefinitions)) {
if (messageFormat.plugin !== plugin.info.name) {
throw new Error(`Type of ${name} message must start with "${plugin.info.name}:"`);
}
let handler = name + messageFormat.handlerSuffix;
try {
if (plugin === null || !plugin[handler]) {
messageFormat.attach(link);
} else {
messageFormat.attach(link, async (message, format) => await plugin[handler](message, format, link));
}
} catch (err) {
if (err.code === "MISSING_LINK_HANDLER") {
err.plugin = plugin.info.name;
err.handler = handler;
}
throw err;
}
}
}
/**
* Invokes the given hook on all plugins
*
* @param {Map<string, Object>} plugins -
* Mapping of plugin names to plugins to invoke the hook on.
* @param {string} hook - Name of hook to invoke.
* @param {...*} args - Arguments to pass on to the hook.
* @returns {Array} Non-empty return values from the hooks.
*/
async function invokeHook(plugins, hook, ...args) {
let results = [];
for (let [name, plugin] of plugins) {
try {
// Use an object to detect if the hook failed on timeout or tried to return a value looking like an error
const timeout = {};
let result = await helpers.timeout(
plugin[hook](...args),
15000,
timeout
);
if (result === timeout) {
throw new Error(`Invoking hook ${hook} timed out for plugin ${name}`);
} else if (result !== undefined) {
results.push(result);
}
} catch (err) {
plugin.logger.error(`Ignoring error from plugin ${name} in ${hook}:\n${err.stack}`);
}
}
return results;
}
module.exports = {
BaseInstancePlugin,
BaseMasterPlugin,
BaseControlPlugin,
BaseWebPlugin,
attachPluginMessages,
invokeHook,
};
+98
View File
@@ -0,0 +1,98 @@
/* eslint-disable node/global-require */
/**
* Helpers for loading plugins in Node.js
* @module lib/plugin_loader
*/
"use strict";
const libErrors = require("./errors");
const path = require("path");
/**
* Load plugin information
*
* Loads plugin info modules for the paths to the given plugins. Once
* loaded the info modules will not be reloaded should this function be
* called again.
*
* @param {Map<string, string>} pluginList -
* Mapping of plugin name to require path for the plugins to load.
* @returns {Array<Object>} Array of plugin info modules.
* @static
*/
async function loadPluginInfos(pluginList) {
let plugins = [];
for (let [pluginName, pluginPath] of pluginList) {
let pluginInfo;
let pluginPackage;
try {
pluginInfo = require(path.posix.join(pluginPath, "info"));
pluginPackage = require(path.posix.join(pluginPath, "package.json"));
} catch (err) {
throw new libErrors.PluginError(pluginName, err);
}
if (pluginInfo.name !== pluginName) {
throw new libErrors.EnvironmentError(
`Expected plugin at ${pluginPath} to be named ${pluginName} but got ${pluginInfo.name}`
);
}
pluginInfo.requirePath = pluginPath;
pluginInfo.version = pluginPackage.version;
plugins.push(pluginInfo);
}
return plugins;
}
/**
* Load master plugin class of a plugin
*
* @param {Object} pluginInfo -
* Plugin info object returned from {@link
* module:lib/plugin_loader.loadPluginInfos} to load class from.
* @returns {function} plugin class
* @static
*/
async function loadMasterPluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint));
return entrypoint.MasterPlugin;
}
/**
* Load instance plugin class of a plugin
*
* @param {Object} pluginInfo -
* Plugin info object returned from {@link
* module:lib/plugin_loader.loadPluginInfos} to load class from.
* @returns {function} plugin class
* @static
*/
async function loadInstancePluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint));
return entrypoint.InstancePlugin;
}
/**
* Load control plugin class of a plugin
*
* @param {Object} pluginInfo -
* Plugin info object returned from {@link
* module:lib/plugin_loader.loadPluginInfos} to load class from.
* @returns {function} plugin class
* @static
*/
async function loadControlPluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.controlEntrypoint));
return entrypoint.ControlPlugin;
}
module.exports = {
loadPluginInfos,
loadMasterPluginClass,
loadInstancePluginClass,
loadControlPluginClass,
};
Loaded 100 of 385 files, more files were not shown because too many files have changed in this diff. Show more