runhelper
- 9 Devlogs
- 15 Total hours
A pair of programs that allows users to manage a game server running on their personal computers through another server.
A pair of programs that allows users to manage a game server running on their personal computers through another server.
To get ready to ship, I finished the readmes to include instructions on how to run the project and published binaries on its Github page. While setting up a demo server, I realised the webapp would not show the port of the server, which matters if there are multiple servers running at the same time. I added a port field to the ServerInfo struct and implemented finding it from GAME_ARGS and the game server’s config file for each game server.
I finished implementing the mod resolver for tModLoader. My first thought was to read the .twld file; since it’s generated by tModLoader, it probably held information about the mods. I couldn’t find docs for its file format, so I first used the file tool, which showed that it was a gzipped file. I decompressed the file and tried to see if it was a text format or simple binary format by opening it in a hex editor. It wasn’t though, so I looked harder at the tModLoader docs, and found that it simply saved the WorldFileData to a TagCompound, which is serialized as an NBT.
I then had the problem of storing the world path within the Terraria struct, since it would require a special argument on the server_info function that other implementations wouldn’t need. I thought about creating a new trait that would be passed to server_info, but I realised I could use the V: Variant and detect the world path from the config, since V already is generic. This required changing the trait bound from Copy to Clone, since the ServerType would have to hold a PathBuf which does not implement Copy.
I finally used the Steamworks Web API to search the Steam Workspace, where mods are uploaded, to find info like the icon url, whether it’s client or server sided, etc.
I started by trying to add support for the Satisfactory Dedicated Server, so I created a module for it in routes::start, where the other game server impls are. I saw that the start module directory was starting to get big, so I moved its contents to a new module at crate::games. Because Satisfactory does not have any server variants, I initially implemented Variant for (), returning None in its fn detect, and used () as its variant. After testing it, I realised that detect returning None would make the server not runnable, since it’s supposed to detect if there is a server at a given path. I removed the impl for () and created a single variant enum instead. I implemented fn run by running FactoryServer.exe/FactoryServer.sh with the arg -unattended and fn server_info by inspecting the exe’s product_version. I was looking into how to gracefully stop a Satisfactory server, and realised that the GameServer trait does not require a fn stop to be defined, and that it had handled stopping in the routes::stop function, matching on the ServerType. I added fn stop to the GameServer trait, and moved the impls to the respective game impls. I also saw that in crate::tasks::shutdown, it only sent /stop (only working for Minecraft) and then force-killed the process. I replaced this with running the stop implementation. For Satisfactory, stop had to send SIGINT on Linux and CtrlC on Windows. I used libc::kill for Linux, and GenerateConsoleCtrlEvent for Windows. Since I already needed the crates libc and windows-sys for this, I replaced the existing force_kill implementation that ran CLI commands with libc::kill and TerminateProcess. Now that GameServer was fully implemented for Satisfactory, I tried starting the game. It opened a visible console window, even with different args. To make it run without a window, I looked into creation flags like CREATE_NO_WINDOW, but that didn’t work. The wiki said that to run the server as a service, I would have to use NSSM, but that requires administrator privileges. I then looked at Task Manager and saw that FactoryServer.exe seemed to only run FactoryServer-Win64-Shipping-Cmd.exe with the arguments FactoryGame -unattended. So, I ran it with those arguments, and the server would now start without creating a console window.
I added support for vanilla minecraft servers, refactoring some functions that ended up useful for both paper and vanilla.
I finished a todo on ensuring the different game server implementations are and stay consistent. I did this by defining a common GameServer trait that takes a generic Variant type; the Variant type needs to be generic because different games may have different server variants. The GameServer trait requires fn spawn and fn server_info to be defined and provides fn run that calls spawn and server_info, setting the AppState’s server info with the result. This ended up making the respective mod.rss more concise and deduplicating some logging code.
I checked again and saw that the sockets still weren’t being cleaned up, looked through the helper routes again, and saw that the ws handler was infinitely looping even after it failed to send, causing the socket to not be released. I removed the loop and the fd count seems to be stable.
I fixed an issue where socket fds were leaked, which caused the helper to eventually stop serving the static webapp due to an fd limit. I thought I fixed it by closing the websocket after it stopped being able to receive, but this would only happen if the websocket was already closed by the runner, meaning the call to close the websocket would always fail. I originally thought that because I reconnected in a loop, the websocket object would get dropped per iteration, but maybe due to async, it didn’t. So, to ensure the OS socket is closed, I manually drop the websocket object after recv fails.
Since the console route is unauthenticated, I had a filter that censors numbers to hide things like IPs. However, a person should at least be able to see what players there are, using the /list command. So, the filter used to check if a line contains a specific string that /list outputs, but this could allow players to type that string to bypass the filter. I first fixed this with a regex, ^.+]: There are, but my benchmarks showed that it took 100x longer than the original. I finally fixed it by splitting the line by ] and checking if specific indexes (depending on the server type) start with the string. This solution takes around the same time as the original (avg ~1μs).
I noticed that after adding a certain plugin to a Minecraft server, the runner’s stdout reader would stop abruptly, with only a log saying the reader stopped. I realised that when it encountered an error, I made it stop without logging the actual error. After making it log the error, I saw that it stopped due to the output not being UTF-8. I fixed this by replacing my use of BufReader’s .lines() and reading the stdout to a Vec<u8>, converting to a string lossily, so that it handles non-UTF-8 output.
I also knew that the plugin/mod resolver was not very robust, not finding the actual plugin/mod most times. I improved this by reading the author info from the plugin/mod metadata with a more accurate data structure, eg. accounting for the authors field to be a string or a list of strings, using the author to compare with search results to ensure the correct result is selected. I also now try both the reported plugin/mod name and the name from its filename.