fix: rojo died on SSH exit - launch via Task Scheduler, not Start-Process
CI / quality (push) Failing after 47s
CI / quality (push) Failing after 47s
Root cause: Windows OpenSSH kills the whole process tree when the SSH session exits, so rojo started over SSH died seconds later. Proven: 1 proc inside the session, 0 from a fresh session. Guard now calls 'schtasks /Run /TN RojoTWR' instead of Start-Process. Verified unattended: killed rojo, scheduler restored it (Last Result: 0).
This commit is contained in:
+21
-7
@@ -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
|
(Launching via `start` over SSH does **not** work: the process dies when the SSH
|
||||||
session exits. Use the task.)
|
session exits. Use the task.)
|
||||||
|
|
||||||
**Rojo does not stay up on its own.** It was observed dying repeatedly (Studio
|
**Rojo does not stay up on its own.** The root cause was found on 2026-08-06:
|
||||||
restarts, machine sleep, the one-shot task simply ending). A watchdog handles
|
**Windows OpenSSH kills the entire process tree when the SSH session exits.**
|
||||||
this now:
|
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 /Run /TN "RojoTWR" start rojo detached (survives SSH exit)
|
||||||
schtasks /Query /TN "RojoTWR-Guard" runs it every 3 minutes
|
%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
|
%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:
|
The guard is deliberately conservative:
|
||||||
- if `rojo` already owns 34872 it exits 0 and does nothing (safe mid-session —
|
- if `rojo` already owns 34872 it exits 0 and does nothing (safe mid-session —
|
||||||
it will not disturb a live Studio connection)
|
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
|
- if `ServerPackages` is missing it logs that instead of letting Rojo silently
|
||||||
exit on startup
|
exit on startup
|
||||||
|
|
||||||
Verified by killing `rojo` and confirming the guard restored it with the port
|
Verified end-to-end: killed `rojo`, waited, and the scheduler restored it
|
||||||
owned by `rojo`.
|
unattended (`Last Result: 0`, port owned by `rojo`).
|
||||||
|
|
||||||
**Clicking Rojo → Connect in Studio is still manual and always will be.** The
|
**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`
|
guard keeps the *server* alive; it cannot press the button. `Rojo_autoReconnect`
|
||||||
|
|||||||
@@ -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" }
|
||||||
Reference in New Issue
Block a user