feat(activity): install on Windows + Windows release asset (nanoya-biz/rkd#1147) #11

Merged
dozken merged 2 commits from feat/1147-activity-windows-install into main 2026-09-28 14:41:58 +02:00
Owner

Summary

This is the Windows half of nanoya-biz/rkd#1147 on the plugin side. rkd-activity now serves and pairs on Windows over the named pipe, and install/uninstall register it as a per-user autostart in HKCU\…\Run. A v* tag also builds and attaches rkd-activity-<tag>-x86_64-pc-windows-msvc.tar.gz + .sha256.

This compiles and passes clippy (-D warnings, all targets, tests included) for x86_64-pc-windows-msvc, and it links with cargo-xwin, but it has never been run on Windows. Nobody here has a Windows machine. local/WINDOWS-CHECKLIST.md step 11 is how to check it.

Refs nanoya-biz/rkd#1147

What changed

  • serve/pair on Windows: server/pipe.rs is a copy of rkd-terminal's named-pipe transport. It serves \\.\pipe\rkd-plugins-<SID>-activity with an owner-only DACL, checks that each client is the same user, and refuses a second serve. cli.rs gets the same Windows connect / request_code / Ctrl+C handling as rkd-terminal. Before this PR, rkd-activity.exe only printed "not available on this platform". Game detection on Windows is still out of scope (#1122), so serve always reports that nothing is being played.
  • install: it writes the REG_SZ value rkd-activity = "<exe>" serve under HKCU\Software\Microsoft\Windows\CurrentVersion\Run. The path is always quoted, so a profile path with a space in it works. It then stops any serve already answering on the pipe, starts <exe> serve with DETACHED_PROCESS | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP (it also tries CREATE_BREAKAWAY_FROM_JOB and drops that flag if the job forbids breakaway), and waits until the new serve answers. Running it again sets the same value and restarts serve, so an upgraded binary takes over at once, the same as systemd's restart. --no-start only writes the value.
  • uninstall: it deletes the value, then ends the serve on the pipe. It finds that process with GetNamedPipeServerProcessId while holding a client handle, then calls TerminateProcess and waits for it to exit. Running it when nothing is installed is fine. The binary stays for the app to remove, as in the launchd/systemd flow.
  • Which executables install accepts: the same rules as unix. The path is canonical with \\?\ stripped. %TEMP% and cargo build folders are refused unless --force. A binary another user could replace is refused even with --force: the file and every folder above it must be owned by the user, SYSTEM, Administrators or TrustedInstaller, and must not grant anyone else DELETE, WRITE_DAC, WRITE_OWNER or GENERIC_WRITE/ALL. On folders FILE_DELETE_CHILD is also refused, and on the exe FILE_WRITE_DATA/APPEND_DATA. Inherit-only ACEs are ignored, so C:\ itself passes, but a folder created directly in C:\ fails because it inherits Authenticated Users' Modify.
  • Code layout: autostart.rs holds all the rules (Run value string, path choice, install/uninstall flow, ACL rule) as plain strings behind a System trait, and they are unit-tested on every host. autostart/win.rs holds the Win32 calls. build_dir_reason moved from service.rs to cli.rs so both platforms share it.
  • Dependency: windows-sys 0.61, the version rkd-local-link already uses. The lockfile gains one dependency edge and no new crate. I chose it over winreg because it was already in the tree and it also covers the pipe, process, console and DACL calls.
  • release.yml: a new build-windows job runs on ubuntu-24.04: rustup target add, apt clang lld llvm, cargo install --locked cargo-xwin --version 0.23.1 (built from source, as the app's CI does), then cargo xwin build --release -p rkd-activity. It checks the output is a PE32+ console binary that does not import vcruntime/msvcp, packages and uploads it, and publish now waits for it. rkd-terminal gets no Windows asset: its Windows build is experimental and nothing installs it (it would be a one-word change). install.sh stays unix-only.
  • Static CRT: the new .cargo/config.toml sets +crt-static for x86_64-pc-windows-msvc. Without it the exe imported VCRUNTIME140.dll and would fail to start on machines without the VC++ Redistributable.
  • package-release.sh: accepts <plugin>.exe for *-windows-* targets and only there. The archive keeps the unix layout: <name>/rkd-activity.exe, README.md and LICENSE.
  • Docs: local/README.md (new Windows section for rkd-activity), WINDOWS-CHECKLIST.md step 11, and the root README table.

Contract notes (for the app-side PR)

  • The archive member is rkd-activity-v<ver>-x86_64-pc-windows-msvc/rkd-activity.exe. It appears exactly once, is a regular file, and sits beside README.md/LICENSE like the unix assets, so the app's binary_member() needs .exe on Windows. I read the contract's "single regular-file member" as the one member the app extracts, not a one-entry tarball, because the unix assets have the same README/LICENSE entries.
  • A console program started from a Run value gets a console window at sign-in. To avoid that, serve calls FreeConsole when it is the only process on its console (GetConsoleProcessList == 1), so a window may flash briefly at sign-in. This is a mitigation, not a fix. A real fix would be a GUI-subsystem launcher or a consoleAllocationPolicy manifest (Windows 11 24H2+). Checklist step 11 asks the tester to note what happens.
  • serve started by install has no stdout/stderr (NUL), so nothing is logged on Windows. status shows whether it runs.
  • install makes its own std handles non-inheritable before it spawns serve. Otherwise serve would inherit the app's pipe to install's stdout and keep the app waiting for EOF.

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings (macOS host)
  • cargo clippy --target x86_64-pc-windows-msvc --workspace --all-targets --locked -- -D warnings (exactly as ci.yml; plain cross-clippy needed no C toolchain)
  • cargo test --workspace --locked (macOS): all green, including 11 new autostart tests (Run value quoting with spaces, UNC, refusals; plain canonical path choice; ancestors; ACL rule; install/restart/no-start/never-answers; uninstall idempotent and still removes the value when stopping fails; status) and the moved build_and_temp_dirs_are_named
  • cargo xwin build --release --locked -p rkd-activity -p rkd-terminal --target x86_64-pc-windows-msvc links; cargo xwin test --no-run -p rkd-activity links the unit tests and the new tests/pipe.rs
  • sh scripts/test-release.sh (shellcheck included): new packaging checks for the .exe member, refusal without .exe, and refusal of .exe for a non-Windows target. A hand-applied mutation of the member rule turned them red.
  • sh scripts/test-install.sh
  • Dry run of the new "Package and attach" steps on the locally built exe (listing below)
  • Anything on Windows (checklist step 11): not run, no Windows machine
  • The release job itself: only runs on a v* tag; not tagged (needs the owner's OK)

Evidence

rkd-activity --help, main (before) vs this branch (after):

     rkd-activity install [--no-start] [--force]
         Run this binary as a background service (a launchd agent on macOS, a
-        systemd user unit on Linux) that starts now and whenever you are
-        logged in. Refused when another user could replace the binary, even
-        with --force. Run it again after moving the binary.
+        systemd user unit on Linux, a Run value in the registry on Windows)
+        that starts now and whenever you are logged in. Refused when another
+        user could replace the binary, even with --force. Run it again after
+        moving the binary.
 ...
-game's name, Steam app id and start time, and never uses the network.
+game's name, Steam app id and start time, and never uses the network. On Windows
+the socket is a named pipe, and it runs and pairs but detects nothing yet.

On Windows, main's rkd-activity.exe (before) did nothing but print this, from main.rs on origin/main:

rkd-activity: not available on this platform; it detects games on Linux and macOS only

release.yml jobs and targets, before (main):

  create-release:
  build-linux:
        target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl]
  build-macos:
      TARGET: aarch64-apple-darwin
  publish:            needs: [create-release, build-linux, build-macos]

after (this branch):

  create-release:
  build-linux:
        target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl]
  build-macos:
      TARGET: aarch64-apple-darwin
  build-windows:
      TARGET: x86_64-pc-windows-msvc
  publish:            needs: [create-release, build-linux, build-macos, build-windows]

The new job's package step, run locally on the cargo-xwin build:

$ file rkd-activity.exe
PE32+ executable (console) x86-64, for MS Windows
$ llvm-objdump -p rkd-activity.exe | grep 'DLL Name'
    DLL Name: KERNEL32.dll
    DLL Name: api-ms-win-core-synch-l1-2-0.dll
    DLL Name: ntdll.dll
    DLL Name: bcryptprimitives.dll
    DLL Name: advapi32.dll
    DLL Name: bcrypt.dll
$ sh scripts/package-release.sh v0.8.0 x86_64-pc-windows-msvc rkd-activity.exe dist
$ tar -tvzf dist/rkd-activity-v0.8.0-x86_64-pc-windows-msvc.tar.gz
drwxr-xr-x  0 dozken staff       0 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/
-rwxr-xr-x  0 dozken staff 1499136 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/rkd-activity.exe
-rw-r--r--  0 dozken staff    6426 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/README.md

Before the +crt-static config, the same exe also imported VCRUNTIME140.dll and api-ms-win-crt-*.dll.

No version bump, tag or release in this PR.

https://claude.ai/code/session_01Bs7kRiXy79YjZrXDBuMabP

## Summary This is the Windows half of nanoya-biz/rkd#1147 on the plugin side. `rkd-activity` now serves and pairs on Windows over the named pipe, and `install`/`uninstall` register it as a per-user autostart in `HKCU\…\Run`. A `v*` tag also builds and attaches `rkd-activity-<tag>-x86_64-pc-windows-msvc.tar.gz` + `.sha256`. **This compiles and passes clippy (`-D warnings`, all targets, tests included) for `x86_64-pc-windows-msvc`, and it links with cargo-xwin, but it has never been run on Windows.** Nobody here has a Windows machine. `local/WINDOWS-CHECKLIST.md` step 11 is how to check it. Refs nanoya-biz/rkd#1147 ## What changed - **serve/pair on Windows:** `server/pipe.rs` is a copy of rkd-terminal's named-pipe transport. It serves `\\.\pipe\rkd-plugins-<SID>-activity` with an owner-only DACL, checks that each client is the same user, and refuses a second `serve`. `cli.rs` gets the same Windows `connect` / `request_code` / Ctrl+C handling as rkd-terminal. Before this PR, `rkd-activity.exe` only printed "not available on this platform". Game detection on Windows is still out of scope (#1122), so `serve` always reports that nothing is being played. - **install:** it writes the `REG_SZ` value `rkd-activity` = `"<exe>" serve` under `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`. The path is always quoted, so a profile path with a space in it works. It then stops any `serve` already answering on the pipe, starts `<exe> serve` with `DETACHED_PROCESS | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP` (it also tries `CREATE_BREAKAWAY_FROM_JOB` and drops that flag if the job forbids breakaway), and waits until the new `serve` answers. Running it again sets the same value and restarts `serve`, so an upgraded binary takes over at once, the same as systemd's `restart`. `--no-start` only writes the value. - **uninstall:** it deletes the value, then ends the `serve` on the pipe. It finds that process with `GetNamedPipeServerProcessId` while holding a client handle, then calls `TerminateProcess` and waits for it to exit. Running it when nothing is installed is fine. The binary stays for the app to remove, as in the launchd/systemd flow. - **Which executables `install` accepts:** the same rules as unix. The path is canonical with `\\?\` stripped. `%TEMP%` and cargo build folders are refused unless `--force`. A binary another user could replace is refused even with `--force`: the file and every folder above it must be owned by the user, SYSTEM, Administrators or TrustedInstaller, and must not grant anyone else DELETE, WRITE_DAC, WRITE_OWNER or GENERIC_WRITE/ALL. On folders `FILE_DELETE_CHILD` is also refused, and on the exe `FILE_WRITE_DATA`/`APPEND_DATA`. Inherit-only ACEs are ignored, so `C:\` itself passes, but a folder created directly in `C:\` fails because it inherits Authenticated Users' Modify. - **Code layout:** `autostart.rs` holds all the rules (Run value string, path choice, install/uninstall flow, ACL rule) as plain strings behind a `System` trait, and they are unit-tested on every host. `autostart/win.rs` holds the Win32 calls. `build_dir_reason` moved from `service.rs` to `cli.rs` so both platforms share it. - **Dependency:** `windows-sys` 0.61, the version `rkd-local-link` already uses. The lockfile gains one dependency edge and no new crate. I chose it over `winreg` because it was already in the tree and it also covers the pipe, process, console and DACL calls. - **release.yml:** a new `build-windows` job runs on ubuntu-24.04: `rustup target add`, apt `clang lld llvm`, `cargo install --locked cargo-xwin --version 0.23.1` (built from source, as the app's CI does), then `cargo xwin build --release -p rkd-activity`. It checks the output is a PE32+ console binary that does not import vcruntime/msvcp, packages and uploads it, and `publish` now waits for it. rkd-terminal gets no Windows asset: its Windows build is experimental and nothing installs it (it would be a one-word change). `install.sh` stays unix-only. - **Static CRT:** the new `.cargo/config.toml` sets `+crt-static` for `x86_64-pc-windows-msvc`. Without it the exe imported `VCRUNTIME140.dll` and would fail to start on machines without the VC++ Redistributable. - **package-release.sh:** accepts `<plugin>.exe` for `*-windows-*` targets and only there. The archive keeps the unix layout: `<name>/rkd-activity.exe`, README.md and LICENSE. - Docs: `local/README.md` (new Windows section for rkd-activity), `WINDOWS-CHECKLIST.md` step 11, and the root README table. ## Contract notes (for the app-side PR) - The archive member is `rkd-activity-v<ver>-x86_64-pc-windows-msvc/rkd-activity.exe`. It appears exactly once, is a regular file, and sits beside README.md/LICENSE like the unix assets, so the app's `binary_member()` needs `.exe` on Windows. I read the contract's "single regular-file member" as the one member the app extracts, not a one-entry tarball, because the unix assets have the same README/LICENSE entries. - A console program started from a Run value gets a console window at sign-in. To avoid that, `serve` calls `FreeConsole` when it is the only process on its console (`GetConsoleProcessList` == 1), so a window may flash briefly at sign-in. This is a mitigation, not a fix. A real fix would be a GUI-subsystem launcher or a `consoleAllocationPolicy` manifest (Windows 11 24H2+). Checklist step 11 asks the tester to note what happens. - `serve` started by `install` has no stdout/stderr (NUL), so nothing is logged on Windows. `status` shows whether it runs. - `install` makes its own std handles non-inheritable before it spawns `serve`. Otherwise `serve` would inherit the app's pipe to install's stdout and keep the app waiting for EOF. ## Test plan - [x] `cargo fmt --all --check` - [x] `cargo clippy --workspace --all-targets --locked -- -D warnings` (macOS host) - [x] `cargo clippy --target x86_64-pc-windows-msvc --workspace --all-targets --locked -- -D warnings` (exactly as ci.yml; plain cross-clippy needed no C toolchain) - [x] `cargo test --workspace --locked` (macOS): all green, including 11 new `autostart` tests (Run value quoting with spaces, UNC, refusals; plain canonical path choice; ancestors; ACL rule; install/restart/no-start/never-answers; uninstall idempotent and still removes the value when stopping fails; status) and the moved `build_and_temp_dirs_are_named` - [x] `cargo xwin build --release --locked -p rkd-activity -p rkd-terminal --target x86_64-pc-windows-msvc` links; `cargo xwin test --no-run -p rkd-activity` links the unit tests and the new `tests/pipe.rs` - [x] `sh scripts/test-release.sh` (shellcheck included): new packaging checks for the .exe member, refusal without .exe, and refusal of .exe for a non-Windows target. A hand-applied mutation of the member rule turned them red. - [x] `sh scripts/test-install.sh` - [x] Dry run of the new "Package and attach" steps on the locally built exe (listing below) - [ ] Anything on Windows (checklist step 11): not run, no Windows machine - [ ] The release job itself: only runs on a `v*` tag; not tagged (needs the owner's OK) ## Evidence `rkd-activity --help`, main (before) vs this branch (after): ```diff rkd-activity install [--no-start] [--force] Run this binary as a background service (a launchd agent on macOS, a - systemd user unit on Linux) that starts now and whenever you are - logged in. Refused when another user could replace the binary, even - with --force. Run it again after moving the binary. + systemd user unit on Linux, a Run value in the registry on Windows) + that starts now and whenever you are logged in. Refused when another + user could replace the binary, even with --force. Run it again after + moving the binary. ... -game's name, Steam app id and start time, and never uses the network. +game's name, Steam app id and start time, and never uses the network. On Windows +the socket is a named pipe, and it runs and pairs but detects nothing yet. ``` On Windows, main's `rkd-activity.exe` (before) did nothing but print this, from `main.rs` on origin/main: ``` rkd-activity: not available on this platform; it detects games on Linux and macOS only ``` release.yml jobs and targets, before (main): ``` create-release: build-linux: target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl] build-macos: TARGET: aarch64-apple-darwin publish: needs: [create-release, build-linux, build-macos] ``` after (this branch): ``` create-release: build-linux: target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl] build-macos: TARGET: aarch64-apple-darwin build-windows: TARGET: x86_64-pc-windows-msvc publish: needs: [create-release, build-linux, build-macos, build-windows] ``` The new job's package step, run locally on the cargo-xwin build: ``` $ file rkd-activity.exe PE32+ executable (console) x86-64, for MS Windows $ llvm-objdump -p rkd-activity.exe | grep 'DLL Name' DLL Name: KERNEL32.dll DLL Name: api-ms-win-core-synch-l1-2-0.dll DLL Name: ntdll.dll DLL Name: bcryptprimitives.dll DLL Name: advapi32.dll DLL Name: bcrypt.dll $ sh scripts/package-release.sh v0.8.0 x86_64-pc-windows-msvc rkd-activity.exe dist $ tar -tvzf dist/rkd-activity-v0.8.0-x86_64-pc-windows-msvc.tar.gz drwxr-xr-x 0 dozken staff 0 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/ -rwxr-xr-x 0 dozken staff 1499136 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/rkd-activity.exe -rw-r--r-- 0 dozken staff 6426 Sep 28 13:38 rkd-activity-v0.8.0-x86_64-pc-windows-msvc/README.md ``` Before the `+crt-static` config, the same exe also imported `VCRUNTIME140.dll` and `api-ms-win-crt-*.dll`. No version bump, tag or release in this PR. https://claude.ai/code/session_01Bs7kRiXy79YjZrXDBuMabP
rkd-activity now builds a working CLI on Windows: serve and pair over
the named pipe \\.\pipe\rkd-plugins-<SID>-activity (server/pipe.rs, a
copy of rkd-terminal's transport), status, and install/uninstall as a
per-user autostart. Detection stays Linux/macOS only (#1122), so
serve reports that nothing is played.

install writes HKCU\...\CurrentVersion\Run\rkd-activity =
"<exe>" serve, stops a serve already answering on the pipe, starts
serve detached with no console window and waits until it answers.
uninstall deletes the value and ends the serve on the pipe; the
binary stays for the app to remove, as on unix. Both are idempotent.

The executable rules match unix: the canonical path (without \\?\),
refused in %TEMP% or a cargo build folder unless --force, and refused
outright when the file or a folder above it is owned by or grants
delete/write/re-own rights to anyone but the user, SYSTEM,
Administrators or TrustedInstaller.

The rules live in autostart.rs as plain strings behind a System trait
and are unit-tested on every host; the Win32 calls (registry, pipe
server pid, TerminateProcess, DACL) are in autostart/win.rs through
windows-sys 0.61, which rkd-local-link already pulls in. build_dir_
reason moves from service.rs to cli.rs so both platforms share it.

Compiled and linted for x86_64-pc-windows-msvc, linked with
cargo-xwin, never run on Windows: WINDOWS-CHECKLIST.md step 11.

Claude-Session: https://claude.ai/code/session_01Bs7kRiXy79YjZrXDBuMabP
ci: Windows activity asset (nanoya-biz/rkd#1147)
All checks were successful
CI / windows-check (pull_request) Successful in 34s
CI / rust (pull_request) Successful in 51s
CI / install-script (pull_request) Successful in 14s
e4fec2ecb8
A v* tag now also builds rkd-activity for x86_64-pc-windows-msvc:
cross-built on the Linux runner with cargo-xwin (clang/lld from apt,
cargo-xwin 0.23.1 from source, as the RKD app's CI does) and attached
as rkd-activity-<tag>-x86_64-pc-windows-msvc.tar.gz + .sha256, the
same layout as the unix assets with the member rkd-activity.exe.
publish waits for it.

package-release.sh takes <plugin>.exe for a *-windows-* target and
only there. .cargo/config.toml links the C runtime statically for
that target, and the job fails if the exe imports vcruntime/msvcp:
the app runs it on machines without the VC++ Redistributable.

rkd-terminal gets no Windows asset: its Windows build is experimental
and nothing installs it. install.sh stays unix-only.

Claude-Session: https://claude.ai/code/session_01Bs7kRiXy79YjZrXDBuMabP
dozken merged commit 32ec83dcbb into main 2026-09-28 14:41:58 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rkd/plugins!11
No description provided.