Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rust-carbon-server

The modded Rust server image we run on Flux: pfeiffermax/rust-game-server with Carbon instead of Oxide, because on a Flux node Oxide cannot compile a plugin at all.

Same game, same runds.sh contract, same app spec shape. Published as runonflux/rust-carbon-server:latest.

Why it exists

On Flux nodes, every Oxide plugin install fails. Always the same way, about 25 seconds after the file lands:

[CSharp] Started Oxide.Compiler v successfully
[Error] Timed out waiting for plugin to be compiled: <PluginName>
[Warning] Compiler failed to gracefully shutdown, killing the process...

Oxide compiles plugins in a separate process, Oxide.Compiler, which it launches with Process.Start. On a node that process never runs. Traced at the kernel level on one of our own nodes, with ftrace and zero dropped events, the sequence is:

  1. Mono fork()s — a real process, clone_flags 0x1200011, preceded by three pipe2 calls.
  2. The child opens /proc/self/fd, reads it, and closes every inherited descriptor, 3 through 0x5e.
  3. Then it stops making syscalls and spins in user space at 100% CPU, forever: State: R, syscall: running, one thread, a 3.4 GB address space, and a user-space stack that never leaves a 27-byte window of libmonobdwgc-2.0.so — Mono's Boehm garbage collector.

It never reaches execve. The parent sees Process.Start return, logs "Started … successfully", opens its message-broker socket, and waits for a compiler that does not exist. Each attempt leaves a second RustDedicated burning a core until Oxide kills it 60 seconds later.

What it is not: the image, the Oxide version, the compiler binary (identical md5 on both sides), the app spec, the mounts, FluxOS, cgroup CPU throttling, the heap size, a pty, or the number of visible CPUs. All eliminated by measurement, including running the same image under a plain docker run with FluxOS stopped, which fails identically. The same image on a developer machine compiles the same plugin in 1.4 seconds.

Carbon does not have this problem, by construction. It compiles plugins in-process with Roslyn loaded as a library — carbon/managed/lib/Microsoft.CodeAnalysis.CSharp.dll — so nothing has to be spawned. uMod plugins keep working through Carbon.Hooks.Oxide.dll.

What this image adds

Three things, all forced by how a Flux app is shaped:

  • The Carbon runtime survives an empty volume. A Flux volume arrives empty and is mounted over /srv/rust/carbon, which would hide a runtime shipped at that path — exactly what happens to oxide/ on the Oxide image. So the tree is baked in at /opt/carbon-template, outside every mount, and seeded on first boot. Later boots refresh only managed/, tools/ and native/: plugins, configs, data and lang are the customer's and are never touched.
  • Uploaded plugins are loadable, without an exception in the log. Carbon rewrites the .cs files it loads — superseded copies go to backups/ — where Oxide only ever read them. FluxOS writes dashboard uploads as root, so without help a freshly uploaded plugin fails with Access to the path ... is denied and never loads. PID 1 stays root and hands the file to the game user on the inotify event itself, in single-digit milliseconds. That timing is the point: Carbon's own watcher opens an upload about 280 ms after it lands, so a periodic pass — the first version of this, every 5 s — always lost the race and printed a stack trace at the customer before loading the plugin seconds later. The periodic pass is still there underneath as the safety net.
  • docker stop reaches the game. The server runs as a child of PID 1 with SIGTERM forwarded, so a stop is a shutdown rather than a 10-second timeout and a kill.

The game itself still runs as uid 10001, the user the base image uses.

Build and test

./tests/test-flux.sh                                 # pure logic, no docker
docker build -t rust-carbon-server:local .
./tests/test-entrypoint.sh rust-carbon-server:local  # PID 1 against a stub server
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:stable -x scripts/*.sh tests/*.sh

Versions

Two upstreams, pinned differently because they version differently:

tag how it is followed
base image pfeiffermax/rust-game-server:build-NNNNNNNN immutable per Facepunch build; the workflow resolves the newest and bumps it
Carbon production_build a moving tag, republished in place; the version actually built is read from Carbon.Linux.Release.info and recorded as a label

.github/workflows/rebuild-on-new-release.yml runs daily and rebuilds when either one moves. Neither :latest nor Carbon's edge/experimental/rustbeta builds are ever used: a Flux app pinned to a rolling tag gets every customer's container rebuilt onto unreleased code.

App spec

The Carbon tree needs its own mount, the way the Oxide one does:

containerData: "g:/srv/rust/server/rustcarbon|m:carbon:/srv/rust/carbon"

Without the second mount the plugins live in the container's writable layer: they survive a restart and are erased by the next redeploy, and the dashboard's file browser cannot see them at all. Plugins go in carbon/plugins, their settings in carbon/configs.

About

Modded Rust dedicated server image for Flux Cloud Network: pfeiffermax/rust-game-server with Carbon instead of Oxide, because Oxide's out-of-process plugin compiler cannot start on a Flux node

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages