Skip to content

feat(replayos): add RePlayOS platform integration#692

Merged
wizzomafizzo merged 6 commits into
mainfrom
feat/replayos-integration
Apr 18, 2026
Merged

feat(replayos): add RePlayOS platform integration#692
wizzomafizzo merged 6 commits into
mainfrom
feat/replayos-integration

Conversation

@wizzomafizzo

@wizzomafizzo wizzomafizzo commented Apr 17, 2026

Copy link
Copy Markdown
Member
  • Add pkg/platforms/replayos platform implementation targeting RePlayOS (Raspberry Pi 4, Debian 13, libretro). Game tracking polls /proc/{pid}/maps for loaded cores every 2s and watches _recent/ via inotify for games launched from the RePlayOS menu.
  • Add screenshot capture using the RePlayOS keyboard hotkey (s), with Caps Lock toggling to temporarily disable Keyboard Real Mode when needed.
  • Add systemd service with resource scheduling to ensure Zaparoo never wins CPU/IO contention against replay: Nice=10, CPUWeight=10, CPUAffinity=0-2, IOSchedulingClass=best-effort prio 7, IOWeight=10, OOMScoreAdjust=500. Also sets GOMAXPROCS(2) in the daemon.
  • Add cmd/replayos entry point with TUI, daemon, install, and uninstall modes matching other platform binaries.
  • Add scripts/tasks/replayos.yml with build-arm64 and deploy tasks.
  • Refactor PNG/BMP screenshot completeness checks (IEND tail, BMP header size) into pkg/platforms/shared/screenshot.go, shared by MiSTer and RePlayOS.
  • Add ReplayOS platform ID constant and unrestricted input mode default.
  • Add RePlayOS to scripts/install.sh.

Summary by CodeRabbit

  • New Features

    • ReplayOS platform support: ROM launching across multiple storages, screenshot capture, game tracking, and system service install/uninstall.
    • Build & deploy: ARM64 build & CI matrix entry plus tasks to build and deploy to ReplayOS devices.
  • Refactor

    • Shared screenshot completion checks extracted for reuse.
  • Docs

    • Platform documentation link added for ReplayOS.
  • Tests

    • Extensive ReplayOS unit and integration tests added.

- Add ReplayOS platform implementation with game tracking via /proc maps polling and inotify on _recent/ for menu-launched games
- Add screenshot support using keyboard hotkey sequence with Caps Lock toggle to handle Keyboard Real Mode
- Add systemd service with scheduling hardening (Nice=10, CPUWeight=10, CPUAffinity=0-2, IOSchedulingClass=best-effort, OOMScoreAdjust=500) to yield resources to replay
- Add GOMAXPROCS(2) cap in daemon mode to leave CPU headroom for replay
- Add ReplayOS build and deploy tasks
- Refactor screenshot completeness checks (IEND/BMP header) into shared package used by both MiSTer and ReplayOS
- Add platform ID constant and input mode support for ReplayOS
- Add install.sh support for ReplayOS
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a Linux ReplayOS platform: platform implementation (storage, autostart, launch), game tracker, screenshot support, system map, systemd unit and installer/CI/deploy tasks, shared screenshot helpers, tests, and input default adjustments for ReplayOS.

Changes

Cohort / File(s) Summary
Platform core & tests
pkg/platforms/replayos/platform.go, pkg/platforms/replayos/platform_test.go
New exported Platform type with storage detection, StartPre/StartPost/Stop, RootDirs, Settings, LaunchMedia/autostart logic (writes autostart + restarts replay.service), launchers, and extensive unit tests.
Tracker & tests
pkg/platforms/replayos/tracker.go, pkg/platforms/replayos/tracker_test.go
Background tracker that discovers Replay PID, inspects loaded core via /proc, watches .rec recent files to resolve ROM paths, maps core/ROM→system IDs, and updates active media; tested with proc/FS fixtures.
Screenshots (ReplayOS) & tests
pkg/platforms/replayos/screenshot.go, pkg/platforms/replayos/screenshot_test.go
Screenshot flow: trigger keyboard sequence (real-mode handling), poll captures/{system}/ for new complete PNGs, validate completeness, return image bytes; includes keyboard sequencing and timeout tests.
System map & tests
pkg/platforms/replayos/systemmap.go, pkg/platforms/replayos/systemmap_test.go
Defines folder→SystemInfo mapping (SystemID, optional LauncherID, Extensions) and GetLauncherID behavior; unit tests for fallback/override.
Systemd unit & CLI
cmd/replayos/conf/zaparoo.service, cmd/replayos/main.go
New zaparoo.service unit file and a Linux-only CLI entrypoint supporting install/uninstall/daemon/TUI modes, service install/uninstall flows, runtime daemon/TUI operation, signal handling, and re-exec-on-restart semantics.
Shared screenshot helpers & refactor
pkg/platforms/shared/screenshot.go, pkg/platforms/shared/screenshot_test.go, pkg/platforms/mister/screenshot.go
Extracted PNG/BMP completion checks into shared package; tests made platform-agnostic; MiSTer screenshot now delegates to shared.ScreenshotFileComplete.
Build, tasks & CI
.github/workflows/build.yml, Taskfile.dist.yml, scripts/tasks/replayos.yml, scripts/tasks/utils/makezip/main.go
Added ReplayOS ARM64 CI matrix row and build invocation, included replayos Taskfile fragment, new build-arm64/deploy tasks, and docs mapping for ReplayOS.
Installer & scripts
scripts/install.sh, scripts/tasks/replayos.yml
Installer detects ReplayOS and adds install_replayos(); Taskfile deploy copies binary to /media/sd/zaparoo, manages zaparoo.service, and supports DRY_RUN handling.
Platform registry & input defaults
pkg/platforms/ids/ids.go, pkg/zapscript/input.go, pkg/zapscript/input_test.go
Added ReplayOS platform ID and include it in platforms that default to InputModeUnrestricted; minor comment clarifications and test update.
CLI/packaging include & service file
Taskfile.dist.yml, cmd/replayos/conf/zaparoo.service
Taskfile includes replayos tasks; new systemd unit file placed under CLI conf.

Sequence Diagram

sequenceDiagram
    participant User
    participant TUI
    participant Platform
    participant Autostart as Autostart File
    participant Replay as replay.service
    participant Tracker
    participant Core as Emulator Core
    participant RecFile as .rec File

    User->>TUI: select ROM to launch
    TUI->>Platform: LaunchMedia(romPath)
    Platform->>Platform: validate ROM under active storage
    Platform->>Autostart: write relative /roms/... autostart
    Platform->>Replay: systemctl restart replay.service
    Note over Replay: service boots and loads emulator core
    Replay->>Core: load ROM

    par Monitoring & recent-file
        Tracker->>Replay: query replay MainPID
        Tracker->>Tracker: inspect /proc/... -> loaded core
        Tracker->>RecFile: watch for .rec creation
        RecFile-->>Tracker: .rec created (contains ROM path)
    end

    Tracker->>Tracker: map ROM folder/core to SystemID
    Tracker-->>TUI: report ActiveMedia (system, ROM, core)
    TUI->>User: display running game
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through mounts where roms abide,
I wrote an autostart and nudged the ride.
Cores wake, screenshots flash, trackers peek and sing—
systemd hums, the game begins to spring.
Hooray! — a rabbit's cheer for ReplayOS bring. 🎮✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(replayos): add RePlayOS platform integration' accurately summarizes the main change—adding a new RePlayOS platform implementation with all supporting infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/replayos-integration

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread pkg/platforms/replayos/platform.go Outdated
@sentry

sentry Bot commented Apr 17, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
pkg/platforms/shared/screenshot.go (1)

35-44: Consider adding JPEG support for forward compatibility.

The dispatcher currently returns an error for unsupported formats. If RePlayOS or future platforms emit JPEG screenshots, this would fail unexpectedly. Consider either:

  • Adding a basic JPEG completion check (SOI/EOI markers)
  • Returning (true, nil) for unknown formats with a log warning, allowing the caller to proceed

This is optional given the current PNG/BMP scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/shared/screenshot.go` around lines 35 - 44, The
ScreenshotFileComplete function currently only handles PNG and BMP; add basic
JPEG support by handling ".jpg" and ".jpeg" in ScreenshotFileComplete and
calling a new helper JPEGFileComplete(path) that opens the file and checks JPEG
SOI (0xFF 0xD8) at the start and EOI (0xFF 0xD9) at the end, returning
(true,nil) if both markers are present and (false,error) on I/O or marker
mismatch; ensure JPEGFileComplete's errors are propagated and update imports if
needed (e.g., os/io) so callers won't fail for JPEG screenshots.
scripts/install.sh (1)

500-541: Consider adding error handling for mkdir -p.

Line 524's mkdir -p /media/sd/zaparoo could fail silently on permission issues before the cp fails with a less clear error.

Proposed fix
     # Install binary
     info "Installing to /media/sd/zaparoo/..."
-    mkdir -p /media/sd/zaparoo
+    if ! mkdir -p /media/sd/zaparoo; then
+        abort "Failed to create /media/sd/zaparoo directory"
+    fi
 
     if ! cp "${ZAPAROO_BIN}" /media/sd/zaparoo/zaparoo; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install.sh` around lines 500 - 541, In install_replayos, add error
handling around the directory creation step: after calling mkdir -p
/media/sd/zaparoo check its exit status and abort with a clear message
(including the command error or $?) if it fails, so that mkdir failures (e.g.,
permissions) are reported before the subsequent cp; update the mkdir invocation
in install_replayos to perform this check and call abort "Failed to create
/media/sd/zaparoo" (or similar) on failure.
pkg/platforms/replayos/screenshot.go (1)

83-107: Consider logging the best-effort Caps Lock restoration attempt.

Line 94 silently discards the error from the best-effort Caps Lock restoration. While this is intentional (as indicated by the comment), adding a trace-level log would aid debugging if users report issues with keyboard state after failed screenshots.

🔧 Optional: Add trace logging for best-effort restoration
 	if err := p.KeyboardPress("s"); err != nil {
 		if p.keyboardRealMode {
 			// Best-effort: try to restore Real Mode even after the screenshot key failed.
-			_ = p.KeyboardPress("{capslock}")
+			if restoreErr := p.KeyboardPress("{capslock}"); restoreErr != nil {
+				log.Trace().Err(restoreErr).Msg("best-effort capslock restoration also failed")
+			}
 		}
 		return fmt.Errorf("send screenshot key: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/screenshot.go` around lines 83 - 107, In
triggerScreenshot on Platform, replace the silent discard of the best‑effort
Caps Lock restoration (the `_ = p.KeyboardPress("{capslock}")` inside the error
branch after sending the "s" key) with a trace‑level log that records the error;
call the existing logger (e.g. log.Trace().Err(err).Msg("best-effort restore
keyboard real mode after failed screenshot") ) so the attempt remains non‑fatal
but the failure is visible for debugging.
pkg/platforms/replayos/platform_test.go (1)

301-351: Add test for successful launch path in TestLaunchGame.

The TestLaunchGame only tests error cases. A test verifying the happy path (ROM on active storage successfully launching) would improve coverage, though it requires mocking restartReplayService() or restructuring for testability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform_test.go` around lines 301 - 351, Add a "happy
path" subtest inside TestLaunchGame that sets up a temp storage with mkRoms,
sets p := newTestPlatform(), p.activeStorage and p.storagePaths to that storage,
and uses a way to stub/mock restartReplayService on Platform so it does not
actually restart (e.g., set a replaceable method or inject a no-op before
calling launchGame); call p.launchGame(nil, romPath, nil) where romPath is under
the active storage and assert no error and expected return values (e.g., non-nil
launch info or correct resolved path). Ensure the test cleans up with defer
p.cancel() and runs t.Parallel() like the others.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/replayos/main.go`:
- Around line 179-220: The two user-facing prints in installService and
uninstallService (the fmt.Println calls) violate the guideline to use zerolog;
replace them with zerolog info-level logs (e.g., log.Info().Msg("...")) and
remove the fmt dependency from this file, ensuring you import
github.com/rs/zerolog/log; apply the same change pattern to other platform
entrypoints mentioned (functions in cmd/mistex/main.go and cmd/batocera/main.go)
so all CLI confirmation output uses zerolog rather than fmt.Println, or if you
intentionally allow prints, add an explicit documented exemption to the coding
guidelines and reference it in these functions.

In `@pkg/platforms/replayos/platform.go`:
- Around line 257-284: The Generic `.sh` launcher in the Launchers method uses
context.Background(), so launched commands won't be cancelled with the platform;
update the closure to capture the receiver and call exec.CommandContext with the
platform's context (e.g., use p.ctx or the platform's context field/method)
instead of context.Background(), keeping the rest of the closure (error wrap and
nil process return) intact; locate this in the Launchers method where the
Generic launcher closure is defined and replace context.Background() with the
platform context.
- Around line 379-419: detectStorages can return an active mount (mount) that
wasn’t detected in all (no roms/), causing active not to be in storagePaths;
after resolving mount via tokenMap[token] in detectStorages, validate that mount
is present in the detected list (all) and if it is not, treat it like an unknown
token: if all has entries, warn and return all[0] as fallback, otherwise return
an error—use symbols token, mount, all, tokenMap, and the function
detectStorages/readStorageToken to locate and implement this check.

In `@pkg/platforms/replayos/tracker.go`:
- Around line 187-207: The file lacks tests for findReplayChild because it
directly calls os.ReadFile and isReplayBinary, making it hard to stub; refactor
to introduce package-level variables (e.g., var readFile = os.ReadFile and var
isReplayBinaryFunc = isReplayBinary) and update findReplayChild to call
readFile(...) and isReplayBinaryFunc(...), then add a _test.go that replaces
readFile and isReplayBinaryFunc with test stubs to simulate: (1) a children file
containing a replay PID and isReplayBinaryFunc returning true for that PID
(assert returned PID), and (2) children with no replay PID or malformed fields
(assert error). Ensure tests restore the originals after each case.

---

Nitpick comments:
In `@pkg/platforms/replayos/platform_test.go`:
- Around line 301-351: Add a "happy path" subtest inside TestLaunchGame that
sets up a temp storage with mkRoms, sets p := newTestPlatform(), p.activeStorage
and p.storagePaths to that storage, and uses a way to stub/mock
restartReplayService on Platform so it does not actually restart (e.g., set a
replaceable method or inject a no-op before calling launchGame); call
p.launchGame(nil, romPath, nil) where romPath is under the active storage and
assert no error and expected return values (e.g., non-nil launch info or correct
resolved path). Ensure the test cleans up with defer p.cancel() and runs
t.Parallel() like the others.

In `@pkg/platforms/replayos/screenshot.go`:
- Around line 83-107: In triggerScreenshot on Platform, replace the silent
discard of the best‑effort Caps Lock restoration (the `_ =
p.KeyboardPress("{capslock}")` inside the error branch after sending the "s"
key) with a trace‑level log that records the error; call the existing logger
(e.g. log.Trace().Err(err).Msg("best-effort restore keyboard real mode after
failed screenshot") ) so the attempt remains non‑fatal but the failure is
visible for debugging.

In `@pkg/platforms/shared/screenshot.go`:
- Around line 35-44: The ScreenshotFileComplete function currently only handles
PNG and BMP; add basic JPEG support by handling ".jpg" and ".jpeg" in
ScreenshotFileComplete and calling a new helper JPEGFileComplete(path) that
opens the file and checks JPEG SOI (0xFF 0xD8) at the start and EOI (0xFF 0xD9)
at the end, returning (true,nil) if both markers are present and (false,error)
on I/O or marker mismatch; ensure JPEGFileComplete's errors are propagated and
update imports if needed (e.g., os/io) so callers won't fail for JPEG
screenshots.

In `@scripts/install.sh`:
- Around line 500-541: In install_replayos, add error handling around the
directory creation step: after calling mkdir -p /media/sd/zaparoo check its exit
status and abort with a clear message (including the command error or $?) if it
fails, so that mkdir failures (e.g., permissions) are reported before the
subsequent cp; update the mkdir invocation in install_replayos to perform this
check and call abort "Failed to create /media/sd/zaparoo" (or similar) on
failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7331f6b-8a6f-412b-8ea6-0cadf95021e3

📥 Commits

Reviewing files that changed from the base of the PR and between f2dbc7f and 1cc5f20.

📒 Files selected for processing (20)
  • .github/workflows/build.yml
  • Taskfile.dist.yml
  • cmd/replayos/conf/zaparoo.service
  • cmd/replayos/main.go
  • pkg/platforms/ids/ids.go
  • pkg/platforms/mister/screenshot.go
  • pkg/platforms/replayos/platform.go
  • pkg/platforms/replayos/platform_test.go
  • pkg/platforms/replayos/screenshot.go
  • pkg/platforms/replayos/screenshot_test.go
  • pkg/platforms/replayos/systemmap.go
  • pkg/platforms/replayos/tracker.go
  • pkg/platforms/replayos/tracker_test.go
  • pkg/platforms/shared/screenshot.go
  • pkg/platforms/shared/screenshot_test.go
  • pkg/zapscript/input.go
  • pkg/zapscript/input_test.go
  • scripts/install.sh
  • scripts/tasks/replayos.yml
  • scripts/tasks/utils/makezip/main.go

Comment thread cmd/replayos/main.go
Comment thread pkg/platforms/replayos/platform.go
Comment thread pkg/platforms/replayos/platform.go
Comment thread pkg/platforms/replayos/tracker.go
… active mount detection

- deleteAutostart: restructure to only log "deleted autostart file" when
  os.Remove succeeds; ErrNotExist is silently ignored, other errors warn.
  Both log entries now include the file path.
- detectStorages: after resolving a valid system_storage token, verify the
  resolved mount is actually in the detected list (has a roms/ directory).
  If it is not, warn and fall back to the first detected mount, matching
  the behaviour of the unknown-token and missing-config paths.
- screenshot: log best-effort Caps Lock restoration failures at trace
  level instead of discarding the error silently.
- install.sh: abort with a clear message if mkdir -p /media/sd/zaparoo
  fails, so permissions errors are not masked by the subsequent cp failure.
…and screenshot

Add command.Executor, procPath, and clockwork.Clock injection fields to
Platform to enable hermetic testing of systemctl calls, /proc reads, and
time-dependent key sequences. Convert restartReplayService, getReplayPID,
isReplayBinary, findReplayChild, getLoadedCore, and healthCheck to methods
that use these fields.

Add tests for SystemMap.GetLauncherID, Platform trivial returns, RootDirs,
Launchers, deleteAutostart, StopActiveLauncher, restartReplayService,
launchGame success path, isReplayBinary, findReplayChild, getLoadedCore,
getReplayPID, checkAndUpdateRunningGame, startGameTracker shutdown,
startRecentWatcher, waitForScreenshot, and triggerScreenshot key sequences
including capslock-wrap and error recovery paths.

Add PNGFileComplete and BMPFileComplete missing-file error tests in
pkg/platforms/shared.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/platforms/replayos/platform.go (1)

407-529: Thread the ReplayOS filesystem helpers through afero.Fs.

detectStorages, readStorageToken, readRealMode, writeAutostart, and deleteAutostart are all new testable helpers, but they still hard-wire os.* calls. Please move them behind an injected afero.Fs like the other new seams in this platform so they can be exercised hermetically.

As per coding guidelines, "pkg/**/*.go: Use afero for filesystem operations in testable code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 407 - 529, The helper
functions detectStorages, readStorageToken, readRealMode, writeAutostart, and
deleteAutostart currently call os.* directly; change them to accept an afero.Fs
(or use a struct field that holds afero.Fs) so all filesystem ops use afero
(replace os.Stat, os.ReadFile, os.MkdirAll, os.WriteFile, os.Remove,
filepath.Rel remains) and update callers to pass the injected fs; update
function signatures (e.g., detectStorages(fs afero.Fs, cfgPath string, ...),
readStorageToken(fs afero.Fs, cfgPath string), readRealMode(fs afero.Fs, cfgPath
string), writeAutostart(fs afero.Fs, storagePath, romPath string),
deleteAutostart(fs afero.Fs, storagePath string)) and ensure error handling/log
messages remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 533-539: Change restartReplayService to return an error (func (p
*Platform) restartReplayService() error): call systemctl and if it fails return
the error instead of only logging it. Update every caller of
restartReplayService to check the returned error and abort/surface the failure
before mutating platform state (do not clear media/tracker state or report
success on failure); propagate or handle the error appropriately so the
operation fails fast. Ensure callers that previously ignored errors now either
return the error up the stack or log and stop further state changes.

In `@pkg/platforms/replayos/screenshot.go`:
- Around line 72-77: The current code records a raw wall-clock timestamp
(triggerTime := time.Now()) and then waitForScreenshot filters files by
ModTime() >= that timestamp, which fails on filesystems with coarse/truncated
mtimes; instead capture the newest existing screenshot file/path and its ModTime
immediately before calling p.triggerScreenshot(), then call waitForScreenshot
with that baseline (or a small filesystem-resolution margin) and update
waitForScreenshot to accept any file whose path differs from the pre-trigger
path OR whose ModTime is newer than the pre-trigger ModTime minus a small
resolution margin; refer to triggerTime, triggerScreenshot(),
waitForScreenshot(), and the pre-trigger newest file/path when making the
change.

---

Nitpick comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 407-529: The helper functions detectStorages, readStorageToken,
readRealMode, writeAutostart, and deleteAutostart currently call os.* directly;
change them to accept an afero.Fs (or use a struct field that holds afero.Fs) so
all filesystem ops use afero (replace os.Stat, os.ReadFile, os.MkdirAll,
os.WriteFile, os.Remove, filepath.Rel remains) and update callers to pass the
injected fs; update function signatures (e.g., detectStorages(fs afero.Fs,
cfgPath string, ...), readStorageToken(fs afero.Fs, cfgPath string),
readRealMode(fs afero.Fs, cfgPath string), writeAutostart(fs afero.Fs,
storagePath, romPath string), deleteAutostart(fs afero.Fs, storagePath string))
and ensure error handling/log messages remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 116cb4d8-b5ee-4fc3-9515-a29a98885cab

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc5f20 and 4d10e0a.

📒 Files selected for processing (10)
  • .github/workflows/build.yml
  • pkg/platforms/replayos/platform.go
  • pkg/platforms/replayos/platform_test.go
  • pkg/platforms/replayos/screenshot.go
  • pkg/platforms/replayos/screenshot_test.go
  • pkg/platforms/replayos/systemmap_test.go
  • pkg/platforms/replayos/tracker.go
  • pkg/platforms/replayos/tracker_test.go
  • pkg/platforms/shared/screenshot_test.go
  • scripts/install.sh
✅ Files skipped from review due to trivial changes (1)
  • pkg/platforms/replayos/screenshot_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/platforms/shared/screenshot_test.go
  • scripts/install.sh
  • pkg/platforms/replayos/tracker.go
  • pkg/platforms/replayos/tracker_test.go

Comment thread pkg/platforms/replayos/platform.go Outdated
Comment thread pkg/platforms/replayos/screenshot.go Outdated
…t mtime baseline

restartReplayService now returns an error instead of logging and continuing.
StopActiveLauncher and launchGame check the error and abort before mutating
platform state, so a failed systemctl restart no longer clears active media
or reports success to callers.

Screenshot captures the newest pre-existing PNG path and mtime before
triggering, then waitForScreenshot accepts any file whose path differs from
the baseline or whose mtime is strictly after it. This avoids false negatives
on exFAT/ext4 filesystems where coarse mtime resolution can cause a newly
written file to compare as older than a wall-clock triggerTime.

findNewestPNG (time-filtered) is replaced by newestPNG (returns newest
unconditionally); callers now own the baseline comparison. Tests updated
to match the new signatures and to cover the error propagation and
coarse-mtime paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
pkg/platforms/replayos/platform.go (1)

302-303: ⚠️ Potential issue | 🟡 Minor

Generic .sh launcher still bypasses the platform context.

Line 303 starts the script with context.Background(), so p.cancel() will not stop it during platform shutdown. Reuse the platform context here instead of detaching the process from the platform lifecycle.

#!/bin/bash
# Verify that the platform owns a cancellable context, but the Generic launcher does not use it.
rg -n -C2 'context\.WithCancel|Start\(context\.Background\(' pkg/platforms/replayos/platform.go
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 302 - 303, The Launch
function is starting the generic .sh launcher with context.Background(), which
detaches it from the platform lifecycle; change the call in Launch (the
p.cmdExec().Start invocation) to use the platform's cancellable context (e.g.,
p.ctx or the context returned/owned by the platform instance) so that p.cancel()
will stop the process on shutdown; update any callers or struct fields if needed
to ensure Launch can access the platform context rather than using
context.Background().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 219-225: StopActiveLauncher currently ignores failures from
deleteAutostart(p.activeStorage) and proceeds to call p.restartReplayService(),
which can relaunch the previous game; change the flow so that deleteAutostart's
error is checked and returned immediately (do not proceed to
restartReplayService) when non-nil. Update the same pattern in the delayed
cleanup path (the other StopActiveLauncher occurrence around the 524-534
region). Look for the symbols StopActiveLauncher, p.activeStorage,
deleteAutostart, and restartReplayService to locate the code and make the
deleteAutostart error cause an early return instead of continuing.
- Around line 362-372: The code writes the autostart (autostart.auto) and sets
p.pendingROMPath under p.trackerMu, but if p.restartReplayService() fails it
returns early leaving a stale autostart file and p.pendingROMPath set; update
the error path in the function that calls writeAutostart and
restartReplayService so that on restart error you (1) remove or revert the
autostart file (the writeAutostart side-effect) and (2) clear p.pendingROMPath
under p.trackerMu before returning the error, ensuring the mutex is
locked/unlocked correctly; use the existing symbols writeAutostart, p.trackerMu,
p.pendingROMPath and p.restartReplayService to locate and implement the
rollback.
- Around line 417-418: The filesystem helpers in platform.go currently call os.*
(e.g., os.Stat, os.ReadDir) directly (see the os.Stat usage around the check
that appends path to all), which prevents hermetic tests; change the Platform
type or the helper function signatures to accept an afero.Fs (or embed afero.Fs
on Platform) and replace direct os.* calls with the afero.Fs equivalents (Stat,
ReadDir, ReadFile, etc.) in all helper functions referenced (including the
blocks at ~457-480 and ~504-526). Ensure all places that construct Platform or
call these helpers provide an afero.NewOsFs() in production and a memMapFs in
tests so behavior stays the same while enabling test isolation.

---

Duplicate comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 302-303: The Launch function is starting the generic .sh launcher
with context.Background(), which detaches it from the platform lifecycle; change
the call in Launch (the p.cmdExec().Start invocation) to use the platform's
cancellable context (e.g., p.ctx or the context returned/owned by the platform
instance) so that p.cancel() will stop the process on shutdown; update any
callers or struct fields if needed to ensure Launch can access the platform
context rather than using context.Background().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0749e402-ff9b-40a5-b50e-4f9aaa20dd93

📥 Commits

Reviewing files that changed from the base of the PR and between 4d10e0a and 0ce1107.

📒 Files selected for processing (4)
  • pkg/platforms/replayos/platform.go
  • pkg/platforms/replayos/platform_test.go
  • pkg/platforms/replayos/screenshot.go
  • pkg/platforms/replayos/screenshot_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/platforms/replayos/platform_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/platforms/replayos/screenshot.go
  • pkg/platforms/replayos/screenshot_test.go

Comment thread pkg/platforms/replayos/platform.go
Comment thread pkg/platforms/replayos/platform.go
Comment on lines +417 to +418
if info, statErr := os.Stat(romsPath); statErr == nil && info.IsDir() {
all = append(all, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Inject an afero.Fs for these filesystem helpers.

These helpers are all testable platform logic, but they hard-code os.* filesystem access. Threading an afero.Fs through Platform or these helper methods would keep ReplayOS tests hermetic and align this package with the repo rule for pkg/**.

As per coding guidelines, pkg/**/*.go: Use afero for filesystem operations in testable code.

Also applies to: 457-480, 504-526

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 417 - 418, The filesystem
helpers in platform.go currently call os.* (e.g., os.Stat, os.ReadDir) directly
(see the os.Stat usage around the check that appends path to all), which
prevents hermetic tests; change the Platform type or the helper function
signatures to accept an afero.Fs (or embed afero.Fs on Platform) and replace
direct os.* calls with the afero.Fs equivalents (Stat, ReadDir, ReadFile, etc.)
in all helper functions referenced (including the blocks at ~457-480 and
~504-526). Ensure all places that construct Platform or call these helpers
provide an afero.NewOsFs() in production and a memMapFs in tests so behavior
stays the same while enabling test isolation.

…eleteAutostart errors

- deleteAutostart now returns error; StopActiveLauncher bails if deletion
  fails before calling restartReplayService
- launchGame rolls back autostart file and clears pendingROMPath when
  restartReplayService fails, so a later manual restart does not
  autolaunch a ROM the caller never successfully started
- launchGame health-check goroutine treats deleteAutostart as best-effort
  (logs warning instead of discarding the error silently)

Tests: assert rollback in LaunchGame_RestartFails; cover deleteAutostart
error path by placing a file where the directory should be.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
pkg/platforms/replayos/platform.go (2)

421-428: 🛠️ Refactor suggestion | 🟠 Major

Back these helpers with afero.Fs instead of direct os.* calls.

detectStorages, the config readers, and the autostart helpers are all testable platform logic, but they still hit the host filesystem directly. Thread an afero.Fs through Platform/these helpers so ReplayOS tests can stay hermetic.

As per coding guidelines, pkg/**/*.go: Use afero for filesystem operations in testable code.

Also applies to: 466-490, 512-546

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 421 - 428, The helpers
(notably detectStorages) currently call os.* directly; change them to use an
injected afero.Fs so tests can provide an in-memory FS: add an afero.Fs field to
the Platform struct (or pass afero.Fs into detectStorages and the config
reader/autostart helper functions), replace os.Stat/os.ReadFile/os.MkdirAll/etc.
with the afero.Fs equivalents (and keep filepath.Join for path composition), and
update callers to pass Platform.fs (or the new fs argument); apply the same
pattern to the other mentioned helpers around lines 466-490 and 512-546 so all
filesystem operations use the injected afero.Fs.

304-306: ⚠️ Potential issue | 🟡 Minor

Use the platform context for Generic launcher processes.

context.Background() detaches the script from platform shutdown/cancellation, so a launched .sh can outlive ReplayOS lifecycle management. Please start it with the platform context instead.

🛠️ Proposed fix
 	launchers = append(launchers, platforms.Launcher{
 		ID:            "Generic",
 		Extensions:    []string{".sh"},
 		AllowListOnly: true,
 		Launch: func(_ *config.Instance, path string, _ *platforms.LaunchOptions) (*os.Process, error) {
-			if err := p.cmdExec().Start(context.Background(), path); err != nil {
+			ctx := p.ctx
+			if ctx == nil {
+				ctx = context.Background()
+			}
+			if err := p.cmdExec().Start(ctx, path); err != nil {
 				return nil, fmt.Errorf("failed to start command: %w", err)
 			}
 			return nil, nil //nolint:nilnil // Command launches don't return a process handle
 		},
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 304 - 306, The Launch
callback uses context.Background() which detaches launched processes from
platform shutdown; change the call in the Launch function so p.cmdExec().Start
uses the platform's context instead of context.Background() (e.g., use p.ctx or
the appropriate platform context accessor on the receiver) so launched scripts
are canceled when the platform is shut down; update the single call site where
p.cmdExec().Start is invoked in Launch to pass the platform context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 229-231: The code clears p.lastKnownCore while holding p.trackerMu
but leaves p.pendingROMPath intact, which can cause the next core transition to
be attributed to a stale ROM; under the same lock where p.lastKnownCore is reset
(guarded by p.trackerMu) also set p.pendingROMPath = "" (or nil as appropriate)
to clear any queued ROM path so the tracker won't use an old value—update the
block that currently calls p.trackerMu.Lock()/Unlock() to reset both
p.lastKnownCore and p.pendingROMPath together.
- Around line 384-394: The goroutine currently returns immediately on ctx.Done
and skips calling deleteAutostart, leaving stale autostart.auto; modify the
goroutine so deleteAutostart(activeStorage) is called in a best-effort path even
if ctx is cancelled (i.e., don’t return on the ctx.Done case before cleanup).
Concretely, restructure the select around the timer/ctx (or use a defer) so that
after the wait you still invoke deleteAutostart(activeStorage) and only skip the
subsequent healthCheck when ctx is cancelled; ensure you reference the existing
healthCheckDelay timer and call deleteAutostart(activeStorage) in the
cancellation path or defer block to guarantee cleanup.

---

Duplicate comments:
In `@pkg/platforms/replayos/platform.go`:
- Around line 421-428: The helpers (notably detectStorages) currently call os.*
directly; change them to use an injected afero.Fs so tests can provide an
in-memory FS: add an afero.Fs field to the Platform struct (or pass afero.Fs
into detectStorages and the config reader/autostart helper functions), replace
os.Stat/os.ReadFile/os.MkdirAll/etc. with the afero.Fs equivalents (and keep
filepath.Join for path composition), and update callers to pass Platform.fs (or
the new fs argument); apply the same pattern to the other mentioned helpers
around lines 466-490 and 512-546 so all filesystem operations use the injected
afero.Fs.
- Around line 304-306: The Launch callback uses context.Background() which
detaches launched processes from platform shutdown; change the call in the
Launch function so p.cmdExec().Start uses the platform's context instead of
context.Background() (e.g., use p.ctx or the appropriate platform context
accessor on the receiver) so launched scripts are canceled when the platform is
shut down; update the single call site where p.cmdExec().Start is invoked in
Launch to pass the platform context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0cc1a36-357e-4d51-88a7-ba655210cf28

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce1107 and 9915803.

📒 Files selected for processing (2)
  • pkg/platforms/replayos/platform.go
  • pkg/platforms/replayos/platform_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/platforms/replayos/platform_test.go

Comment on lines +229 to +231
p.trackerMu.Lock()
p.lastKnownCore = ""
p.trackerMu.Unlock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear pendingROMPath when aborting back to the menu.

This resets lastKnownCore but leaves any queued ROM path behind. If the tracker hasn't consumed it yet, the next core transition can be attributed to the old ROM. Clear p.pendingROMPath under the same lock here.

🛠️ Proposed fix
 	p.trackerMu.Lock()
 	p.lastKnownCore = ""
+	p.pendingROMPath = ""
 	p.trackerMu.Unlock()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
p.trackerMu.Lock()
p.lastKnownCore = ""
p.trackerMu.Unlock()
p.trackerMu.Lock()
p.lastKnownCore = ""
p.pendingROMPath = ""
p.trackerMu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 229 - 231, The code clears
p.lastKnownCore while holding p.trackerMu but leaves p.pendingROMPath intact,
which can cause the next core transition to be attributed to a stale ROM; under
the same lock where p.lastKnownCore is reset (guarded by p.trackerMu) also set
p.pendingROMPath = "" (or nil as appropriate) to clear any queued ROM path so
the tracker won't use an old value—update the block that currently calls
p.trackerMu.Lock()/Unlock() to reset both p.lastKnownCore and p.pendingROMPath
together.

Comment on lines +384 to +394
go func() {
t1 := time.NewTimer(healthCheckDelay)
defer t1.Stop()
select {
case <-ctx.Done():
return
case <-t1.C:
}
if err := deleteAutostart(activeStorage); err != nil {
log.Warn().Err(err).Msg("failed to clear autostart after launch")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't skip autostart cleanup on context cancellation.

If p.ctx is cancelled before the first timer fires, this goroutine exits without deleting autostart.auto. That leaves a stale boot target behind for the next replay.service restart. The cancellation path should still attempt best-effort cleanup and only skip healthCheck.

🛠️ Proposed fix
 	go func() {
 		t1 := time.NewTimer(healthCheckDelay)
 		defer t1.Stop()
 		select {
 		case <-ctx.Done():
-			return
+			if err := deleteAutostart(activeStorage); err != nil {
+				log.Warn().Err(err).Msg("failed to clear autostart during shutdown")
+			}
+			return
 		case <-t1.C:
 		}
 		if err := deleteAutostart(activeStorage); err != nil {
 			log.Warn().Err(err).Msg("failed to clear autostart after launch")
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
go func() {
t1 := time.NewTimer(healthCheckDelay)
defer t1.Stop()
select {
case <-ctx.Done():
return
case <-t1.C:
}
if err := deleteAutostart(activeStorage); err != nil {
log.Warn().Err(err).Msg("failed to clear autostart after launch")
}
go func() {
t1 := time.NewTimer(healthCheckDelay)
defer t1.Stop()
select {
case <-ctx.Done():
if err := deleteAutostart(activeStorage); err != nil {
log.Warn().Err(err).Msg("failed to clear autostart during shutdown")
}
return
case <-t1.C:
}
if err := deleteAutostart(activeStorage); err != nil {
log.Warn().Err(err).Msg("failed to clear autostart after launch")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/platforms/replayos/platform.go` around lines 384 - 394, The goroutine
currently returns immediately on ctx.Done and skips calling deleteAutostart,
leaving stale autostart.auto; modify the goroutine so
deleteAutostart(activeStorage) is called in a best-effort path even if ctx is
cancelled (i.e., don’t return on the ctx.Done case before cleanup). Concretely,
restructure the select around the timer/ctx (or use a defer) so that after the
wait you still invoke deleteAutostart(activeStorage) and only skip the
subsequent healthCheck when ctx is cancelled; ensure you reference the existing
healthCheckDelay timer and call deleteAutostart(activeStorage) in the
cancellation path or defer block to guarantee cleanup.

@wizzomafizzo
wizzomafizzo merged commit 5e4dcf3 into main Apr 18, 2026
11 checks passed
@wizzomafizzo
wizzomafizzo deleted the feat/replayos-integration branch April 18, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant