diff --git a/PROJECT-BRIEF.md b/PROJECT-BRIEF.md index 5fd3429..007501a 100644 --- a/PROJECT-BRIEF.md +++ b/PROJECT-BRIEF.md @@ -144,16 +144,30 @@ A scheduled task `RojoTWR` does exactly this — `schtasks /Run /TN "RojoTWR"`. (Launching via `start` over SSH does **not** work: the process dies when the SSH session exits. Use the task.) -**Rojo does not stay up on its own.** It was observed dying repeatedly (Studio -restarts, machine sleep, the one-shot task simply ending). A watchdog handles -this now: +**Rojo does not stay up on its own.** The root cause was found on 2026-08-06: +**Windows OpenSSH kills the entire process tree when the SSH session exits.** +Anything started over SSH with `Start-Process` — including Rojo — dies seconds +later. This presents as "Rojo was running a minute ago and now the plugin won't +connect," and it is *not* a plugin fault. + +Proof: `rojo procs: 1` inside the SSH session, `rojo procs: 0` from a new +session moments later. + +The fix is to hand ownership to **Task Scheduler**, which is not part of the SSH +process tree: ``` -%USERPROFILE%\rojo-guard.ps1 idempotent restart script -schtasks /Query /TN "RojoTWR-Guard" runs it every 3 minutes +schtasks /Run /TN "RojoTWR" start rojo detached (survives SSH exit) +%USERPROFILE%\rojo-guard.ps1 idempotent watchdog +schtasks /Query /TN "RojoTWR-Guard" runs the watchdog every 3 minutes %USERPROFILE%\rojo-guard.log only logs real events, silent when healthy ``` +Repo copy of the watchdog: `tools/win/rojo-guard.ps1`. + +> **Never start Rojo with `Start-Process`/`start` over SSH.** Always go through +> `schtasks /Run /TN "RojoTWR"`. The guard itself follows this rule internally. + The guard is deliberately conservative: - if `rojo` already owns 34872 it exits 0 and does nothing (safe mid-session — it will not disturb a live Studio connection) @@ -162,8 +176,8 @@ The guard is deliberately conservative: - if `ServerPackages` is missing it logs that instead of letting Rojo silently exit on startup -Verified by killing `rojo` and confirming the guard restored it with the port -owned by `rojo`. +Verified end-to-end: killed `rojo`, waited, and the scheduler restored it +unattended (`Last Result: 0`, port owned by `rojo`). **Clicking Rojo → Connect in Studio is still manual and always will be.** The guard keeps the *server* alive; it cannot press the button. `Rojo_autoReconnect` diff --git a/tools/win/rojo-guard.ps1 b/tools/win/rojo-guard.ps1 new file mode 100644 index 0000000..09a26e4 --- /dev/null +++ b/tools/win/rojo-guard.ps1 @@ -0,0 +1,38 @@ +# rojo-guard.ps1 -- keep Rojo alive for Tokyo Wangan Racing. +# Idempotent: starts Rojo only if it is not already listening on 34872. +# Safe to run every few minutes from Task Scheduler. + +$ErrorActionPreference = "SilentlyContinue" +$repo = Join-Path $env:USERPROFILE "roblox\tokyo-wangan-racing" +$exe = Join-Path $env:USERPROFILE ".local\bin\rojo.exe" +$port = 34872 +$log = Join-Path $env:USERPROFILE "rojo-guard.log" + +function Log($m) { "$(Get-Date -f 'yyyy-MM-dd HH:mm:ss') $m" | Add-Content $log } + +# Is something already LISTENING on the port? +$listening = netstat -ano | Select-String ":$port\s.*LISTENING" + +if ($listening) { + $ownerPid = ($listening[0] -split "\s+")[-1] + $owner = (Get-Process -Id $ownerPid).ProcessName + if ($owner -eq "rojo") { exit 0 } # healthy, nothing to do + Log "PORT HIJACKED by '$owner' (pid $ownerPid) -- not starting. Kill it first." + exit 1 # sshd tunnel or similar: do NOT mask it +} + +# Dependencies must exist or rojo exits instantly. +if (-not (Test-Path (Join-Path $repo "ServerPackages"))) { + Log "ServerPackages MISSING -- rojo would exit on startup. Run 'wally install'." + exit 1 +} + +# Launch DETACHED via Task Scheduler. +# Do NOT use Start-Process here: if this script is ever invoked over SSH, +# Windows OpenSSH kills the whole process tree on session exit and rojo dies +# seconds later. schtasks hands ownership to the scheduler, which survives. +schtasks /Run /TN "RojoTWR" | Out-Null +Start-Sleep -Seconds 4 + +if (Get-Process rojo -EA SilentlyContinue) { Log "started rojo on $port" } +else { Log "FAILED to start rojo" }