From 97673b471029b318042cf0d3743e7bf2e58e5f78 Mon Sep 17 00:00:00 2001 From: Jordan Ye <79342877+Jordan231111@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:57:58 -0400 Subject: [PATCH 1/3] fix(adb): heal wedged offline transport + Player.log boot gate (#18) On a slow boot the per-instance adb transport can wedge in 'offline' (socket up, handshake never finalized). adb's 'connect' no-ops on it ('already connected'), so Boot-And-Wait polled getprop over a dead transport until timeout -- the #18 failure on a 2013 low-end PC where the guest booted fine but adb never came online. Boot-And-Wait now: (1) HEALs an offline transport via disconnect+connect to force a fresh handshake instead of a no-op reconnect -- the deterministic form of the manual kill-server+connect that recovered it by hand; (2) gates 'booted' on a host-side Player.log [Ready] signal (per-instance tagged), independent of adb; (3) bases liveness on the conf adb port listening / Player.log rather than the WMI command-line read alone, so a false WMI zero no longer spams 'retrying launch'; (4) fails fast when nothing is alive and caps the post-[Ready] wait, while still tolerating multi-minute boots. AdbShellRetry/AdbTry now heal (not just reconnect) on a dropped transport. Adds always-on redacted file logging (Init-BsrLog/LogV) so a failing run leaves a full step-by-step log, plus a standalone read-only debug.cmd probe. New unit tests for Parse-AdbState and the Player.log ready/alive parsers (256 pass). Re-embedded into blueStackRoot.cmd; embedded-sync check green. --- blueStackRoot.cmd | 132 ++++++++++++-- debug.cmd | 303 ++++++++++++++++++++++++++++++++ tests/Run-Magisk-Unit-Tests.ps1 | 22 +++ tools/bsr_magisk.ps1 | 132 ++++++++++++-- 4 files changed, 555 insertions(+), 34 deletions(-) create mode 100644 debug.cmd diff --git a/blueStackRoot.cmd b/blueStackRoot.cmd index c2666fd..c068c03 100644 --- a/blueStackRoot.cmd +++ b/blueStackRoot.cmd @@ -38194,7 +38194,29 @@ function Redact-UserPath($value){ $s = $s -replace '(?i)(/Users/)([^/]+)(?=$|/)','${1}xxxxx' $s } -function Say($m,$c='Gray'){ Write-Host (Redact-UserPath $m) -ForegroundColor $c } +# --------- optional redacted file log (extensive logging for debug builds) ---------- +# Stays off until Init-BsrLog runs, which only happens on the real dispatch path -- never when the unit +# tests dot-source this file. Say tees to it; LogV writes verbose adb/boot detail to the FILE ONLY so the +# console output is unchanged. All sinks go through Redact-UserPath, so the account name never lands in a log. +$Script:LogFile = $null +function Init-BsrLog { + if($Script:LogFile){ return } + try{ + $dir = Join-Path $env:TEMP 'bsr_work' + if(-not (Test-Path $dir)){ New-Item -ItemType Directory -Path $dir -Force | Out-Null } + $Script:LogFile = Join-Path $dir ("bsr_run_{0}_{1}.log" -f $Action,(Get-Date -Format 'yyyyMMdd_HHmmss')) + [System.IO.File]::AppendAllText($Script:LogFile, (Redact-UserPath ("==== bsr_magisk $Action $(Get-Date -Format o) PS$($PSVersionTable.PSVersion) instance=$Instance ====`r`n"))) + }catch{ $Script:LogFile = $null } +} +function Say($m,$c='Gray'){ + $r = Redact-UserPath $m + Write-Host $r -ForegroundColor $c + if($Script:LogFile){ try{ [System.IO.File]::AppendAllText($Script:LogFile, ([string]$r + "`r`n")) }catch{} } +} +function LogV($m){ + if(-not $Script:LogFile){ return } + try{ [System.IO.File]::AppendAllText($Script:LogFile, (' {0:HH:mm:ss} {1}' -f (Get-Date), (Redact-UserPath ([string]$m))) + "`r`n") }catch{} +} function Fwd($p){ $p -replace '\\','/' } # --------- normalize incoming paths (callers may pass a trailing '\' or a stray '"') ---------- @@ -38242,6 +38264,7 @@ if (-not $Debugfs) { } if (-not $Conf) { $Conf = Join-Path $DataRoot 'bluestacks.conf' } if (-not $Vhd -and $Instance) { $Vhd = Join-Path $DataRoot "Engine\$Instance\Root.vhd" } +$PlayerLog = Join-Path $DataRoot 'Logs\Player.log' # host-side boot-phase log; every line is tagged with the instance name # Resolve BlueStacks' OWN adb (HD-Adb.exe). We deliberately do NOT fall back to a system adb.exe: # mixing a system adb (e.g. Android SDK platform-tools v1.0.41) with BlueStacks' HD-Adb (v1.0.36) # triggers the "adb server version doesn't match this client; killing..." war -- the two kill each @@ -38632,6 +38655,36 @@ function Get-LiveAdbPorts{ return @($out | Sort-Object -Unique) } +# Test seam: unit tests set this to a scriptblock returning the Player.log text to scan. +$Script:PlayerLogProbe = $null +$Script:PlayerLogOffset = 0 +# Mark the current end of Player.log so the readiness scan only looks at THIS boot's lines (the log is +# append-only and shared across instances/runs, so an old [Ready] from a previous session must not count). +function Set-PlayerLogMark { $Script:PlayerLogOffset = if($PlayerLog -and (Test-Path $PlayerLog)){ try{ (Get-Item $PlayerLog).Length }catch{ 0 } } else { 0 } } +# New Player.log text since the mark (bounded to the last ~1 MB so a very chatty log stays cheap to scan). +function Get-PlayerLogNew { + if($Script:PlayerLogProbe){ return [string](& $Script:PlayerLogProbe) } + if(-not ($PlayerLog -and (Test-Path $PlayerLog))){ return '' } + $txt='' + try{ + $fs=[IO.File]::Open($PlayerLog,'Open','Read','ReadWrite') + try{ + $len=$fs.Length; $start=$Script:PlayerLogOffset + if($len -lt $start){ $start=0 } + if(($len - $start) -gt 1MB){ $start=$len - 1MB } + $fs.Position=$start + $txt=(New-Object IO.StreamReader($fs)).ReadToEnd() + } finally { $fs.Close() } + }catch{ $txt='' } + $txt +} +# Host-side boot signals (no adb needed -- immune to the offline-transport race). BlueStacks tags every +# Player.log line " []" and the phase walks StartingKernel -> StartingAndroid -> Ready. +# Ready = fully booted (home launcher up); Alive = any phase line for THIS instance (cheap liveness a false +# WMI command-line read cannot contradict, so it stops the endless relaunch on a slow boot). +function Test-PlayerLogReady([string]$name=$Instance){ $t=Get-PlayerLogNew; if(-not $t){ return $false }; [bool]($t -match ('(?im)\s'+[regex]::Escape($name)+'\s+\[Ready\]')) } +function Test-PlayerLogAlive([string]$name=$Instance){ $t=Get-PlayerLogNew; if(-not $t){ return $false }; [bool]($t -match ('(?im)\s'+[regex]::Escape($name)+'\s+\[(StartingKernel|StartingAndroid|Ready|Stopping)\]')) } + # Candidate adb ports for THIS instance, in priority order. status.adb_port is the runtime port # BlueStacks writes on boot; adb_port is the Multi-Instance Manager's assigned port (clones get # 5585/5595/...). Those (from BlueStacks' OWN conf) come FIRST -- authoritative when fresh. Then the @@ -38662,6 +38715,21 @@ function Is-BlueStacks([string]$serial){ } $Script:AdbSerial = $null # the pinned 127.0.0.1: transport for the current boot function AdbConnect{ $s = if($Script:AdbSerial){$Script:AdbSerial}else{"127.0.0.1:$((Get-AdbPortCandidates)[0])"}; & $Adb @('connect',$s) *>$null } +# Transport state for $serial from `adb get-state`: 'device' (usable), 'offline' (socket up but handshake +# not done), or an error line. Returns the LAST non-empty, non-'* daemon *' token so daemon-start noise on +# the first call doesn't mask the real state. (Parse split out so it is unit-testable without adb.) +function Parse-AdbState([string]$text){ ($text -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '^\*') } | Select-Object -Last 1) } +function Get-AdbState([string]$serial){ Parse-AdbState (& $Adb @('-s',$serial,'get-state') 2>&1 | Out-String) } +# Force a FRESH transport for a wedged 'offline' TCP device. adb never re-handshakes a connected-but-offline +# socket -- a plain 'connect' on it just returns 'already connected' and leaves it offline -- so on a slow +# boot the transport can sit offline indefinitely while the guest is in fact up. disconnect (drop the dead +# socket) + connect (new handshake against a now-ready adbd) is what recovers it; the same thing a manual +# kill-server+connect does, but scoped to this one transport so other instances are left untouched. +function Repair-AdbTransport([string]$serial){ + & $Adb @('disconnect',$serial) *>$null + Start-Sleep -Milliseconds 600 + (& $Adb @('connect',$serial) 2>&1 | Out-String) +} # False if adb reported a transient transport/device error (common while a freshly-booted instance # is still restarting adbd). Such output should be retried after a reconnect, not trusted. function AdbOk([string]$o){ -not ($o -match "device '.*' not found" -or $o -match 'device .* not found' -or $o -match 'no devices/emulators found' -or $o -match 'device offline' -or $o -match 'error: closed') } @@ -38671,7 +38739,7 @@ function AdbShellRetry([string]$serial,[string]$cmd,[int]$tries=6){ for($k=0;$k -lt $tries;$k++){ $o=(& $Adb @('-s',$serial,'shell',$cmd) 2>&1 | Out-String) if(AdbOk $o){ return $o } - Start-Sleep 3; & $Adb @('start-server') *>$null; AdbConnect + Start-Sleep 3; & $Adb @('start-server') *>$null; Repair-AdbTransport $serial | Out-Null } $o } @@ -38681,7 +38749,7 @@ function AdbTry([string[]]$a,[int]$tries=4){ for($k=0;$k -lt $tries;$k++){ $o=(& $Adb @($a) 2>&1 | Out-String) if(AdbOk $o){ return $o } - Start-Sleep 3; & $Adb @('start-server') *>$null; AdbConnect + Start-Sleep 3; & $Adb @('start-server') *>$null; if($Script:AdbSerial){ Repair-AdbTransport $Script:AdbSerial | Out-Null } else { AdbConnect } } $o } @@ -38709,49 +38777,77 @@ function Boot-And-Wait([int]$timeoutSec=300){ Initialize-AdbServer # pin HD-Adb to its private server port BEFORE any connect (version-conflict immunity) if(-not (Test-Path -LiteralPath $Player)){ throw "HD-Player.exe not found: $Player" } $sw=[Diagnostics.Stopwatch]::StartNew() - $lastLaunch=-999; $lastProgress=-999; $extended=$false; $sawLife=$false; $lastDiag='' + $lastLaunch=-999; $lastProgress=-999; $extended=$false; $sawLife=$false; $sawReady=$false; $readyAt=-1; $lastDiag='' function Start-BsrInstanceLaunch { Say "[*] launching instance $Instance ..." Cyan Start-Process -FilePath $Player -ArgumentList @('--instance',$Instance) | Out-Null $sw.Elapsed.TotalSeconds } $lastLaunch = Start-BsrInstanceLaunch + Set-PlayerLogMark # only count [Ready] lines written AFTER this launch, not a prior boot's Say "[*] HD-Adb server port: $env:ANDROID_ADB_SERVER_PORT" DarkGray - # Find the adb endpoint from BlueStacks' OWN per-instance conf ports (re-read each pass: BlueStacks - # writes the actual bound port during boot). Try each candidate, require boot_completed=1, and confirm - # it is really our BlueStacks instance -- so neither a stale port nor a foreign emulator on 5555 can - # mislead us. We pin to that 127.0.0.1: transport (never the transient emulator-XXXX serial). - $serial=$null; $fallback=$null + # Find the adb endpoint from BlueStacks' OWN per-instance conf ports (status.adb_port first). Each pass we + # connect, read get-state, and -- crucially -- HEAL a wedged 'offline' transport (disconnect + reconnect) + # instead of letting a plain 'connect' no-op on it; THEN require boot_completed=1 and confirm the device + # is really our BlueStacks instance. A host-side Player.log [Ready] is an independent 'guest booted' + # signal, so a slow boot is tolerated while a genuinely dead launch still fails fast. + $serial=$null; $fallback=$null; $primary=@(Get-AdbPortCandidates)[0] while(-not $serial){ $elapsed=$sw.Elapsed.TotalSeconds $limit = if($sawLife){ $timeoutSec + 300 } else { $timeoutSec } if($elapsed -ge $limit){ break } + # Guest booted (Player.log [Ready]) but adb never came online even after healing -> conclusive + # failure; don't burn the full slow-boot grace on it. + if($sawReady -and $readyAt -ge 0 -and ($elapsed - $readyAt) -ge 120){ break } + # Clearly NOT a slow boot: nothing alive at all after a short while -> stop instead of waiting it out. + if(-not $sawLife -and $elapsed -ge 90){ break } if($elapsed -ge $timeoutSec -and $sawLife -and -not $extended){ Say "[~] instance is alive but not adb-ready after $timeoutSec s; extending wait (slow BlueStacks boot)." Yellow $extended=$true } Start-Sleep 3; & $Adb @('start-server') *>$null + + # Liveness is instance-specific (so another running instance can't mask a dead launch) and does NOT + # trust the WMI command-line read alone (it reads null / != instance on some hosts -> a false zero). $playerCount = Get-HdPlayerCount $Instance - if($playerCount -gt 0){ $sawLife=$true } - if($playerCount -eq 0 -and ($elapsed - $lastLaunch) -ge 45){ - Say "[~] no HD-Player process seen yet; retrying launch for $Instance ..." Yellow + $ourPortUp = (@(Get-LiveAdbPorts) -contains $primary) + $logAlive = Test-PlayerLogAlive $Instance + $alive = ($playerCount -gt 0 -or $ourPortUp -or $logAlive) + if($alive){ $sawLife=$true } + if(-not $sawReady -and (Test-PlayerLogReady $Instance)){ $sawReady=$true; $sawLife=$true; $readyAt=$elapsed; Say "[+] Player.log: $Instance reached [Ready] (guest booted)" Green } + + # Relaunch ONLY when nothing says the instance is alive -- a false WMI zero no longer spams launches + # while the instance is clearly up (its adb port is listening or Player.log is advancing). + if(-not $alive -and ($elapsed - $lastLaunch) -ge 45){ + Say "[~] instance not detected (no process / adb port / Player.log); retrying launch for $Instance ..." Yellow $lastLaunch = Start-BsrInstanceLaunch } + $cands = @(Get-AdbPortCandidates) foreach($port in $cands){ $cand="127.0.0.1:$port" $conn=(& $Adb @('connect',$cand) 2>&1 | Out-String) if($conn -match '(?i)(connected to|already connected)'){ $sawLife=$true } + $state = Get-AdbState $cand + LogV "connect $cand -> [$(Compact-Line $conn 60)] state=[$state]" + # The slow-boot fix: a connected-but-offline transport never self-heals, so force a fresh socket. + if($state -ne 'device'){ + $rc = Repair-AdbTransport $cand + $state = Get-AdbState $cand + LogV "heal $cand -> reconnect=[$(Compact-Line $rc 60)] state=[$state]" + } + if($state -ne 'device'){ $lastDiag = "$cand state=[$state]"; continue } # boot_completed must be EXACTLY "1" on its own line -- a "device '...:port' not found" error # contains the port digits and would false-positive a naive -match '1'. $out=(& $Adb @('-s',$cand,'shell','getprop','sys.boot_completed') 2>&1 | Out-String) - $lastDiag = "$cand connect=[$(Compact-Line $conn)] boot=[$(Compact-Line $out)]" + $lastDiag = "$cand state=device boot=[$(Compact-Line $out)]" + LogV "getprop sys.boot_completed @ $cand -> [$(Compact-Line $out)]" if(-not (($out -split "`n" | ForEach-Object { $_.Trim() }) -contains '1')){ continue } if(Is-BlueStacks $cand){ $serial=$cand; break } # confirmed: our instance elseif(-not $fallback){ $fallback=$cand } # booted, but identity unconfirmed } if(($sw.Elapsed.TotalSeconds - $lastProgress) -ge 30 -and -not $serial){ - Say ("[~] waiting for adb: elapsed={0:n0}s hdplayer($Instance)={1} candidates={2} last={3}" -f $sw.Elapsed.TotalSeconds,$playerCount,($cands -join ','),$lastDiag) DarkGray + Say ("[~] waiting for adb: elapsed={0:n0}s hdplayer($Instance)={1} ready={2} candidates={3} last={4}" -f $sw.Elapsed.TotalSeconds,$playerCount,$sawReady,($cands -join ','),$lastDiag) DarkGray $lastProgress=$sw.Elapsed.TotalSeconds } } @@ -38759,11 +38855,11 @@ function Boot-And-Wait([int]$timeoutSec=300){ if(-not $serial){ throw "instance '$Instance' did not boot / become adb-reachable within $([int]$sw.Elapsed.TotalSeconds) s (adb server port $env:ANDROID_ADB_SERVER_PORT; last: $lastDiag)" } $Script:AdbSerial=$serial # Stabilize: a freshly-booted instance (esp. a first boot) restarts adbd a few times, which drops the - # transport -> the next call fails with "device '127.0.0.1:' not found". Wait until a plain shell - # is reliably reachable (3 consecutive hits, reconnecting each time) before handing the serial to callers. + # transport -> the next call fails with "device '127.0.0.1:' not found". HEAL (not just reconnect) + # on each drop until a plain shell is reliably reachable (3 consecutive hits) before handing it over. $stable=0 for($s=0;$s -lt 30 -and $stable -lt 3;$s++){ - AdbConnect + if((Get-AdbState $serial) -ne 'device'){ Repair-AdbTransport $serial | Out-Null } else { AdbConnect } $t=(& $Adb @('-s',$serial,'shell','echo BSR_RDY') 2>&1 | Out-String) if($t -match 'BSR_RDY'){ $stable++ } else { $stable=0; Start-Sleep 3 } } @@ -39041,6 +39137,7 @@ function Do-Undo { # Only dispatch when run normally (-File / &). When DOT-SOURCED (. bsr_magisk.ps1) -- e.g. by the test # suite to unit-test the resolver functions -- skip the pipeline so nothing boots or writes. if ($MyInvocation.InvocationName -ne '.') { + Init-BsrLog try { switch($Action){ 'Prep' { Do-Prep } @@ -39059,6 +39156,7 @@ if ($MyInvocation.InvocationName -ne '.') { # forever; we only ever started one if Initialize-AdbServer ran (an online action), so tidy it # up so nothing of ours lingers on the port after the tool exits. (runs even if an action threw) if ($Script:AdbServerInit -and (Test-Path -LiteralPath $Adb)) { & $Adb @('kill-server') *>$null } + if ($Script:LogFile) { Say "[*] full step-by-step debug log: $Script:LogFile" DarkGray } } } __BSR_MAGISK_END__ diff --git a/debug.cmd b/debug.cmd new file mode 100644 index 0000000..d17040f --- /dev/null +++ b/debug.cmd @@ -0,0 +1,303 @@ +@echo off +setlocal EnableExtensions +title BlueStacksRoot ADB Diagnostic + +rem =========================================================================== +rem debug.cmd -- read-only ADB / boot-timing diagnostic for BlueStacksRoot. +rem Does NOT touch any disk image, conf, or HD-Player binary. It only launches +rem the instance and observes how adb + the boot progress behave, writing a +rem redacted log to the Desktop. Run it, reproduce, then attach the .log file. +rem +rem Usage: debug.cmd (auto-detects the most-recent instance) +rem debug.cmd Rvc64 (diagnose a specific instance) +rem =========================================================================== + +rem --- self-elevate to Administrator (parity with the real tool's conditions) --- +net session >nul 2>&1 +if not "%errorlevel%"=="0" ( + echo [*] Requesting Administrator elevation... + if "%~1"=="" ( + powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" + ) else ( + powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -ArgumentList '%~1' -Verb RunAs" + ) + exit /b +) + +rem --- extract the embedded PowerShell body (after the marker) to a temp .ps1 --- +set "SELF=%~f0" +set "PS1=%TEMP%\bsr_debug_%RANDOM%%RANDOM%.ps1" +powershell -NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText($env:SELF); $m='#__BSR'+'_DEBUG_PS__'; $i=$t.IndexOf($m); if($i -lt 0){ Write-Error 'marker not found'; exit 1 }; [IO.File]::WriteAllText($env:PS1, $t.Substring($i))" +powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %1 +del "%PS1%" >nul 2>&1 +echo. +echo ============================================================ +echo Done. Attach the bsr_debug_*.log on your Desktop to GitHub. +echo ============================================================ +pause +exit /b + +#__BSR_DEBUG_PS__ +param([string]$Instance) +$ErrorActionPreference = 'Continue' + +# ----------------------------- logging / redaction ----------------------------- +function Redact($v){ + if($null -eq $v){ return $v } + $s = [string]$v + $up = $env:USERPROFILE + if($up){ + $s = $s -replace [regex]::Escape($up), '%USERPROFILE%' + $s = $s -replace [regex]::Escape(($up -replace '\\','/')), '%USERPROFILE%' + } + $s = $s -replace '(?i)([A-Z]:[\\/]+Users[\\/]+)([^\\/]+)', '${1}xxxxx' + $s +} +$ts = Get-Date -Format 'yyyyMMdd_HHmmss' +$Desktop = [Environment]::GetFolderPath('Desktop'); if(-not $Desktop){ $Desktop = $env:USERPROFILE } +$LogFile = Join-Path $Desktop "bsr_debug_$ts.log" +function Log($m,$c='Gray'){ + $line = ('{0:HH:mm:ss.fff} {1}' -f (Get-Date), (Redact $m)) + try { Write-Host $line -ForegroundColor $c } catch { Write-Host $line } + try { Add-Content -LiteralPath $LogFile -Value $line -Encoding utf8 } catch {} +} +function Section($t){ Log ''; Log ('==================== ' + $t + ' ====================') Cyan } +function Compact($s,[int]$max=100){ if($null -eq $s){ return '' }; $x = (($s -replace "`r?`n",' | ').Trim()); if($x.Length -gt $max){ $x.Substring(0,$max-3)+'...' } else { $x } } + +Log "BlueStacksRoot ADB diagnostic" Green +Log "log file : $(Redact $LogFile)" +Log "OS : $([Environment]::OSVersion.VersionString) PowerShell $($PSVersionTable.PSVersion)" + +# ----------------------------- registry discovery ----------------------------- +function Get-Reg { + foreach($k in @('HKLM:\SOFTWARE\BlueStacks_nxt','HKLM:\SOFTWARE\BlueStacks_msi5', + 'HKLM:\SOFTWARE\WOW6432Node\BlueStacks_nxt','HKLM:\SOFTWARE\WOW6432Node\BlueStacks_msi5')){ + try{ $p = Get-ItemProperty -Path $k -ErrorAction Stop + if($p -and ($p.InstallDir -or $p.DataDir -or $p.UserDefinedDir)){ return $p } }catch{} + } + return $null +} +$reg = Get-Reg +$Install = if($reg -and $reg.InstallDir){ $reg.InstallDir.TrimEnd('\') } else { Join-Path $env:ProgramFiles 'BlueStacks_nxt' } +$DataRoot = if($reg -and $reg.DataDir){ $reg.DataDir } elseif($reg -and $reg.UserDefinedDir){ $reg.UserDefinedDir } else { Join-Path $env:ProgramData 'BlueStacks_nxt' } +if($DataRoot -match '(?i)[\\/]engine[\\/]?$'){ $DataRoot = $DataRoot -replace '(?i)[\\/]engine[\\/]?$','' } +$DataRoot = $DataRoot.TrimEnd('\','/') +$Conf = Join-Path $DataRoot 'bluestacks.conf' +$PlayerLog = Join-Path $DataRoot 'Logs\Player.log' +$Player = Join-Path $Install 'HD-Player.exe' +$AdbExe = Join-Path $Install 'HD-Adb.exe' +if(-not (Test-Path $AdbExe)){ + foreach($c in @((Join-Path $env:ProgramFiles 'BlueStacks_nxt\HD-Adb.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'BlueStacks_nxt\HD-Adb.exe'), + (Join-Path $env:ProgramFiles 'BlueStacks_msi5\HD-Adb.exe'))){ if(Test-Path $c){ $AdbExe=$c; break } } +} + +function Adb([string[]]$a){ try{ (& $AdbExe @a 2>&1 | Out-String).Trim() }catch{ "ERR: $($_.Exception.Message)" } } +function State($serial){ $o = Adb @('-s',$serial,'get-state'); ($o -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Last 1) } + +# ----------------------------- instance selection ----------------------------- +function Get-ConfInstances { + if(-not (Test-Path $Conf)){ return @() } + try{ $ct=[IO.File]::ReadAllText($Conf) }catch{ return @() } + @([regex]::Matches($ct,'(?im)^\s*bst\.instance\.([A-Za-z0-9_]+)\.adb_port\s*=') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique) +} +$allInst = Get-ConfInstances +if([string]::IsNullOrWhiteSpace($Instance)){ + $eng = Join-Path $DataRoot 'Engine'; $pick = $null + if(Test-Path $eng){ + $pick = Get-ChildItem $eng -Directory -EA SilentlyContinue | Where-Object { $allInst -contains $_.Name } | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty Name + } + if(-not $pick -and $allInst.Count -ge 1){ $pick = $allInst[0] } + if(-not $pick){ $pick = 'Rvc64' } + $Instance = $pick +} + +function Get-ConfPort($name,$key){ + if(-not (Test-Path $Conf)){ return $null } + try{ $ct=[IO.File]::ReadAllText($Conf) }catch{ return $null } + $m=[regex]::Match($ct,'(?im)^\s*bst\.instance\.'+[regex]::Escape($name)+'\.'+[regex]::Escape($key)+'\s*=\s*"?(\d+)"?') + if($m.Success){ $m.Groups[1].Value } else { $null } +} +$statusPort = Get-ConfPort $Instance 'status.adb_port' +$adbPort = Get-ConfPort $Instance 'adb_port' +$PrimaryPort = if($statusPort){ $statusPort } elseif($adbPort){ $adbPort } else { '5555' } + +# ----------------------------- host helpers ----------------------------- +function Get-BandListeners($lo,$hi){ + $o = New-Object System.Collections.Generic.List[object] + try{ + Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalPort -ge $lo -and $_.LocalPort -le $hi } | + ForEach-Object { [void]$o.Add([pscustomobject]@{ Port=$_.LocalPort; PID=$_.OwningProcess; Proc=(Get-Process -Id $_.OwningProcess -EA SilentlyContinue).Name }) } + }catch{ + try{ foreach($ln in (netstat -ano -p tcp 2>$null)){ if($ln -match 'LISTENING' -and $ln -match ':(\d{4,5})\b'){ $p=[int]$Matches[1]; if($p -ge $lo -and $p -le $hi){ [void]$o.Add([pscustomobject]@{Port=$p;PID='?';Proc='?'}) } } } }catch{} + } + ,@($o | Sort-Object Port -Unique) +} +function Fmt-Band($b){ ($b | ForEach-Object { "$($_.Port)/$($_.Proc)" }) -join ',' } + +function Test-CmdLine($cmdLine,$name){ + if([string]::IsNullOrWhiteSpace($cmdLine) -or [string]::IsNullOrWhiteSpace($name)){ return $false } + $e=[regex]::Escape($name); ($cmdLine -match "(?i)(^|\s)--instance(?:\s+|=)(`"$e`"|$e)(?=\s|$)") +} +function Probe-Player($name){ + $r = [ordered]@{ proc=0; wmi_total=0; wmi_match=0; cmds=@() } + $r.proc = @(Get-Process -Name 'HD-Player' -EA SilentlyContinue).Count + try{ + $w = @(Get-CimInstance Win32_Process -Filter "Name='HD-Player.exe'" -ErrorAction Stop) + $r.wmi_total = $w.Count + $r.wmi_match = @($w | Where-Object { Test-CmdLine $_.CommandLine $name }).Count + $r.cmds = @($w | ForEach-Object { $_.CommandLine }) + }catch{ $r.cmds = @("WMI ERROR: $($_.Exception.Message)") } + $r +} + +$script:logOffset = 0 +function Snapshot-Log { if(Test-Path $PlayerLog){ try{ $script:logOffset = (Get-Item $PlayerLog).Length }catch{ $script:logOffset = 0 } } else { $script:logOffset = 0 } } +function Read-NewLog($name){ + if(-not (Test-Path $PlayerLog)){ return @() } + try{ + $fs=[IO.File]::Open($PlayerLog,'Open','Read','ReadWrite') + try{ + if($fs.Length -lt $script:logOffset){ $script:logOffset = 0 } # rotated / shrank + $fs.Position = $script:logOffset + $sr = New-Object IO.StreamReader($fs) + $txt = $sr.ReadToEnd() + $script:logOffset = $fs.Position + } finally { $fs.Close() } + @($txt -split "`r?`n" | Where-Object { $_ -match (' ' + [regex]::Escape($name) + ' \[') }) + }catch{ @() } +} +function Phase-Of($line){ $m=[regex]::Match($line, [regex]::Escape($Instance)+'\s+\[([A-Za-z]+)\]'); if($m.Success){ $m.Groups[1].Value } } + +# ----------------------------- report environment ----------------------------- +Section 'ENVIRONMENT' +Log "Install : $(Redact $Install) exists=$([bool](Test-Path $Install))" +Log "DataRoot : $(Redact $DataRoot)" +Log "Conf : $(Redact $Conf) exists=$([bool](Test-Path $Conf))" +Log "Player.log: $(Redact $PlayerLog) exists=$([bool](Test-Path $PlayerLog))" +Log "HD-Player : exists=$([bool](Test-Path $Player))" +Log "HD-Adb : exists=$([bool](Test-Path $AdbExe)) version=[$(Compact (Adb @('version')))]" +if(-not (Test-Path $Player) -or -not (Test-Path $AdbExe)){ Log '[!] HD-Player.exe or HD-Adb.exe not found -- cannot continue.' Red; return } + +Section 'CONF PORTS' +Log "instances in conf : $($allInst -join ', ')" +Log "TARGET instance : $Instance" Yellow +Log "status.adb_port : $statusPort" +Log "adb_port : $adbPort" +Log "PRIMARY port used : $PrimaryPort (this is what the fix should try FIRST)" Yellow + +# ----------------------------- private adb server port ----------------------------- +$serverBand = Get-BandListeners 15037 15057 +$serverPort = '15037' +$owned = @{}; foreach($b in $serverBand){ $owned[[int]$b.Port] = ($b.Proc -ieq 'HD-Adb') } +foreach($p in 15037..15057){ if(-not $owned.ContainsKey($p) -or $owned[$p]){ $serverPort = "$p"; break } } +$env:ANDROID_ADB_SERVER_PORT = $serverPort +Log "ADB server port : $serverPort (current 15037-15057 listeners: $(Fmt-Band $serverBand))" + +# ----------------------------- clean cold start ----------------------------- +Section 'CLEAN START' +Log 'killing BlueStacks processes for a clean cold-boot timing measurement...' +Get-Process -EA SilentlyContinue | Where-Object { $_.Name -match '^(HD-|Bstk|BlueStacks)' } | Stop-Process -Force -EA SilentlyContinue +Start-Sleep 3 +Log "kill-server -> $(Compact (Adb @('kill-server')))" +Log "start-server -> $(Compact (Adb @('start-server')))" +Snapshot-Log + +Section 'LAUNCH + WATCH' +Log "launch: HD-Player.exe --instance $Instance" +try{ Start-Process -FilePath $Player -ArgumentList @('--instance',$Instance) | Out-Null }catch{ Log "[!] launch failed: $($_.Exception.Message)" Red } +$sw = [Diagnostics.Stopwatch]::StartNew() + +# timing knobs (reasonable, but fail fast when it is clearly NOT just slow boot) +$HARD_CAP = 480 # absolute ceiling +$NOPROGRESS = 120 # nothing alive at all by here -> bail +$POST_READY = 75 # booted but adb won't come online even after heal -> conclusive + +$readyAt=$null; $firstOnlineAt=$null; $healWorkedAt=$null; $sawProc=$false; $lastPhase=$null +$identityLogged=$false; $nextHeal=20; $done=$false; $verdict='(inconclusive)' + +while(-not $done){ + $el = [int]$sw.Elapsed.TotalSeconds + if($el -ge $HARD_CAP){ $verdict = "TIMEOUT: no success within $HARD_CAP s"; break } + Start-Sleep 2 + try { + $pp = Probe-Player $Instance + if($pp.proc -gt 0){ $sawProc = $true } + + foreach($l in (Read-NewLog $Instance)){ + $ph = Phase-Of $l; if($ph){ $lastPhase = $ph } + if(-not $readyAt -and ($l -match '\[Ready\]' -or $l -match 'HomeActivity' -or $l -match 'Player state:.*->\s*Player state:\s*Ready')){ + $readyAt = $el; Log "*** Player.log: instance reached [Ready] (fully booted) at elapsed=${el}s ***" Green + } + } + + $bl = Get-BandListeners 5550 5900 + $cand = "127.0.0.1:$PrimaryPort" + $conn = Adb @('connect',$cand) + $state= State $cand + $devs = Adb @('devices') + $bc = if($state -eq 'device'){ Adb @('-s',$cand,'shell','getprop','sys.boot_completed') } else { '' } + + Log ("t=${el}s proc[getproc=$($pp.proc) wmi_total=$($pp.wmi_total) wmi_match=$($pp.wmi_match)] phase=$lastPhase band=[$(Fmt-Band $bl)] connect=[$(Compact $conn 40)] state=[$state] boot=[$(Compact $bc 14)] devices=[$(Compact $devs 60)]") + + if($pp.proc -gt 0 -and $pp.wmi_match -eq 0){ + Log " >> WMI false-zero: HD-Player IS running but instance-filtered match=0 (this is why the real tool spams 'retrying launch'):" Yellow + foreach($cl in $pp.cmds){ Log " cmdline: $(Redact (Compact $cl 160))" DarkGray } + } + + if($state -eq 'device' -and -not $identityLogged){ + $identityLogged=$true + Log " guest identity: release=[$(Compact (Adb @('-s',$cand,'shell','getprop','ro.build.version.release')) 12)] bst=[$(Compact (Adb @('-s',$cand,'shell','getprop','bst.version')) 20)]" + } + + # SUCCESS without heal + if($state -eq 'device' -and (($bc -split "`r?`n" | ForEach-Object { $_.Trim() }) -contains '1')){ + if(-not $firstOnlineAt){ $firstOnlineAt = $el } + $verdict = "SUCCESS: $cand online + boot_completed=1 at ${el}s (no disconnect needed)"; $done=$true; break + } + + # HEAL EXPERIMENT when offline (periodically, and immediately once [Ready] is seen) + if($state -ne 'device' -and ($el -ge $nextHeal -or ($readyAt -and -not $healWorkedAt))){ + $nextHeal = $el + 25 + Log " -- HEAL EXPERIMENT on $cand (state=$state): disconnect + reconnect --" Magenta + Log " disconnect -> $(Compact (Adb @('disconnect',$cand)) 50)" + Start-Sleep 1 + Log " connect -> $(Compact (Adb @('connect',$cand)) 50)" + Start-Sleep 2 + $s2 = State $cand + Log " get-state -> $s2" $(if($s2 -eq 'device'){'Green'}else{'Yellow'}) + if($s2 -eq 'device'){ + if(-not $healWorkedAt){ $healWorkedAt=$el; Log " >> HEAL WORKED: disconnect+connect flipped $cand offline->device at ${el}s" Green } + $bc2 = Adb @('-s',$cand,'shell','getprop','sys.boot_completed') + Log " boot_completed after heal -> $(Compact $bc2 14)" + if(($bc2 -split "`r?`n" | ForEach-Object { $_.Trim() }) -contains '1'){ + $verdict = "SUCCESS via HEAL: $cand online+boot_completed=1 at ${el}s -- disconnect+connect WAS required (this is the fix)"; $done=$true; break + } + } + } + + # fail-fast: nothing alive at all + if(-not $sawProc -and $el -ge $NOPROGRESS -and $bl.Count -eq 0 -and -not $readyAt){ + $verdict = "FAIL-FAST: no HD-Player process, no adb listener, no Player.log activity after ${el}s -- instance never started"; break + } + # fail-fast: process died after we saw it + if($sawProc -and $pp.proc -eq 0){ + $verdict = "FAIL-FAST: HD-Player disappeared at ${el}s -- instance crashed or was closed"; break + } + # conclusive: booted but adb won't come online even with heal + if($readyAt -and ($el - $readyAt) -ge $POST_READY -and -not $firstOnlineAt -and -not $healWorkedAt){ + $verdict = "CONCLUSIVE: instance booted (Player.log [Ready] at ${readyAt}s) but $cand stayed offline AND disconnect+connect did not recover it after +${POST_READY}s"; break + } + } catch { + Log " [iter error] $($_.Exception.Message)" DarkYellow + } +} + +Section 'VERDICT' +Log $verdict $(if($verdict -match '^SUCCESS'){'Green'}else{'Red'}) +Log ("timeline: PlayerLog[Ready]=$readyAt s | firstAdbOnline=$firstOnlineAt s | healWorked=$healWorkedAt s | sawProcess=$sawProc | primaryPort=$PrimaryPort") +Log '' +Log '------------------------------------------------------------------' +Log "Full log: $(Redact $LogFile)" Cyan +Log 'Attach that .log file to the GitHub issue. (Instance left running for inspection.)' Cyan diff --git a/tests/Run-Magisk-Unit-Tests.ps1 b/tests/Run-Magisk-Unit-Tests.ps1 index 69eb501..8de7834 100644 --- a/tests/Run-Magisk-Unit-Tests.ps1 +++ b/tests/Run-Magisk-Unit-Tests.ps1 @@ -226,6 +226,27 @@ try { ) foreach ($c in $adbCases) { Ok "adbok: $($c[0])" ((AdbOk $c[1]) -eq $c[2]) } + Section 'Parse-AdbState (get-state classifier)' + Eq 'state: device' 'device' (Parse-AdbState "device`n") + Eq 'state: offline' 'offline' (Parse-AdbState 'offline') + Eq 'state: skips daemon-start noise' 'device' (Parse-AdbState "* daemon not running; starting now on tcp:15037 *`n* daemon started successfully *`ndevice") + Eq 'state: not-found line preserved' "error: device '127.0.0.1:5555' not found" (Parse-AdbState "error: device '127.0.0.1:5555' not found") + Eq 'state: empty -> empty' '' (Parse-AdbState '') + Ok 'state: offline is not device' ((Parse-AdbState 'offline') -ne 'device') + + Section 'Player.log boot/liveness signals' + $plReady = '2026-06-03 15:11:46.616-0400 5988 13764 PLR Rvc64 [Ready] I: HomeActivity shown' + $plStarting = '2026-06-03 15:11:10.000-0400 5988 2360 SER Rvc64 [StartingAndroid] I: GUEST booting' + $plOther = '2026-06-03 15:11:46.616-0400 5988 13764 PLR Tiramisu64_9 [Ready] I: shown' + $plClone = '2026-06-03 15:11:46.616-0400 5988 13764 PLR Rvc64_9 [Ready] I: shown' + function Probe([string]$txt) { $script:PlayerLogProbe = { $txt }.GetNewClosure() } + Probe $plReady; Ok 'plog: [Ready] -> ready' (Test-PlayerLogReady 'Rvc64'); Ok 'plog: [Ready] -> alive' (Test-PlayerLogAlive 'Rvc64') + Probe $plStarting; Ok 'plog: [StartingAndroid] not ready' (-not (Test-PlayerLogReady 'Rvc64')); Ok 'plog: [StartingAndroid] alive' (Test-PlayerLogAlive 'Rvc64') + Probe $plOther; Ok 'plog: other instance not ready' (-not (Test-PlayerLogReady 'Rvc64')); Ok 'plog: other instance not alive' (-not (Test-PlayerLogAlive 'Rvc64')) + Probe $plClone; Ok 'plog: clone tag not base ready' (-not (Test-PlayerLogReady 'Rvc64')) + Probe ''; Ok 'plog: empty not ready' (-not (Test-PlayerLogReady 'Rvc64')); Ok 'plog: empty not alive' (-not (Test-PlayerLogAlive 'Rvc64')) + $script:PlayerLogProbe = $null + Section 'Compact-Line' Eq 'compact: newline collapse' 'a | b | c' (Compact-Line "a`r`nb`nc" 80) Eq 'compact: no truncate at max' 'abcdef' (Compact-Line 'abcdef' 6) @@ -291,6 +312,7 @@ try { } finally { $script:LiveAdbPortProbe = $null $script:AdbServerPortProbe = $null + $script:PlayerLogProbe = $null foreach ($d in $script:made) { try { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } catch { } } } diff --git a/tools/bsr_magisk.ps1 b/tools/bsr_magisk.ps1 index e08fc5b..7a60973 100644 --- a/tools/bsr_magisk.ps1 +++ b/tools/bsr_magisk.ps1 @@ -60,7 +60,29 @@ function Redact-UserPath($value){ $s = $s -replace '(?i)(/Users/)([^/]+)(?=$|/)','${1}xxxxx' $s } -function Say($m,$c='Gray'){ Write-Host (Redact-UserPath $m) -ForegroundColor $c } +# --------- optional redacted file log (extensive logging for debug builds) ---------- +# Stays off until Init-BsrLog runs, which only happens on the real dispatch path -- never when the unit +# tests dot-source this file. Say tees to it; LogV writes verbose adb/boot detail to the FILE ONLY so the +# console output is unchanged. All sinks go through Redact-UserPath, so the account name never lands in a log. +$Script:LogFile = $null +function Init-BsrLog { + if($Script:LogFile){ return } + try{ + $dir = Join-Path $env:TEMP 'bsr_work' + if(-not (Test-Path $dir)){ New-Item -ItemType Directory -Path $dir -Force | Out-Null } + $Script:LogFile = Join-Path $dir ("bsr_run_{0}_{1}.log" -f $Action,(Get-Date -Format 'yyyyMMdd_HHmmss')) + [System.IO.File]::AppendAllText($Script:LogFile, (Redact-UserPath ("==== bsr_magisk $Action $(Get-Date -Format o) PS$($PSVersionTable.PSVersion) instance=$Instance ====`r`n"))) + }catch{ $Script:LogFile = $null } +} +function Say($m,$c='Gray'){ + $r = Redact-UserPath $m + Write-Host $r -ForegroundColor $c + if($Script:LogFile){ try{ [System.IO.File]::AppendAllText($Script:LogFile, ([string]$r + "`r`n")) }catch{} } +} +function LogV($m){ + if(-not $Script:LogFile){ return } + try{ [System.IO.File]::AppendAllText($Script:LogFile, (' {0:HH:mm:ss} {1}' -f (Get-Date), (Redact-UserPath ([string]$m))) + "`r`n") }catch{} +} function Fwd($p){ $p -replace '\\','/' } # --------- normalize incoming paths (callers may pass a trailing '\' or a stray '"') ---------- @@ -108,6 +130,7 @@ if (-not $Debugfs) { } if (-not $Conf) { $Conf = Join-Path $DataRoot 'bluestacks.conf' } if (-not $Vhd -and $Instance) { $Vhd = Join-Path $DataRoot "Engine\$Instance\Root.vhd" } +$PlayerLog = Join-Path $DataRoot 'Logs\Player.log' # host-side boot-phase log; every line is tagged with the instance name # Resolve BlueStacks' OWN adb (HD-Adb.exe). We deliberately do NOT fall back to a system adb.exe: # mixing a system adb (e.g. Android SDK platform-tools v1.0.41) with BlueStacks' HD-Adb (v1.0.36) # triggers the "adb server version doesn't match this client; killing..." war -- the two kill each @@ -498,6 +521,36 @@ function Get-LiveAdbPorts{ return @($out | Sort-Object -Unique) } +# Test seam: unit tests set this to a scriptblock returning the Player.log text to scan. +$Script:PlayerLogProbe = $null +$Script:PlayerLogOffset = 0 +# Mark the current end of Player.log so the readiness scan only looks at THIS boot's lines (the log is +# append-only and shared across instances/runs, so an old [Ready] from a previous session must not count). +function Set-PlayerLogMark { $Script:PlayerLogOffset = if($PlayerLog -and (Test-Path $PlayerLog)){ try{ (Get-Item $PlayerLog).Length }catch{ 0 } } else { 0 } } +# New Player.log text since the mark (bounded to the last ~1 MB so a very chatty log stays cheap to scan). +function Get-PlayerLogNew { + if($Script:PlayerLogProbe){ return [string](& $Script:PlayerLogProbe) } + if(-not ($PlayerLog -and (Test-Path $PlayerLog))){ return '' } + $txt='' + try{ + $fs=[IO.File]::Open($PlayerLog,'Open','Read','ReadWrite') + try{ + $len=$fs.Length; $start=$Script:PlayerLogOffset + if($len -lt $start){ $start=0 } + if(($len - $start) -gt 1MB){ $start=$len - 1MB } + $fs.Position=$start + $txt=(New-Object IO.StreamReader($fs)).ReadToEnd() + } finally { $fs.Close() } + }catch{ $txt='' } + $txt +} +# Host-side boot signals (no adb needed -- immune to the offline-transport race). BlueStacks tags every +# Player.log line " []" and the phase walks StartingKernel -> StartingAndroid -> Ready. +# Ready = fully booted (home launcher up); Alive = any phase line for THIS instance (cheap liveness a false +# WMI command-line read cannot contradict, so it stops the endless relaunch on a slow boot). +function Test-PlayerLogReady([string]$name=$Instance){ $t=Get-PlayerLogNew; if(-not $t){ return $false }; [bool]($t -match ('(?im)\s'+[regex]::Escape($name)+'\s+\[Ready\]')) } +function Test-PlayerLogAlive([string]$name=$Instance){ $t=Get-PlayerLogNew; if(-not $t){ return $false }; [bool]($t -match ('(?im)\s'+[regex]::Escape($name)+'\s+\[(StartingKernel|StartingAndroid|Ready|Stopping)\]')) } + # Candidate adb ports for THIS instance, in priority order. status.adb_port is the runtime port # BlueStacks writes on boot; adb_port is the Multi-Instance Manager's assigned port (clones get # 5585/5595/...). Those (from BlueStacks' OWN conf) come FIRST -- authoritative when fresh. Then the @@ -528,6 +581,21 @@ function Is-BlueStacks([string]$serial){ } $Script:AdbSerial = $null # the pinned 127.0.0.1: transport for the current boot function AdbConnect{ $s = if($Script:AdbSerial){$Script:AdbSerial}else{"127.0.0.1:$((Get-AdbPortCandidates)[0])"}; & $Adb @('connect',$s) *>$null } +# Transport state for $serial from `adb get-state`: 'device' (usable), 'offline' (socket up but handshake +# not done), or an error line. Returns the LAST non-empty, non-'* daemon *' token so daemon-start noise on +# the first call doesn't mask the real state. (Parse split out so it is unit-testable without adb.) +function Parse-AdbState([string]$text){ ($text -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '^\*') } | Select-Object -Last 1) } +function Get-AdbState([string]$serial){ Parse-AdbState (& $Adb @('-s',$serial,'get-state') 2>&1 | Out-String) } +# Force a FRESH transport for a wedged 'offline' TCP device. adb never re-handshakes a connected-but-offline +# socket -- a plain 'connect' on it just returns 'already connected' and leaves it offline -- so on a slow +# boot the transport can sit offline indefinitely while the guest is in fact up. disconnect (drop the dead +# socket) + connect (new handshake against a now-ready adbd) is what recovers it; the same thing a manual +# kill-server+connect does, but scoped to this one transport so other instances are left untouched. +function Repair-AdbTransport([string]$serial){ + & $Adb @('disconnect',$serial) *>$null + Start-Sleep -Milliseconds 600 + (& $Adb @('connect',$serial) 2>&1 | Out-String) +} # False if adb reported a transient transport/device error (common while a freshly-booted instance # is still restarting adbd). Such output should be retried after a reconnect, not trusted. function AdbOk([string]$o){ -not ($o -match "device '.*' not found" -or $o -match 'device .* not found' -or $o -match 'no devices/emulators found' -or $o -match 'device offline' -or $o -match 'error: closed') } @@ -537,7 +605,7 @@ function AdbShellRetry([string]$serial,[string]$cmd,[int]$tries=6){ for($k=0;$k -lt $tries;$k++){ $o=(& $Adb @('-s',$serial,'shell',$cmd) 2>&1 | Out-String) if(AdbOk $o){ return $o } - Start-Sleep 3; & $Adb @('start-server') *>$null; AdbConnect + Start-Sleep 3; & $Adb @('start-server') *>$null; Repair-AdbTransport $serial | Out-Null } $o } @@ -547,7 +615,7 @@ function AdbTry([string[]]$a,[int]$tries=4){ for($k=0;$k -lt $tries;$k++){ $o=(& $Adb @($a) 2>&1 | Out-String) if(AdbOk $o){ return $o } - Start-Sleep 3; & $Adb @('start-server') *>$null; AdbConnect + Start-Sleep 3; & $Adb @('start-server') *>$null; if($Script:AdbSerial){ Repair-AdbTransport $Script:AdbSerial | Out-Null } else { AdbConnect } } $o } @@ -575,49 +643,77 @@ function Boot-And-Wait([int]$timeoutSec=300){ Initialize-AdbServer # pin HD-Adb to its private server port BEFORE any connect (version-conflict immunity) if(-not (Test-Path -LiteralPath $Player)){ throw "HD-Player.exe not found: $Player" } $sw=[Diagnostics.Stopwatch]::StartNew() - $lastLaunch=-999; $lastProgress=-999; $extended=$false; $sawLife=$false; $lastDiag='' + $lastLaunch=-999; $lastProgress=-999; $extended=$false; $sawLife=$false; $sawReady=$false; $readyAt=-1; $lastDiag='' function Start-BsrInstanceLaunch { Say "[*] launching instance $Instance ..." Cyan Start-Process -FilePath $Player -ArgumentList @('--instance',$Instance) | Out-Null $sw.Elapsed.TotalSeconds } $lastLaunch = Start-BsrInstanceLaunch + Set-PlayerLogMark # only count [Ready] lines written AFTER this launch, not a prior boot's Say "[*] HD-Adb server port: $env:ANDROID_ADB_SERVER_PORT" DarkGray - # Find the adb endpoint from BlueStacks' OWN per-instance conf ports (re-read each pass: BlueStacks - # writes the actual bound port during boot). Try each candidate, require boot_completed=1, and confirm - # it is really our BlueStacks instance -- so neither a stale port nor a foreign emulator on 5555 can - # mislead us. We pin to that 127.0.0.1: transport (never the transient emulator-XXXX serial). - $serial=$null; $fallback=$null + # Find the adb endpoint from BlueStacks' OWN per-instance conf ports (status.adb_port first). Each pass we + # connect, read get-state, and -- crucially -- HEAL a wedged 'offline' transport (disconnect + reconnect) + # instead of letting a plain 'connect' no-op on it; THEN require boot_completed=1 and confirm the device + # is really our BlueStacks instance. A host-side Player.log [Ready] is an independent 'guest booted' + # signal, so a slow boot is tolerated while a genuinely dead launch still fails fast. + $serial=$null; $fallback=$null; $primary=@(Get-AdbPortCandidates)[0] while(-not $serial){ $elapsed=$sw.Elapsed.TotalSeconds $limit = if($sawLife){ $timeoutSec + 300 } else { $timeoutSec } if($elapsed -ge $limit){ break } + # Guest booted (Player.log [Ready]) but adb never came online even after healing -> conclusive + # failure; don't burn the full slow-boot grace on it. + if($sawReady -and $readyAt -ge 0 -and ($elapsed - $readyAt) -ge 120){ break } + # Clearly NOT a slow boot: nothing alive at all after a short while -> stop instead of waiting it out. + if(-not $sawLife -and $elapsed -ge 90){ break } if($elapsed -ge $timeoutSec -and $sawLife -and -not $extended){ Say "[~] instance is alive but not adb-ready after $timeoutSec s; extending wait (slow BlueStacks boot)." Yellow $extended=$true } Start-Sleep 3; & $Adb @('start-server') *>$null + + # Liveness is instance-specific (so another running instance can't mask a dead launch) and does NOT + # trust the WMI command-line read alone (it reads null / != instance on some hosts -> a false zero). $playerCount = Get-HdPlayerCount $Instance - if($playerCount -gt 0){ $sawLife=$true } - if($playerCount -eq 0 -and ($elapsed - $lastLaunch) -ge 45){ - Say "[~] no HD-Player process seen yet; retrying launch for $Instance ..." Yellow + $ourPortUp = (@(Get-LiveAdbPorts) -contains $primary) + $logAlive = Test-PlayerLogAlive $Instance + $alive = ($playerCount -gt 0 -or $ourPortUp -or $logAlive) + if($alive){ $sawLife=$true } + if(-not $sawReady -and (Test-PlayerLogReady $Instance)){ $sawReady=$true; $sawLife=$true; $readyAt=$elapsed; Say "[+] Player.log: $Instance reached [Ready] (guest booted)" Green } + + # Relaunch ONLY when nothing says the instance is alive -- a false WMI zero no longer spams launches + # while the instance is clearly up (its adb port is listening or Player.log is advancing). + if(-not $alive -and ($elapsed - $lastLaunch) -ge 45){ + Say "[~] instance not detected (no process / adb port / Player.log); retrying launch for $Instance ..." Yellow $lastLaunch = Start-BsrInstanceLaunch } + $cands = @(Get-AdbPortCandidates) foreach($port in $cands){ $cand="127.0.0.1:$port" $conn=(& $Adb @('connect',$cand) 2>&1 | Out-String) if($conn -match '(?i)(connected to|already connected)'){ $sawLife=$true } + $state = Get-AdbState $cand + LogV "connect $cand -> [$(Compact-Line $conn 60)] state=[$state]" + # The slow-boot fix: a connected-but-offline transport never self-heals, so force a fresh socket. + if($state -ne 'device'){ + $rc = Repair-AdbTransport $cand + $state = Get-AdbState $cand + LogV "heal $cand -> reconnect=[$(Compact-Line $rc 60)] state=[$state]" + } + if($state -ne 'device'){ $lastDiag = "$cand state=[$state]"; continue } # boot_completed must be EXACTLY "1" on its own line -- a "device '...:port' not found" error # contains the port digits and would false-positive a naive -match '1'. $out=(& $Adb @('-s',$cand,'shell','getprop','sys.boot_completed') 2>&1 | Out-String) - $lastDiag = "$cand connect=[$(Compact-Line $conn)] boot=[$(Compact-Line $out)]" + $lastDiag = "$cand state=device boot=[$(Compact-Line $out)]" + LogV "getprop sys.boot_completed @ $cand -> [$(Compact-Line $out)]" if(-not (($out -split "`n" | ForEach-Object { $_.Trim() }) -contains '1')){ continue } if(Is-BlueStacks $cand){ $serial=$cand; break } # confirmed: our instance elseif(-not $fallback){ $fallback=$cand } # booted, but identity unconfirmed } if(($sw.Elapsed.TotalSeconds - $lastProgress) -ge 30 -and -not $serial){ - Say ("[~] waiting for adb: elapsed={0:n0}s hdplayer($Instance)={1} candidates={2} last={3}" -f $sw.Elapsed.TotalSeconds,$playerCount,($cands -join ','),$lastDiag) DarkGray + Say ("[~] waiting for adb: elapsed={0:n0}s hdplayer($Instance)={1} ready={2} candidates={3} last={4}" -f $sw.Elapsed.TotalSeconds,$playerCount,$sawReady,($cands -join ','),$lastDiag) DarkGray $lastProgress=$sw.Elapsed.TotalSeconds } } @@ -625,11 +721,11 @@ function Boot-And-Wait([int]$timeoutSec=300){ if(-not $serial){ throw "instance '$Instance' did not boot / become adb-reachable within $([int]$sw.Elapsed.TotalSeconds) s (adb server port $env:ANDROID_ADB_SERVER_PORT; last: $lastDiag)" } $Script:AdbSerial=$serial # Stabilize: a freshly-booted instance (esp. a first boot) restarts adbd a few times, which drops the - # transport -> the next call fails with "device '127.0.0.1:' not found". Wait until a plain shell - # is reliably reachable (3 consecutive hits, reconnecting each time) before handing the serial to callers. + # transport -> the next call fails with "device '127.0.0.1:' not found". HEAL (not just reconnect) + # on each drop until a plain shell is reliably reachable (3 consecutive hits) before handing it over. $stable=0 for($s=0;$s -lt 30 -and $stable -lt 3;$s++){ - AdbConnect + if((Get-AdbState $serial) -ne 'device'){ Repair-AdbTransport $serial | Out-Null } else { AdbConnect } $t=(& $Adb @('-s',$serial,'shell','echo BSR_RDY') 2>&1 | Out-String) if($t -match 'BSR_RDY'){ $stable++ } else { $stable=0; Start-Sleep 3 } } @@ -907,6 +1003,7 @@ function Do-Undo { # Only dispatch when run normally (-File / &). When DOT-SOURCED (. bsr_magisk.ps1) -- e.g. by the test # suite to unit-test the resolver functions -- skip the pipeline so nothing boots or writes. if ($MyInvocation.InvocationName -ne '.') { + Init-BsrLog try { switch($Action){ 'Prep' { Do-Prep } @@ -925,5 +1022,6 @@ if ($MyInvocation.InvocationName -ne '.') { # forever; we only ever started one if Initialize-AdbServer ran (an online action), so tidy it # up so nothing of ours lingers on the port after the tool exits. (runs even if an action threw) if ($Script:AdbServerInit -and (Test-Path -LiteralPath $Adb)) { & $Adb @('kill-server') *>$null } + if ($Script:LogFile) { Say "[*] full step-by-step debug log: $Script:LogFile" DarkGray } } } From 8bf73b257d4293a7c4ff475a1c358df5c313fcb7 Mon Sep 17 00:00:00 2001 From: Jordan Ye <79342877+Jordan231111@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:14:57 -0400 Subject: [PATCH 2/3] refactor(magisk): drop always-on logging from production build (#18) blueStackRoot.cmd no longer writes %TEMP%\bsr_work\bsr_run_*.log on every run -- the adb fix is unchanged, only the file logging is removed. The standalone debug.cmd probe remains the sole writer of a log file (bsr_debug_*.log on the Desktop), used only when the main build still fails. --- blueStackRoot.cmd | 31 ++----------------------------- tools/bsr_magisk.ps1 | 31 ++----------------------------- 2 files changed, 4 insertions(+), 58 deletions(-) diff --git a/blueStackRoot.cmd b/blueStackRoot.cmd index c068c03..2ea0213 100644 --- a/blueStackRoot.cmd +++ b/blueStackRoot.cmd @@ -38194,29 +38194,7 @@ function Redact-UserPath($value){ $s = $s -replace '(?i)(/Users/)([^/]+)(?=$|/)','${1}xxxxx' $s } -# --------- optional redacted file log (extensive logging for debug builds) ---------- -# Stays off until Init-BsrLog runs, which only happens on the real dispatch path -- never when the unit -# tests dot-source this file. Say tees to it; LogV writes verbose adb/boot detail to the FILE ONLY so the -# console output is unchanged. All sinks go through Redact-UserPath, so the account name never lands in a log. -$Script:LogFile = $null -function Init-BsrLog { - if($Script:LogFile){ return } - try{ - $dir = Join-Path $env:TEMP 'bsr_work' - if(-not (Test-Path $dir)){ New-Item -ItemType Directory -Path $dir -Force | Out-Null } - $Script:LogFile = Join-Path $dir ("bsr_run_{0}_{1}.log" -f $Action,(Get-Date -Format 'yyyyMMdd_HHmmss')) - [System.IO.File]::AppendAllText($Script:LogFile, (Redact-UserPath ("==== bsr_magisk $Action $(Get-Date -Format o) PS$($PSVersionTable.PSVersion) instance=$Instance ====`r`n"))) - }catch{ $Script:LogFile = $null } -} -function Say($m,$c='Gray'){ - $r = Redact-UserPath $m - Write-Host $r -ForegroundColor $c - if($Script:LogFile){ try{ [System.IO.File]::AppendAllText($Script:LogFile, ([string]$r + "`r`n")) }catch{} } -} -function LogV($m){ - if(-not $Script:LogFile){ return } - try{ [System.IO.File]::AppendAllText($Script:LogFile, (' {0:HH:mm:ss} {1}' -f (Get-Date), (Redact-UserPath ([string]$m))) + "`r`n") }catch{} -} +function Say($m,$c='Gray'){ Write-Host (Redact-UserPath $m) -ForegroundColor $c } function Fwd($p){ $p -replace '\\','/' } # --------- normalize incoming paths (callers may pass a trailing '\' or a stray '"') ---------- @@ -38829,19 +38807,16 @@ function Boot-And-Wait([int]$timeoutSec=300){ $conn=(& $Adb @('connect',$cand) 2>&1 | Out-String) if($conn -match '(?i)(connected to|already connected)'){ $sawLife=$true } $state = Get-AdbState $cand - LogV "connect $cand -> [$(Compact-Line $conn 60)] state=[$state]" # The slow-boot fix: a connected-but-offline transport never self-heals, so force a fresh socket. if($state -ne 'device'){ - $rc = Repair-AdbTransport $cand + Repair-AdbTransport $cand | Out-Null $state = Get-AdbState $cand - LogV "heal $cand -> reconnect=[$(Compact-Line $rc 60)] state=[$state]" } if($state -ne 'device'){ $lastDiag = "$cand state=[$state]"; continue } # boot_completed must be EXACTLY "1" on its own line -- a "device '...:port' not found" error # contains the port digits and would false-positive a naive -match '1'. $out=(& $Adb @('-s',$cand,'shell','getprop','sys.boot_completed') 2>&1 | Out-String) $lastDiag = "$cand state=device boot=[$(Compact-Line $out)]" - LogV "getprop sys.boot_completed @ $cand -> [$(Compact-Line $out)]" if(-not (($out -split "`n" | ForEach-Object { $_.Trim() }) -contains '1')){ continue } if(Is-BlueStacks $cand){ $serial=$cand; break } # confirmed: our instance elseif(-not $fallback){ $fallback=$cand } # booted, but identity unconfirmed @@ -39137,7 +39112,6 @@ function Do-Undo { # Only dispatch when run normally (-File / &). When DOT-SOURCED (. bsr_magisk.ps1) -- e.g. by the test # suite to unit-test the resolver functions -- skip the pipeline so nothing boots or writes. if ($MyInvocation.InvocationName -ne '.') { - Init-BsrLog try { switch($Action){ 'Prep' { Do-Prep } @@ -39156,7 +39130,6 @@ if ($MyInvocation.InvocationName -ne '.') { # forever; we only ever started one if Initialize-AdbServer ran (an online action), so tidy it # up so nothing of ours lingers on the port after the tool exits. (runs even if an action threw) if ($Script:AdbServerInit -and (Test-Path -LiteralPath $Adb)) { & $Adb @('kill-server') *>$null } - if ($Script:LogFile) { Say "[*] full step-by-step debug log: $Script:LogFile" DarkGray } } } __BSR_MAGISK_END__ diff --git a/tools/bsr_magisk.ps1 b/tools/bsr_magisk.ps1 index 7a60973..54d3cb6 100644 --- a/tools/bsr_magisk.ps1 +++ b/tools/bsr_magisk.ps1 @@ -60,29 +60,7 @@ function Redact-UserPath($value){ $s = $s -replace '(?i)(/Users/)([^/]+)(?=$|/)','${1}xxxxx' $s } -# --------- optional redacted file log (extensive logging for debug builds) ---------- -# Stays off until Init-BsrLog runs, which only happens on the real dispatch path -- never when the unit -# tests dot-source this file. Say tees to it; LogV writes verbose adb/boot detail to the FILE ONLY so the -# console output is unchanged. All sinks go through Redact-UserPath, so the account name never lands in a log. -$Script:LogFile = $null -function Init-BsrLog { - if($Script:LogFile){ return } - try{ - $dir = Join-Path $env:TEMP 'bsr_work' - if(-not (Test-Path $dir)){ New-Item -ItemType Directory -Path $dir -Force | Out-Null } - $Script:LogFile = Join-Path $dir ("bsr_run_{0}_{1}.log" -f $Action,(Get-Date -Format 'yyyyMMdd_HHmmss')) - [System.IO.File]::AppendAllText($Script:LogFile, (Redact-UserPath ("==== bsr_magisk $Action $(Get-Date -Format o) PS$($PSVersionTable.PSVersion) instance=$Instance ====`r`n"))) - }catch{ $Script:LogFile = $null } -} -function Say($m,$c='Gray'){ - $r = Redact-UserPath $m - Write-Host $r -ForegroundColor $c - if($Script:LogFile){ try{ [System.IO.File]::AppendAllText($Script:LogFile, ([string]$r + "`r`n")) }catch{} } -} -function LogV($m){ - if(-not $Script:LogFile){ return } - try{ [System.IO.File]::AppendAllText($Script:LogFile, (' {0:HH:mm:ss} {1}' -f (Get-Date), (Redact-UserPath ([string]$m))) + "`r`n") }catch{} -} +function Say($m,$c='Gray'){ Write-Host (Redact-UserPath $m) -ForegroundColor $c } function Fwd($p){ $p -replace '\\','/' } # --------- normalize incoming paths (callers may pass a trailing '\' or a stray '"') ---------- @@ -695,19 +673,16 @@ function Boot-And-Wait([int]$timeoutSec=300){ $conn=(& $Adb @('connect',$cand) 2>&1 | Out-String) if($conn -match '(?i)(connected to|already connected)'){ $sawLife=$true } $state = Get-AdbState $cand - LogV "connect $cand -> [$(Compact-Line $conn 60)] state=[$state]" # The slow-boot fix: a connected-but-offline transport never self-heals, so force a fresh socket. if($state -ne 'device'){ - $rc = Repair-AdbTransport $cand + Repair-AdbTransport $cand | Out-Null $state = Get-AdbState $cand - LogV "heal $cand -> reconnect=[$(Compact-Line $rc 60)] state=[$state]" } if($state -ne 'device'){ $lastDiag = "$cand state=[$state]"; continue } # boot_completed must be EXACTLY "1" on its own line -- a "device '...:port' not found" error # contains the port digits and would false-positive a naive -match '1'. $out=(& $Adb @('-s',$cand,'shell','getprop','sys.boot_completed') 2>&1 | Out-String) $lastDiag = "$cand state=device boot=[$(Compact-Line $out)]" - LogV "getprop sys.boot_completed @ $cand -> [$(Compact-Line $out)]" if(-not (($out -split "`n" | ForEach-Object { $_.Trim() }) -contains '1')){ continue } if(Is-BlueStacks $cand){ $serial=$cand; break } # confirmed: our instance elseif(-not $fallback){ $fallback=$cand } # booted, but identity unconfirmed @@ -1003,7 +978,6 @@ function Do-Undo { # Only dispatch when run normally (-File / &). When DOT-SOURCED (. bsr_magisk.ps1) -- e.g. by the test # suite to unit-test the resolver functions -- skip the pipeline so nothing boots or writes. if ($MyInvocation.InvocationName -ne '.') { - Init-BsrLog try { switch($Action){ 'Prep' { Do-Prep } @@ -1022,6 +996,5 @@ if ($MyInvocation.InvocationName -ne '.') { # forever; we only ever started one if Initialize-AdbServer ran (an online action), so tidy it # up so nothing of ours lingers on the port after the tool exits. (runs even if an action threw) if ($Script:AdbServerInit -and (Test-Path -LiteralPath $Adb)) { & $Adb @('kill-server') *>$null } - if ($Script:LogFile) { Say "[*] full step-by-step debug log: $Script:LogFile" DarkGray } } } From 925c6cdbe84d8dc6f999e7c126e6d3da8f09a23a Mon Sep 17 00:00:00 2001 From: Jordan Ye <79342877+Jordan231111@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:25:43 -0400 Subject: [PATCH 3/3] chore: lock debug.cmd to CRLF so it always parses + matches the shipped asset (#18) --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f30fe04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# debug.cmd is a hand-run batch launcher with a multi-line `if ( ... )` elevation block; cmd.exe needs +# CRLF to parse that reliably. Force CRLF on every checkout/platform (and every release built from a +# fresh checkout) so the repo copy always matches the shipped asset and always runs. +debug.cmd text eol=crlf