Commit Graph

35 Commits

Author SHA1 Message Date
Ludovic Marie 4513af8995 horizon-server: the lock test waits for a fork-held descriptor to close
Tests in the same binary spawn processes (tar, the engine); between fork and exec the child holds a copy
of every open descriptor, the lock's too, so relocking right after drop could see WouldBlock. Seen twice
on CI (#1833, #1895).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:13:36 +01:00
Ludovic Marie fb1f421117 Clippy: take the decorated region by value, and gate the unix-only test helper
receive_decorated moved the nine protos out of the box anyway (the
array iterator holds them inline), so taking [Proto; 9] and unboxing at
the three call sites costs the same and leaves boxed_local nothing to
flag on any target. The #[expect] failed on Linux, where the lint does
not fire, which is why verify was red.

scratch is only called from the #[cfg(unix)] tests, so it carries the
same cfg as installed() next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:32:55 +01:00
Alexander Krylov 961af30997 Clippy: clear the two warnings that the whole tree fails on
The update tests' scratch helper only feeds unix-gated tests, and
receive_decorated takes the box the queue already put the protos in.
2026-10-25 04:34:05 +03:00
Ludovic Marie 37c36b548a Merge main into docs/server-api-reference
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:09:34 +02:00
Ludovic Marie aee0b8b589 Format the endpoint table
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:07:26 +02:00
Ludovic Marie f80d00802e Server reference generated from horizon-server's source
docs/api/server-reference.md lists horizon-server's commands (its help
text), every server.toml key (config::TEMPLATE: default, the
server.properties key it writes, what it does) and the control API's
endpoints. Tests fail when the page is out of date (HORIZON_WRITE_DOCS=1
rewrites it), when the template and Config disagree on the keys, when a key
has no comment, or when api::ROUTES and the endpoints route() answers
differ.

api::ROUTES replaces the endpoint list in api.rs's module comment and the
hand-kept path list of the 405 arm. The template's uncommented keys get a
comment (what init writes for new servers; no setting changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:42:51 +02:00
Ludovic Marie ac43cdea70 launcher, server: one hidden-command helper per crate, server children keep its terminal
- horizon-bench reads the machine with horizon-telemetry's `command_output` (now public) instead
  of a second copy of it; the console flag lives in one place.
- horizon-server's `process::command` hides the console only when the server has none (started
  by the launcher). Run from a terminal, `schtasks`, `tar` and `mklink` share it as before, so
  their output still reaches it: a CREATE_NO_WINDOW child can't write to its parent's console.
- horizon-prepare's java/javac keep the stdin they had; only the console flag is new.
- launcher `process::hide_console` returns the command (no `let _ =` on other systems); the
  machine-details thread failing just leaves the report without them, as a slow one does.
- README: the Windows subsystem's effect on the command line (interactive cmd/PowerShell don't
  wait or set the exit code) under "Command line", the deferred probe under "Performance log".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:26:50 +02:00
RobertTGreat 1be8a4caea fix(launcher): defer hardware probes and hide Windows consoles
Collect performance-report hardware after the first frame and preserve logs, input and CLI pipes while suppressing background terminal windows.

Co-Authored-By: OpenAI Codex (GPT-6.1 Sol) <noreply@openai.com>
2026-10-24 18:34:08 +01:00
Ludovic Marie aa7f71a79b Merge pull request #1131 from ludovic111/feat/version-sources
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:24 +02:00
Ludovic Marie 30cdd6298a Version sources: a release build says its version (0.2.3), not 0.1.0 or a folder name
release.yml builds every game release with HORIZON_VERSION, the tag without its v
(docs/versioning.md), as launcher.yml already does for the launcher: no version-bump commits.

- horizon_core::release::{RELEASE, VERSION}: the built-in version, or Cargo.toml's for a build from
  source. (horizon_core::version stays Minecraft's, generated by gen_version_consts.py.)
- horizon-server and horizon-prepare --version, the server's /api/v1/status, horizon-ctl's MCP
  serverInfo and the "Horizon/<version>" user agents use it; they all said 0.1.0.
- The client's crash reports and telemetry say v0.2.3 from the built-in version wherever the game
  is installed; releases from before 0.2 (no version built in) still read the launcher's folder name.
- The launcher's benchmark telemetry used Cargo.toml's version instead of the release's.
- release.yml checks that horizon-prepare and horizon-server --version say the release's version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:20:13 +02:00
Ludovic Marie 0dd31c0757 horizon-server update: semver tags (v0.2.0) follow every date tag
From 0.2 Horizon's game releases are tagged vMAJOR.MINOR.PATCH (docs/versioning.md). newer() compared
tags number by number, so v0.2.0 looked older than v2026.10.19.15 and an installed server would never
offer or install it. Every semver tag now outranks every date tag; within semver, patch and minor
compare as numbers and a pre-release (v0.3.0-rc.1) comes before its release. Pruning old releases
sorts with the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:29:34 +02:00
Ludovic Marie 846f681b9a Signed updates: releases from before signing stay installable with pinned files; tests need a release key
- crates/horizon-release-sign/legacy-releases.txt pins the SHA-256 of every archive of the 131 releases published
  before signing (GitHub's upload digests, written by tools/ci/legacy_releases.py). The launcher and
  horizon-server update install those releases only with exactly those files, so rolling back to one works and
  nobody can swap its files later; every other release needs its signed SHA256SUMS.
- the_launcher_ships_with_a_release_key and horizon_server_ships_with_a_release_key fail while
  release-keys.txt lists no key, so this can't ship refusing everything.
- Test tags moved to v2099.* so they never collide with a pinned release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:20:36 +02:00
Ludovic Marie 36908858a3 Launcher and horizon-server update: install only releases whose signed checksums match
The launcher (game installs and its own updates) and `horizon-server update` fetch a release's
SHA256SUMS and SHA256SUMS.sig, check the signature against the keys built in from
crates/horizon-release-sign/release-keys.txt and the archive against the signed SHA-256, and
refuse anything unsigned, signed for another tag or by another key, or that doesn't match, with a
message saying why. Merge only once release-keys.txt lists the key and a signed stable release
exists: until then every download is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:20:36 +02:00
Ludovic Marie 386cb5203a horizon-server tests: settings and config no longer share a temp folder
a_stopped_server_saves_and_keeps_comments (settings.rs) and
writes_properties_and_syncs_back (config.rs) both used
horizon-server-config-<pid>, so when they ran at the same time one
removed or rewrote the other's horizon.toml and
writes_properties_and_syncs_back failed now and then. The settings test
now has its own folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:11:23 +02:00
Ludovic Marie c94b6a0972 Merge pull request #1009 from ludovic111/feat/server-config-command
🤖 Agent: merged after verify and CI parity passed (40 server tests; the whole create → start → join → whitelist → backup → restore → update → stop flow ran on the Mac mini and the ThinkPad). Adds `horizon-server config`, `backups --json`, `update --check --json`; fixes whitelisting on offline servers and updates as root in containers.
2026-10-19 16:21:56 +02:00
Ludovic Marie a4b7ac4a9b horizon-server update: unpack as the user, not the archive's owners
As root in a user namespace (an unprivileged LXC or rootless container), GNU tar
keeps the archive's owners by default and fails on IDs it can't map, so the update
stopped at "tar couldn't unpack". Found driving the launcher's Update now from one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:31:55 +02:00
Ludovic Marie 43287c18c2 horizon-server whitelist add: the offline UUID on a running offline server
The game resolves names as vanilla does, Mojang first, so on an offline server a name someone owns got that
account's UUID and the player, who joins under the offline UUID, was refused. horizon-server now writes the
offline entry and has the game reload the whitelist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:24:15 +02:00
Ludovic Marie 42bb310df9 horizon-server config: read and change server.toml's settings, live where vanilla can
`config [--json]` prints the settings; `config set KEY=VALUE...` writes them through
toml_edit, checked as server.toml is checked (nothing is written if one is wrong). A
running server takes the difficulty, the default game mode and the whitelist at once
through its console, and the backup settings because the supervisor reads them each
time; the rest waits for the next start, and the command says which.

Also `backups --json` and `update --check --json`, for the launcher's Servers page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:29:51 +02:00
Ludovic Marie ee3e874eb8 horizon-server: delete old releases after an update
An install kept every release it ever unpacked in releases/. Once the game is up on the
new release (or a stopped server is switched), the update now deletes the old ones,
keeping the release in use, the one `previous` names (rollback's target), the one the
running horizon-server came from, and the `updates.keep` newest of the others (1 by
default, a new server.toml setting). A release that can't be deleted is reported and
left for the next update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:50:27 +02:00
Ludovic Marie 3ce89ebb8a horizon-server update: report a switch without an install instead of staying busy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:31:08 +02:00
Ludovic Marie bf2c5e7946 horizon-server update: wait for the old game after a rollback; document updates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:30:21 +02:00
Ludovic Marie 857a88351f horizon-server update and rollback: switch releases, roll back by itself when the game doesn't come up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:21:26 +02:00
Ludovic Marie a8682eb4c5 horizon-server: give the game its folders whole when --dir is relative
`horizon-server run --dir srv` (what init prints) started the game in srv/ with
--control-socket srv/.horizon/engine.json, which the game resolved inside srv/,
so it crashed at every start. spawn now passes absolute paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:20:36 +02:00
Ludovic Marie 25f2e2396d Merge pull request #916 from ludovic111/feat/server-headless-build
🤖 Agent: reviewed; verify passes. Release dry run on this branch (run 39167763008): linux, macos, windows builds and publish all succeed; horizon-server-linux-x86_64.tar.gz (28 MB) built and import-checked (no libasound/libudev). Agent's proof: 880/879 client tests in both builds, dedicated_server/server_entities/lan on the server build, install → init → start → bot join on the ThinkPad without the libraries.
2026-10-18 23:42:28 +02:00
Ludovic Marie 519ec1217c Server build: the game without sound or controllers, so Linux servers need neither ALSA nor udev
horizon-client's cpal output and its controllers (gilrs, the DualSense's HID output) move behind the
audio and controllers features, on by default, so the game is unchanged. Built with
--no-default-features it links neither libasound.so.2 nor libudev.so.1. release.yml packages that
build as horizon-engine in horizon-server-linux-x86_64.tar.gz (xtask package-server, checked by
check_imports.py --server); install.sh installs it on Linux (and --archive installs a local one);
horizon-server already prefers horizon-engine. The Dockerfile uses it too, and CI lints it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:17:21 +02:00
Ludovic Marie 87cf8cb9e9 Merge pull request #904 from ludovic111/fix/server-log-lines
🤖 Agent: reviewed; verify passes. Refused logins name Server thread in latest.log; console, supervisor.log and the panel print local time.
2026-10-18 19:56:57 +02:00
Ludovic Marie bdc758c5e5 Server log: vanilla's thread in latest.log, and local time on the console
A dedicated server printed refused logins on its console as vanilla
does ("[Server thread/INFO]: Disconnecting <name> ..."), but wrote them
to logs/latest.log under the connection thread's own name
("[horizon-lan-conn/INFO]"): the file logger used the OS thread's name,
not the one vanilla logs the line from. The engine's lines now go to the
file under the same thread as on the console.

The console's times were UTC while logs/latest.log and vanilla's
console (Log4j's %d{HH:mm:ss}) use local time, so the two disagreed by
the time zone, as did the supervisor's own lines in
logs/supervisor.log and the web panel's console. Both now print local
time. Backup names keep their UTC time, as documented.

Found by the end-to-end install check on the Mac mini and the ThinkPad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:27:23 +02:00
Ludovic Marie 4ea2243e4d horizon-server on Linux: say which libraries the game needs, and follow updates
On a Linux without ALSA's library (most minimal server images), the
game exits at once with the loader's "error while loading shared
libraries: libasound.so.2". The supervisor took that for a crash and
restarted it five times over two and a half minutes before giving up
with "see logs/latest.log", which holds nothing about it.

Now:
- `run` starts the game with --help first (it loads no game files) and,
  if the system can't load it, stops at once with the loader's line and
  the packages to install on Debian/Ubuntu, Fedora, Arch and NixOS;
- `init` prints the same as a warning after "Game:";
- install.sh lists the libraries `ldd` can't find, with the same advice;
- docs/server.md says so in the install section.

Also, a service installed without the installer's launcher on the PATH
ran `<dir>/releases/<tag>/horizon-server`: Linux reports the running
program with links resolved, so the unit pinned that release and an
update never reached it. The fallback now goes through `<dir>/current`
while it points at that release.

Found by the end-to-end install check on the ThinkPad (NixOS, no
nix-ld libraries) with v2026.10.18.15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:16:14 +02:00
Ludovic Marie 44fba3bf9d horizon-server: find game files prepared without sounds
init and run looked for asset-index.json to recognise a prepared folder,
but horizon-prepare writes that file only with the optional sounds
(`horizon-prepare sounds`). A server prepared the usual way, without
sounds, was never found: init said "no prepared game files" and listed
the folder that held them. They are now recognised by
horizon-resources.json, the manifest every preparation writes and the
one the game itself reads.

Found by installing v2026.10.18.15 with install.sh on the Mac mini and
preparing from the release folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:50:52 +02:00
Ludovic Marie ae22d5dd96 Online mode: players named in commands are their accounts
An online-mode server checks players by their account's UUID, so op,
whitelist, ban and pardon now resolve a name as vanilla's
CachedUserNameToIdResolver does: a player on the server, then one seen
since the start (in memory; vanilla also keeps usercache.json), then
the services' profiles/getByName endpoint through the discovery
document. A name with no account is "That player does not exist", with
vanilla's "Couldn't find profile with name" warning. Offline servers
keep the offline profiles.

horizon-server does the same while the server is stopped (op,
whitelist add, and the players init writes), and removes entries by
the name already on the list. `init --offline` (or HORIZON_ONLINE=false)
sets online-mode off; init's note now warns about offline servers only.

The online-mode harness adds a sixth login: `whitelist add` for the
player who joined earlier needs no lookup and lets them in, and an
unknown name is looked up and refused, on vanilla's server and on
Horizon's (43 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:31:19 +02:00
Ludovic Marie 7f77772e15 horizon-server: the web panel
One page embedded in the binary (crates/horizon-server/panel: HTML, CSS and JS, no framework or CDN): status,
live TPS, MSPT, RAM and CPU graphs over the last 10 minutes, players with op and kick, the console, and
backups with restore, plus restart and stop.

It listens on [panel] bind, 127.0.0.1:8130 by default; a tailnet address reaches it from the owner's
devices, and any other address is refused unless allow_public is set. `horizon-server panel` prints a
one-time login link (10 minutes) that becomes an HttpOnly, SameSite=Strict session cookie. The Host header
must name the panel, every POST needs X-Horizon: 1, failed logins are rate-limited, and the page is served
with a strict Content-Security-Policy. The control API gains GET /api/v1/metrics and, with the token
only, POST /api/v1/panel/login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:18:46 +02:00
Ludovic Marie ed6af00ff8 horizon-server: install scripts, a Docker image, and boot services
- packaging/server/install.sh (Linux, macOS) and install.ps1 (Windows) install the latest release (or
  --version) from GitHub, check the SHA-256 GitHub publishes for it, and keep releases side by side:
  <dir>/current points at the one in use, so an update or a rollback is one link, and the game files
  prepared from the owner's Minecraft stay in <dir>/game-files across updates.
- packaging/server/Dockerfile builds the server and the engine; the prepared game files are mounted
  read-only, and the first start writes server.toml from HORIZON_EULA, HORIZON_WORLD, HORIZON_SEED,
  HORIZON_PORT, HORIZON_WHITELIST, HORIZON_PLAYERS and HORIZON_OP (`init` reads them too).
- `horizon-server service install|uninstall|print` writes and loads a systemd unit (user, or --system),
  a launchd agent, or a Windows task at logon. It runs the installer's launcher, so updates apply on
  the next restart.
- Releases build horizon-server and ship it beside horizon-ctl and horizon-prepare; PLAY.txt says how
  to run a server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:09 +02:00
Ludovic Marie 3510764cc6 horizon-server: backups, restore and automatic backups
`backup [NAME]` zips the world and the settings into backups/; on a running server it turns saving
off, flushes, zips and turns saving back on, so the game stays up. `restore BACKUP` puts a backup's
world in place and keeps the current one in backups/replaced-<time>/; a running server restarts with
it. `[backups] every = "6h"`, `keep = 12` in server.toml back up on a schedule and prune the
automatic backups only. The control API gains GET/POST /api/v1/backups and POST /api/v1/restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:09 +02:00
Ludovic Marie e8832190d1 horizon-server: start, stop, status, cmd and logs through a local control API
The supervisor serves a small HTTP API on 127.0.0.1 (a token in .horizon/api.json, mode 0600, and a
Host check): status, logs, console commands, stop and restart. `start` runs the server in the
background and returns once the game is up; `restart` and `cmd` wait for it too. A sampler reads the
game's tick rate, tick times, players and chunks every second over the existing control endpoint
(`--control-socket`), and RAM and CPU from the system.

The dedicated loop now records its tick times in the game clock, so `/tick query` reports real
numbers on a dedicated server, as it does in single player. The changes to game/dedicated.rs are
additive: the log capture for command replies, the tick counter and the control requests
(game/dedicated/status.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:09 +02:00
Ludovic Marie 9d21209ae8 horizon-server: server.toml, init, and a supervisor that runs the game
The first part of hosting a Horizon server with one small program
(docs/server.md). horizon-server is a new 1.3 MB binary with no window,
GPU, audio or controller code; it runs the game headless
(horizon-client --dedicated) as its engine.

- horizon-server-files: server.properties, eula.txt and the user lists,
  moved out of horizon-client unchanged so both programs share vanilla's
  exact reading and writing; horizon_client::server re-exports it.
- server.toml: typed settings for what owners change, [properties] for
  any other vanilla key, vanilla 26.3's defaults (a test checks them
  against ServerProperties). It is written into server.properties before
  every start; keys the game changes there itself (/whitelist on) are
  copied back afterwards, keeping the owner's comments (toml_edit).
  `import` turns a vanilla server.properties into server.toml.
- init: the EULA (only a yes writes eula=true), world, seed, port (checked
  free), whitelist and first operator; every question has a flag, so it
  also runs unattended.
- run: the console on stdin, a clean `stop` on Ctrl-C, SIGTERM or SIGHUP
  (the game runs in its own process group), and a restart when the game
  crashes: 5 s, doubling to 5 min, giving up after 5 crashes in 10 min.
  One supervisor per folder (a file lock).
- op, deop and whitelist for a stopped server, with vanilla's messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:09 +02:00