wtclaude 96af2afa68
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m22s
feat(sidecar): start as a real Windows service
`sc.exe start RunicGatewayLink` failed with 1053 on every Windows install:
"a timeout was reached (30000 milliseconds) while waiting for the service to
connect", with SERVICE_EXIT_CODE 0. Nothing had crashed. The sidecar was a
plain console program, and the Windows service control manager only supervises
a process that calls StartServiceCtrlDispatcher and identifies itself within
~30 seconds.

The installer's design assumed symmetry with systemd, which supervises any
foreground process. Windows has no equivalent: it is a service-aware binary or
a shim, and a shim was already rejected as a third binary to keep current.

Split the entry point so the platform only owns starting and stopping:

  systemd --> main --> unix::run ---------------+
                                                +--> app::run
  SCM ------> main --> windows::run --> ServiceMain
                                    \-> console fallback

- app.rs is the whole sidecar, unchanged and shared. No #[cfg] on the data path.
- windows.rs speaks the SCM handshake. The dispatcher is tried first and failing
  is expected: ERROR_FAILED_SERVICE_CONTROLLER_CONNECT (1063) means "not started
  by the SCM" and falls through to a normal foreground run, so one binary does
  both with no --service flag to forget.
- Running is reported only once the shard port is bound and the store is open, so
  a bad config fails the start instead of flapping Running -> Stopped, and a
  failed run leaves a nonzero SERVICE_EXIT_CODE instead of the misleading 0.
- A service has no stdout, so service mode logs to uo-link-sidecar.log.<date>
  beside its config, rolled daily, seven kept.
- unix.rs additionally handles SIGTERM, which is what systemctl stop sends and
  which previously took the default disposition mid-write.

The Windows crates are declared under [target.'cfg(windows)'.dependencies].
Verified: a Linux build in rust:1-slim-bookworm succeeds and resolves neither
windows-service nor tracing-appender.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 13:37:28 -05:00

uo-link — Rust sidecar

The Rust sidecar half of the Runic Gateway bridge. The ServUO shard dials out to this sidecar over a loopback TCP socket (newline-delimited JSON); the sidecar owns the WebSocket + REST API the website consumes, along with auth, buffering, and fan-out.

ServUO plugin (C#, net48)  ──loopback TCP, newline-JSON──►  Rust sidecar  ──WebSocket/JSON──►  website
   (RunicGateway/servuo-plugins)                           >>> THIS REPO <<<

The shard never speaks WebSocket and exposes no port of its own — the sidecar is the only network-facing component, which is what keeps the game unreachable from the internet.

Repo What
thisRunicGateway/link The Rust sidecar (sidecar/).
RunicGateway/servuo-plugins The C# ServUO plugin — the shard side of the bridge (overlay/, patches/, deploy.ps1, test scaffolding).
RunicGateway/docs All project documentation — design docs, protocol spec, integration guide, research.

Layout

Path What
sidecar/ The Rust sidecar crate — terminates the loopback link to the shard, exposes WS + REST to the website. See sidecar/README.md.
.gitea/workflows/pr-checks.yml Gates every PR into main on cargo fmt --check, cargo clippy -D warnings, and cargo test.
.gitea/workflows/release.yml Builds + releases the sidecar binary (Linux + Windows) on every merge to main.

Build & run

The sidecar is a standard cargo crate:

cd sidecar
cargo build --release        # binary at target/release/uo-link-sidecar
cp sidecar.toml.example sidecar.toml   # then edit
cargo run --release

Deploying it rather than developing on it: --config <PATH> names the config file (as does $UOLINK_CONFIG), and --print-config prints the resolved settings — including the auth token the website needs — as JSON, provisioning the config file on first run. That is the supported way to read the token back; it is not meant to be scraped from the log.

uo-link-sidecar --print-config --config /etc/runicgateway/sidecar.toml

Running as a service

The same binary runs in the foreground and as a system service — there is no --service flag to remember, because the process can tell how it was started.

  • Linux/systemd supervises any foreground process, so the unit just runs the binary. SIGTERM (what systemctl stop sends) and SIGINT both unwind it cleanly; logs go to the journal.
  • Windows cannot. The service control manager only supervises a process that connects back to it within ~30 seconds via StartServiceCtrlDispatcher; a plain console program registered with sc.exe create is killed with error 1053 despite running perfectly. So on Windows the sidecar speaks that handshake: started by the SCM it runs as a service, started from a shell the connect fails with ERROR_FAILED_SERVICE_CONTROLLER_CONNECT and it falls through to an ordinary foreground run. It reports Running only once the shard port is bound and the store is open, and — having no console — logs to uo-link-sidecar.<date>.log beside its config, rolled daily.

Only the starting and stopping is platform-specific: src/app.rs is the entire sidecar and is shared, while src/windows.rs and src/unix.rs do nothing but start it and tell it when to stop. The Windows crates are declared under [target.'cfg(windows)'.dependencies], so Cargo neither resolves nor builds them for a Linux target.

Registering the service is the installer's job; to do it by hand see INSTALL.md Appendix A4.

.gitea/workflows/release.yml cross-compiles Linux + Windows binaries and cuts a Gitea release on every merge to main (conventional-commit versioning). See sidecar/README.md for configuration and the wire protocol.

Before that, .gitea/workflows/pr-checks.yml runs the same gates on every pull request into maincargo fmt --check, cargo clippy --all-targets -- -D warnings, then cargo test --locked. Run them locally before pushing and the PR will be green:

cd sidecar
cargo fmt                                        # or --check to just report
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked

Deployment & compatibility

The plugin (RunicGateway/servuo-plugins) and this sidecar are deployed together but built independently:

  • The plugin is deployed as source into the ServUO server root and compiled by ServUO at boot — no build artifact, no CI build.
  • The sidecar is a standalone Rust binary released from this repo.

The only coupling is the loopback JSON protocol (the shard dials 127.0.0.1). Compatibility is a protocol concern, not a build-order one — keep the event/command catalog in sync across the two repos. Canonical spec: PLAN.md §5/§7 and INTEGRATION.md. Because a wedged or absent sidecar cannot stall the shard, either side can be deployed or restarted independently.


License

Runic Gateway is free software, licensed under the GNU General Public License v3.0 or later — see LICENSE.md.

Copyright (C) 2026 Runic Gateway

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version. It is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

Contributions are welcome — please read CONTRIBUTING.md (note the AI-usage disclosure requirement) and our Code of Conduct. Report vulnerabilities privately per SECURITY.md.

Description
No description provided
Readme 1 MiB
v1.2.1 Latest
2026-08-07 19:17:11 +00:00
Languages
Rust 98.7%
Python 1.3%