Repository navigation
Expand file tree
/
Copy pathdebug.cmd
More file actions
1480 lines (1398 loc) · 81.6 KB
/
Copy pathdebug.cmd
File metadata and controls
1480 lines (1398 loc) · 81.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
@echo off
setlocal EnableExtensions DisableDelayedExpansion
title BlueStacksRoot Diagnostic
set "SELF=%~f0"
set "BSR_DEBUG_INSTANCE=%~1"
set "BSR_DEBUG_ROOTER=%~2"
set "BSR_DEBUG_HOME=%~dp0"
rem Double-click beside blueStackRoot.cmd for the complete diagnostic report.
rem No arguments needed. Restarts the automatically selected instance; disk probes are read-only.
rem Optional advanced usage: debug.cmd [instance] [path-to-blueStackRoot.cmd]
rem Optional limited check: debug.cmd --files-only [path-to-blueStackRoot.cmd] (no restart or elevation)
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $filesOnly=$env:BSR_DEBUG_INSTANCE -eq '--files-only'; if($env:BSR_DEBUG_INSTANCE -and -not $filesOnly -and $env:BSR_DEBUG_INSTANCE -cnotmatch '^[A-Za-z0-9_]+$'){throw 'Invalid instance name.'}; $admin=([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator); if(-not $admin -and -not $filesOnly){$a='';if($env:BSR_DEBUG_INSTANCE){$a=$env:BSR_DEBUG_INSTANCE};if($env:BSR_DEBUG_ROOTER){$a+=' '+[char]34+$env:BSR_DEBUG_ROOTER+[char]34};if($a){Start-Process -FilePath $env:SELF -ArgumentList $a -Verb RunAs -ErrorAction Stop}else{Start-Process -FilePath $env:SELF -Verb RunAs -ErrorAction Stop};exit 3010}; $t=[IO.File]::ReadAllText($env:SELF); $i=$t.IndexOf('#__BSR'+'_DEBUG_PS__'); if($i -lt 0){throw 'Diagnostic marker missing.'}; & ([scriptblock]::Create($t.Substring($i))) -Instance $env:BSR_DEBUG_INSTANCE -FilesOnly:$filesOnly -RooterPath $env:BSR_DEBUG_ROOTER"
set "BSR_DEBUG_RC=%errorlevel%"
if "%BSR_DEBUG_RC%"=="3010" exit /b 0
echo.
echo Done. Use the Full log path printed above when reporting the problem.
pause
endlocal & exit /b %BSR_DEBUG_RC%
#__BSR_DEBUG_PS__
param([string]$Instance, [switch]$FilesOnly, [string]$RooterPath, [string]$LogDirectory,
[ValidateSet('','File','Temp','Payload')][string]$ProbeMode='', [string]$ProbePath, [string]$ProbeScratch)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
$hostText = [IO.File]::ReadAllText($env:SELF)
$hostStart = $hostText.IndexOf('__BSR_HOST_' + 'BEGIN__')
$hostStop = $hostText.IndexOf('__BSR_HOST_' + 'END__')
if ($hostStart -lt 0 -or $hostStop -le $hostStart) { throw 'Diagnostic HOST helpers are missing; download the complete debug.cmd.' }
$hostStart = $hostText.IndexOf([char]10, $hostStart) + 1
. ([scriptblock]::Create($hostText.Substring($hostStart, $hostStop - $hostStart)))
# ----------------------------- logging / redaction -----------------------------
function Redact($v){ Redact-UserPath $v }
$script:DiagnosticFailures=0
$script:DiagnosticChecks=[ordered]@{}
$script:LogWriteFailed=$false
$script:CaptureGuestErrors=$false;$script:GuestAdbFailures=0
function New-DiagnosticLog([string[]]$Directories){
$name='bsr_debug_'+(Get-Date -Format 'yyyyMMdd_HHmmss_fff')+'_'+$PID+'.log'
foreach($directory in $Directories){
if(-not $directory){continue}
try{
[void][IO.Directory]::CreateDirectory($directory)
$path=Join-Path $directory $name
[IO.File]::WriteAllText($path,'',(New-Object Text.UTF8Encoding($true)))
return $path
}catch{Write-Host (Redact "Log destination unavailable: $directory; $($_.Exception.Message)") -ForegroundColor Yellow}
}
throw 'No diagnostic log destination is writable.'
}
$LogFile=''
if(-not $ProbeMode){
$local=[Environment]::GetFolderPath('LocalApplicationData')
$directories=@($(if($local){Join-Path $local 'BlueStacksRoot\Logs'}),$env:TEMP,[Environment]::GetFolderPath('Desktop'))
if($LogDirectory){$directories=@($LogDirectory)}
$LogFile=New-DiagnosticLog $directories
}
function Log($m,$c='Gray'){
$line = ('{0:HH:mm:ss.fff} {1}' -f (Get-Date), (Redact $m))
if($ProbeMode){[Console]::WriteLine($line)}else{try { Write-Host $line -ForegroundColor $c } catch { Write-Host $line }}
if($LogFile){
try { [IO.File]::AppendAllText($LogFile,$line+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false))) }
catch {
if(-not $script:LogWriteFailed){Write-Host (Redact "[!] Log write failed: $LogFile; $($_.Exception.Message). Remaining output is also shown in this console.") -ForegroundColor Red}
$script:LogWriteFailed=$true
}
}
}
function Section($t){ Log ''; Log ('==================== ' + $t + ' ====================') Cyan }
function Compact($s,[int]$max=100){ if($null -eq $s){ return '' }; $x = (((Redact $s) -replace "`r?`n",' | ').Trim()); if($x.Length -gt $max){ $x.Substring(0,$max-3)+'...' } else { $x } }
function Log-Failure($stage,$failure){
$script:DiagnosticFailures++
Log "[!] $stage" Yellow
Log "category=$($failure.CategoryInfo.Category); id=$($failure.FullyQualifiedErrorId)"
if($null -ne $failure.TargetObject){Log "target: $(Compact ([string]$failure.TargetObject) 1500)"}
if($failure.InvocationInfo){
Log "source: $($failure.InvocationInfo.ScriptName):$($failure.InvocationInfo.ScriptLineNumber); command=$($failure.InvocationInfo.MyCommand.Name)"
if($failure.InvocationInfo.PositionMessage){Log "position: $(Compact $failure.InvocationInfo.PositionMessage 2000)"}
}
if($failure.ScriptStackTrace){Log "stack: $(Compact $failure.ScriptStackTrace 2500)"}
$e=$failure.Exception
for($i=0;$e -and $i -lt 6;$i++){
$hr=[BitConverter]::ToUInt32([BitConverter]::GetBytes([int]$e.HResult),0)
$native=if($e -is [ComponentModel.Win32Exception]){$e.NativeErrorCode}else{'n/a'}
Log ("exception[{0}] {1}; HRESULT=0x{2:X8}; Win32={3}; {4}" -f $i,$e.GetType().FullName,$hr,$native,$e.Message)
$e=$e.InnerException
}
if($failure.ErrorDetails){Log "details: $($failure.ErrorDetails.Message)"}
}
function Set-DiagnosticCheck($name,$status,$detail){
$script:DiagnosticChecks[$name]="$status - $detail"
Log "[$status] $name : $detail"
}
function Report-DiagnosticSummary {
Section 'DIAGNOSTIC SUMMARY'
foreach($name in $script:DiagnosticChecks.Keys){Log "$name : $($script:DiagnosticChecks[$name])"}
Log "Captured errors=$script:DiagnosticFailures; logWriteFailed=$script:LogWriteFailed"
Log 'ADB readiness only describes the emulator connection. It does not verify rooter payload preparation or a completed root installation.'
Log "Full log: $LogFile" Cyan
}
function Report-PolicyEvents {
Section 'RELATED WINDOWS EVENTS (last hour, bounded search)'
foreach($channel in @('Microsoft-Windows-CodeIntegrity/Operational','Microsoft-Windows-AppLocker/EXE and DLL','Microsoft-Windows-Windows Defender/Operational','Application')){
try{
$all=@(Get-WinEvent -FilterHashtable @{LogName=$channel;StartTime=(Get-Date).AddHours(-1)} -MaxEvents 200 -ErrorAction Stop)
$related=@($all | Where-Object {$_.Message -match '(?i)HD-Player\.exe|HD-Adb\.exe|Root\.vhd|BlueStacks|bsr_(engine|magisk|work)'} | Select-Object -First 10)
Log "$channel : scanned=$($all.Count); matched=$($related.Count); scanLimit=200; reportLimit=10"
foreach($event in $related){Log "event=$($event.Id); record=$($event.RecordId); time=$($event.TimeCreated.ToString('s')); level=$($event.LevelDisplayName); $(Compact $event.Message 2500)"}
}catch{Log "$channel : unavailable or no matching events; $(Compact $_.Exception.Message 300)"}
}
Log 'An empty, disabled, or unavailable event log does not prove that a security policy allowed the launch.'
}
function Get-DiagnosticHash([string]$path){ Get-BsrFileHash $path }
function Get-DiagnosticPathFlags([long]$Attributes){
# Values from WinNT.h; .NET Framework does not name all Cloud Files flags.
# 0x40000 is EA for GetFileAttributes (RECALL_ON_OPEN only in enumeration).
[pscustomobject]@{
ReparsePoint=[bool]($Attributes -band 0x400)
Offline=[bool]($Attributes -band 0x1000)
Pinned=[bool]($Attributes -band 0x80000)
Unpinned=[bool]($Attributes -band 0x100000)
RecallOnDataAccess=[bool]($Attributes -band 0x400000)
}
}
function Get-DiagnosticSyncRoot([string]$Path){
if(-not $Path){return ''}
$full=[IO.Path]::GetFullPath($Path).TrimEnd('\','/')
foreach($name in @('OneDrive','OneDriveConsumer','OneDriveCommercial')){
$root=[Environment]::GetEnvironmentVariable($name)
if(-not $root){continue}
$root=[IO.Path]::GetFullPath($root).TrimEnd('\','/')
if($full.Equals($root,[StringComparison]::OrdinalIgnoreCase) -or $full.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase)){return $name}
}
return ''
}
function Report-DiagnosticPath([string]$label,[string]$path){
if(-not $path){Log "$label : not set";return}
Log "$label : $path; chars=$($path.Length); OneDriveRoot=$(Get-DiagnosticSyncRoot $path); shortNameComponent=$([bool]($path -match '~[0-9]'))"
try{
$attributes=[IO.File]::GetAttributes($path)
$flags=Get-DiagnosticPathFlags ([long]$attributes)
Log ("attributes=0x{0:X8} ({1}); reparse={2}; offline={3}; pinned={4}; unpinned={5}; recallOnDataAccess={6}" -f [long]$attributes,$attributes,$flags.ReparsePoint,$flags.Offline,$flags.Pinned,$flags.Unpinned,$flags.RecallOnDataAccess)
if(-not ($attributes -band [IO.FileAttributes]::Directory)){
$file=New-Object IO.FileInfo $path
Log "bytes=$($file.Length); modifiedUtc=$($file.LastWriteTimeUtc.ToString('o'))"
}
if($flags.Offline -or $flags.RecallOnDataAccess){Log 'Content may require retrieval from a storage provider. The read probe below tests availability; no pin or sync settings are changed.'}
}catch{Log-Failure "$label metadata" $_}
}
function Get-DiagnosticBlock([string]$text,[string]$name){
$begin='__BSR_'+$name+'_BEGIN__';$end='__BSR_'+$name+'_END__'
$starts=[regex]::Matches($text,'(?m)^'+[regex]::Escape($begin)+'\r?$')
$ends=[regex]::Matches($text,'(?m)^'+[regex]::Escape($end)+'\r?$')
if($starts.Count -ne 1 -or $ends.Count -ne 1 -or $ends[0].Index -le $starts[0].Index){throw "Missing, duplicate or incomplete $name block."}
$start=$text.IndexOf([char]10,$starts[0].Index)+1
$text.Substring($start,$ends[0].Index-$start)
}
function Remove-DiagnosticScratch([string]$path,[string]$parent){
$full=[IO.Path]::GetFullPath($path)
$root=[IO.Path]::GetFullPath($parent).TrimEnd('\','/')+'\'
if(-not $full.StartsWith($root,[StringComparison]::OrdinalIgnoreCase) -or [IO.Path]::GetFileName($full) -cnotmatch '^bsr_diag_probe_[a-f0-9]{32}$'){throw "Unsafe diagnostic cleanup path: $full"}
if([IO.Directory]::Exists($full)){Remove-Item -LiteralPath $full -Recurse -Force -ErrorAction Stop}
}
function Invoke-DiagnosticTempProbe([string]$root,[string]$scratch=''){
Report-DiagnosticPath 'Temp root' $root
if(-not $root -or -not [IO.Directory]::Exists($root)){throw 'The configured temporary directory is absent or inaccessible.'}
if(-not $scratch){$scratch=Join-Path $root ('bsr_diag_probe_'+[guid]::NewGuid().ToString('N'))}
$step='create directory'
try{
New-Item -Path $scratch -ItemType Directory -Force -ErrorAction Stop | Out-Null
$path=Join-Path $scratch 'probe [1].bin'
$step='write';[IO.File]::WriteAllBytes($path,[byte[]](1,2,3,4))
$step='provider lookup';$file=Get-Item -LiteralPath $path -ErrorAction Stop
if($file.Length -ne 4){throw 'Temporary file size changed after writing.'}
$step='read';$bytes=[IO.File]::ReadAllBytes($path)
if([BitConverter]::ToString($bytes) -cne '01-02-03-04'){throw 'Temporary file content changed after writing.'}
$step='rename';$renamed=Join-Path $scratch 'renamed.bin';[IO.File]::Move($path,$renamed)
$step='delete';Remove-Item -LiteralPath $renamed -Force -ErrorAction Stop
Log 'TEMP create/write/provider lookup/read/rename/delete: PASS'
$step='PowerShell script launch';$scriptPath=Join-Path $scratch 'script [1].ps1'
[IO.File]::WriteAllText($scriptPath,"[Console]::WriteLine('BSR_TEMP_SCRIPT_OK')",(New-Object Text.UTF8Encoding($true)))
$result=Invoke-BsrNative 'powershell.exe' @('-NoProfile','-ExecutionPolicy','Bypass','-File',$scriptPath) 10
if($result.ExitCode -ne 0 -or $result.Output.Trim() -cne 'BSR_TEMP_SCRIPT_OK'){throw "Temporary PowerShell script failed (exit=$($result.ExitCode)): $($result.Output)"}
Log 'TEMP PowerShell -File launch: PASS'
}catch{Log-Failure "TEMP $step (scratch=$scratch)" $_;throw}
finally{Remove-DiagnosticScratch $scratch $root}
}
function Invoke-DiagnosticPayloadProbe([string]$path,[string]$scratch=''){
Report-DiagnosticPath 'Rooter before read' $path
# Bound both memory and the total child process lifetime. A cloud provider can
# stall reads; the parent retains metadata already printed before the timeout.
$file=New-Object IO.FileInfo $path
if($file.Length -gt 128MB){throw 'Rooter exceeds the 128 MB diagnostic read limit.'}
$bytes=[IO.File]::ReadAllBytes($path)
Log "Rooter read: PASS; bytes=$($bytes.Length); SHA256=$(Get-BsrBytesHash $bytes)"
$text=[Text.Encoding]::UTF8.GetString($bytes)
$magiskAst=$null
foreach($name in @('ENGINE','MAGISK','HOST','LAUNCHER')){
if($name -eq 'LAUNCHER' -and -not [regex]::IsMatch($text,'(?m)^__BSR_LAUNCHER_BEGIN__\r?$')){Log 'LAUNCHER block absent (older batch-menu build); continuing payload checks.';continue}
$code=Get-DiagnosticBlock $text $name
$tokens=$null;$errors=$null
$parsed=[Management.Automation.Language.Parser]::ParseInput($code,[ref]$tokens,[ref]$errors)
if($errors){throw "$name script parse error: $($errors[0].Message) at line $($errors[0].Extent.StartLineNumber)"}
Log "$name script: parses; SHA256=$(Get-BsrBytesHash ([Text.Encoding]::UTF8.GetBytes($code)))"
if($name -eq 'MAGISK'){$magiskAst=$parsed}
}
# Import only the payload functions, never the orchestrator's top-level code,
# dispatcher, player/disk functions or Defender exclusions. This also exercises
# older rooters' extraction code, instead of silently testing the latest code.
$functions=@('Get-SelfText','Extract-Block','Get-AvHint','Fail-Damaged','Assert-Extracted','Get-BlockBytes','Ensure-MagiskApk','Ensure-BsrSu','Ensure-Debugfs')
foreach($name in $functions){
$nodes=@($magiskAst.FindAll({param($n) $n -is [Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -ceq $name},$false))
if($nodes.Count -ne 1){throw "Cannot isolate payload probe: expected one $name function in this rooter."}
Log "Payload source: MAGISK/$name, block line $($nodes[0].Extent.StartLineNumber)"
. ([scriptblock]::Create($nodes[0].Extent.Text))
}
$shaNode=@($magiskAst.FindAll({param($n) $n -is [Management.Automation.Language.AssignmentStatementAst] -and $n.Left -is [Management.Automation.Language.VariableExpressionAst] -and $n.Left.VariablePath.UserPath -ceq 'BSR_SU_SHA'},$false))
if($shaNode.Count -ne 1){throw 'Cannot isolate payload probe: bootstrap hash declaration is missing or ambiguous.'}
$shaLiteral=@($shaNode[0].Right.FindAll({param($n) $n -is [Management.Automation.Language.StringConstantExpressionAst]},$true))
if($shaLiteral.Count -ne 1 -or $shaNode[0].Right.Extent.Text.Trim() -cne $shaLiteral[0].Extent.Text){throw 'Bootstrap hash must be a literal string for an isolated probe.'}
$BSR_SU_SHA=$shaLiteral[0].Value
if($BSR_SU_SHA -cnotmatch '^[a-f0-9]{64}$'){throw 'Invalid bootstrap hash declaration.'}
$root=$env:TEMP
if(-not $root -or -not [IO.Directory]::Exists($root)){throw 'The rooter TEMP directory is absent or inaccessible.'}
if(-not $scratch){$scratch=Join-Path $root ('bsr_diag_probe_'+[guid]::NewGuid().ToString('N'))}
[void][IO.Directory]::CreateDirectory($scratch)
$oldTemp=$env:TEMP;$oldTmp=$env:TMP
try{
$env:TEMP=$scratch;$env:TMP=$scratch
$SelfCmd=$path;$Here=$scratch
$script:WorkDir=Join-Path $scratch 'bsr_work\session'
$script:MagiskApk=$null;$script:BsrSu=$null;$script:Debugfs=$null
$script:SelfCmdTextCache=@{};$script:SelfCmdTextCache[$path]=$text;$script:SelfReadCount=0
function Say($m,$c='Gray'){Log $m $c}
Log "Isolated extraction under $scratch. Existing rooter caches and BlueStacks files are untouched."
Log 'Scope: embedded payloads. External APK overrides and existing cache contents are not validated by this probe.'
foreach($name in @('Ensure-MagiskApk','Ensure-BsrSu','Ensure-Debugfs')){
$timer=[Diagnostics.Stopwatch]::StartNew()
Log "BEGIN $name"
try{
& $name
if($name -eq 'Ensure-Debugfs'){
$tool=Invoke-BsrNative $script:Debugfs @('-V') 10
if($tool.ExitCode){throw "debugfs executable/DLL check failed (exit=$($tool.ExitCode)): $($tool.Output)"}
Log "debugfs executable/DLL check: $($tool.Output)"
}
Log "PASS $name; elapsedMs=$($timer.ElapsedMilliseconds)"
}
catch{Log-Failure "Payload preparation: $name" $_}
}
if($script:DiagnosticFailures -eq 0){
foreach($payload in @($script:MagiskApk,$script:BsrSu,$script:Debugfs)){
Log "prepared file=$payload; bytes=$((Get-Item -LiteralPath $payload -Force).Length); SHA256=$(Get-DiagnosticHash $payload)"
}
}
}finally{
$env:TEMP=$oldTemp;$env:TMP=$oldTmp
Remove-DiagnosticScratch $scratch $root
}
}
function Invoke-DiagnosticWorker([string]$mode,[string]$path,[int]$TimeoutSec=45){
# Values stay PowerShell single-quoted data inside an encoded command. Neither
# cmd.exe nor native command-line splitting interprets the paths as code.
function Literal([string]$s){"'"+$s.Replace("'","''")+"'"}
$scratch='';$root=''
if($mode -in @('Temp','Payload')){
$root=if($mode -eq 'Temp'){$path}else{$env:TEMP}
if($root){$scratch=Join-Path $root ('bsr_diag_probe_'+[guid]::NewGuid().ToString('N'))}
}
$command='$ErrorActionPreference=''Stop''; $ProgressPreference=''SilentlyContinue''; try { $env:SELF='+(Literal $env:SELF)+'; $t=[IO.File]::ReadAllText($env:SELF); $i=$t.IndexOf(''#__BSR''+''_DEBUG_PS__''); & ([scriptblock]::Create($t.Substring($i))) -ProbeMode '+(Literal $mode)+' -ProbePath '+(Literal $path)
if($scratch){$command+=' -ProbeScratch '+(Literal $scratch)}
$command+=' } catch { [Console]::Error.WriteLine(''Worker bootstrap failed: ''+$_.Exception.ToString()); exit 1 }'
$encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
try{
$result=Invoke-BsrNative 'powershell.exe' @('-NoProfile','-OutputFormat','Text','-EncodedCommand',$encoded) $TimeoutSec -ReturnOnTimeout
foreach($line in ($result.Output -split '\r?\n')){if($line){Log $line}}
if($result.TimedOut){Log "[!] $mode probe timed out after ${TimeoutSec}s for $path. Partial output above identifies the last operation reached." Yellow}
}finally{
# The worker's finally cannot run after a timeout kills it. The parent knows
# its exact scratch path and removes only that directory, never old caches.
if($scratch){Remove-DiagnosticScratch $scratch $root}
}
$result.ExitCode
}
function Report-FileContext([string]$rooter){
Section 'SCRIPT, CLOUD STORAGE AND TEMP PATHS'
Log "diagnostic=$env:SELF; rooter=$rooter; workingDirectory=$((Get-Location).Path)"
Log "culture=$([Globalization.CultureInfo]::CurrentCulture.Name); uiCulture=$([Globalization.CultureInfo]::CurrentUICulture.Name); languageMode=$($ExecutionContext.SessionState.LanguageMode); process64bit=$([Environment]::Is64BitProcess)"
Log "Legacy resolver regex accepts BSR_INSTALL=$([bool]('BSR_INSTALL=test' -match '^(BSR_[A-Z]+)=(.*)$')); case-sensitive=$([bool]('BSR_INSTALL=test' -cmatch '^(BSR_[A-Z]+)=(.*)$'))"
Log "Desktop=$([Environment]::GetFolderPath('Desktop')); LocalAppData=$([Environment]::GetFolderPath('LocalApplicationData'))"
foreach($name in @('TEMP','TMP','USERPROFILE','OneDrive','OneDriveConsumer','OneDriveCommercial')){Log "$name=$([Environment]::GetEnvironmentVariable($name))"}
Log 'A OneDrive directory name or reparse point alone does not establish an I/O failure. This log location is independent of the rooter location.'
$probes=@(@('Diagnostic file','File',$env:SELF),@('Temporary directory','Temp',$env:TEMP))
$launcherTemp=[IO.Path]::GetTempPath()
if(-not (Test-SamePath $launcherTemp $env:TEMP)){$probes+=,@('Launcher temporary directory','Temp',$launcherTemp)}
$probes+=,@('Rooter payloads','Payload',$rooter)
foreach($probe in $probes){
try{
$code=Invoke-DiagnosticWorker $probe[1] $probe[2]
if($code -eq 0){Set-DiagnosticCheck $probe[0] 'PASS' 'probe completed'}
else{$script:DiagnosticFailures++;Set-DiagnosticCheck $probe[0] 'FAIL' "probe exited $code; see the operation and exception above"}
}catch{Log-Failure $probe[0] $_;Set-DiagnosticCheck $probe[0] 'FAIL' 'probe could not complete'}
}
}
if($ProbeMode){
try{
switch($ProbeMode){
'File' {Report-DiagnosticPath 'Diagnostic file' $ProbePath;Log "SHA256=$(Get-DiagnosticHash $ProbePath)"}
'Temp' {Invoke-DiagnosticTempProbe $ProbePath $ProbeScratch}
'Payload' {Invoke-DiagnosticPayloadProbe $ProbePath $ProbeScratch}
}
}catch{Log-Failure "$ProbeMode probe" $_}
exit ([int]($script:DiagnosticFailures -gt 0))
}
function Report-HostDetails {
Section 'WINDOWS CONTEXT'
$admin=([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
Log "elevated=$admin; process64bit=$([Environment]::Is64BitProcess); OS64bit=$([Environment]::Is64BitOperatingSystem); languageMode=$($ExecutionContext.SessionState.LanguageMode)"
try{$cv=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -EA Stop;Log "Windows build=$($cv.CurrentBuildNumber).$($cv.UBR); displayVersion=$($cv.DisplayVersion)"}catch{Log-Failure 'Windows build query' $_}
try{
$policy=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -EA Stop
foreach($key in @('EnableLUA','ValidateAdminCodeSignatures','EnableSecureUIAPaths','ConsentPromptBehaviorAdmin')){Log "UAC $key=$($policy.$key)"}
}catch{Log-Failure 'UAC policy query' $_}
try{
$dg=Get-CimInstance -Namespace root\Microsoft\Windows\DeviceGuard -ClassName Win32_DeviceGuard -OperationTimeoutSec 5 -EA Stop
Log "DeviceGuard: VBS=$($dg.VirtualizationBasedSecurityStatus); kernelCI=$($dg.CodeIntegrityPolicyEnforcementStatus); userCI=$($dg.UsermodeCodeIntegrityPolicyEnforcementStatus) (CI: 0=off, 1=audit, 2=enforced)"
}catch{Log "DeviceGuard unavailable: $(Compact $_.Exception.Message 250)"}
foreach($name in @('vds','AppIDSvc','WinDefend')){
try{$svc=Get-Service -Name $name -EA Stop;Log "service $name : status=$($svc.Status); startType=$($svc.StartType)"}catch{Log "service $name : unavailable"}
}
Log 'A stopped manual-start service alone does not establish a failure.'
try{Get-CimInstance -Namespace root\SecurityCenter2 -ClassName AntivirusProduct -OperationTimeoutSec 5 -EA Stop | ForEach-Object {Log "registered antivirus: $($_.displayName); productState=$($_.productState)"}}catch{Log "Antivirus registration query unavailable: $(Compact $_.Exception.Message 250)"}
try{Log "Storage module: $((Get-Command Mount-DiskImage -EA Stop).Module.Version); virtdisk.dll=$((Get-Item (Join-Path $env:WINDIR 'System32\virtdisk.dll') -EA Stop).VersionInfo.FileVersion)"}catch{Log-Failure 'Virtual disk provider components' $_}
foreach($path in @($Install,$DataRoot)){
try{
$volume=New-Object IO.DriveInfo ([IO.Path]::GetPathRoot($path))
Log "volume for $path : type=$($volume.DriveType); filesystem=$($volume.DriveFormat); freeBytes=$($volume.AvailableFreeSpace)"
Log "directory ACL $path : $((Get-Acl -LiteralPath $path -EA Stop).Sddl)"
}catch{Log-Failure 'Volume/directory inspection' $_}
}
foreach($path in @($Player,"$Player.bak")){
if(-not(Test-Path -LiteralPath $path)){continue}
try{
$file=Get-Item -LiteralPath $path -EA Stop
Log "player file: $path; bytes=$($file.Length); attributes=$($file.Attributes); version=$($file.VersionInfo.FileVersion); SHA256=$(Get-DiagnosticHash $path)"
Log "signature: $((Get-AuthenticodeSignature -LiteralPath $path -EA Stop).Status)"
$zone=Get-Content -LiteralPath $path -Stream Zone.Identifier -EA SilentlyContinue | Where-Object {$_ -match '^ZoneId=\d+$'}
Log "download zone: $(if($zone){$zone -join ','}else{'no ZoneId recorded'})"
if($path -eq $Player -and $file.Length -lt 64MB){
$text=[Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($path))
$manifest=[regex]::Match($text,'<requestedExecutionLevel\b[^>]{0,200}>')
Log "execution manifest: $(if($manifest.Success){$manifest.Value}else{'not found by bounded text scan'})"
}
}catch{Log-Failure 'Player file inspection' $_}
}
}
function Probe-RootDisk([string]$path){
Section 'READ-ONLY DISK PROBE'
if(-not(Test-Path -LiteralPath $path)){$script:DiagnosticFailures++;Set-DiagnosticCheck 'Disk attach' 'FAIL' "Root image not found: $path";return $true}
if(@(Get-Process -Name HD-Player -EA SilentlyContinue).Count){Set-DiagnosticCheck 'Disk attach' 'SKIP' 'a running player may share this master; close it for mount evidence';return $true}
$mounted=$null;$detached=$true
try{
$format=Get-BsrDiskFormat $path
Log "probe path=$path; detected=$format; extension=$([IO.Path]::GetExtension($path)); access=ReadOnly"
if($format -notin @('VHD','VHDX')){$script:DiagnosticFailures++;Set-DiagnosticCheck 'Disk attach' 'FAIL' 'unsupported or incomplete image; no attach attempted';return $true}
$existing=Get-DiskImage -ImagePath $path -StorageType $format -EA Stop
if($existing.Attached){Set-DiagnosticCheck 'Disk attach' 'SKIP' 'image already attached; existing attachment left alone';return $true}
$mounted=Mount-DiskImage -ImagePath $path -StorageType $format -Access ReadOnly -NoDriveLetter -PassThru -EA Stop
$disk=$mounted | Get-Disk -EA Stop
Log "attach succeeded: disk=$($disk.Number); bytes=$($disk.Size); style=$($disk.PartitionStyle); logicalSector=$($disk.LogicalSectorSize); physicalSector=$($disk.PhysicalSectorSize); readOnly=$($disk.IsReadOnly); offline=$($disk.IsOffline)"
foreach($part in @(Get-Partition -DiskNumber $disk.Number -EA Stop)){Log "partition=$($part.PartitionNumber); offset=$($part.Offset); bytes=$($part.Size); type=$($part.Type)"}
Set-DiagnosticCheck 'Disk attach' 'PASS' 'read-only attach and partition inspection completed'
}catch{Log-Failure 'Disk attach/inspection failed (no disk writes attempted)' $_;Set-DiagnosticCheck 'Disk attach' 'FAIL' 'see the native disk error above'}
finally{
if($mounted){try{Dismount-DiskImage -InputObject $mounted -EA Stop | Out-Null;Log 'Our read-only attachment was detached.'}catch{$detached=$false;Log-Failure 'Detach failed; player launch will be skipped' $_;Set-DiagnosticCheck 'Disk attach' 'FAIL' 'could not detach our read-only attachment'}}
}
return $detached
}
Log "BlueStacksRoot diagnostic 2026-10-07 (files, payloads, host and ADB)" Green
Log 'Privacy: user-directory names are masked. Technical paths, ACLs, Windows errors and related event details are kept. Review the log before posting; it is never uploaded automatically.'
Log "log file : $(Redact $LogFile)"
Log "OS : $([Environment]::OSVersion.VersionString) PowerShell $($PSVersionTable.PSVersion)"
if(-not $FilesOnly){Log 'Full diagnostic: automatically checking files, payloads, Windows, disks and the emulator connection. No command-line options are needed.' Cyan}
try {
if(-not $RooterPath){$RooterPath=Join-Path (Split-Path -Parent $env:SELF) 'blueStackRoot.cmd'}
Report-FileContext $RooterPath
if($FilesOnly){
Set-DiagnosticCheck 'BlueStacks / ADB' 'SKIP' 'files-only mode; no player restart, disk attach or root changes'
return
}
# ----------------------------- validated layout discovery -----------------------------
function Prop($o,$n){ Get-ObjectProperty $o $n }
function Norm($v){ Normalize-DiscoveryPath $v }
function ExeFrom($v){ Get-ExePathFromCommand $v }
function Records { Get-RegBlueStacksRecords }
function DataAt($v){ Get-DataRootFromPath $v }
function InstallAt($v){ Get-InstallRootFromPath $v }
function Same($a,$b){ Test-SamePath $a $b }
function RecordData($r){ Get-RecordDataRoot $r }
$custom=$null;$customFile=if($env:BSR_DEBUG_HOME){Join-Path $env:BSR_DEBUG_HOME 'bluestacksconfig.txt'}else{$null}
if($customFile-and(Test-Path -LiteralPath $customFile)){try{$custom=([IO.File]::ReadAllText($customFile)).Trim()}catch{Log-Failure 'Saved custom path read' $_;return}}
$records=@(Records);$customData=DataAt $custom;$customInstall=InstallAt $custom
if($custom-and-not$customData-and-not$customInstall){throw "Saved custom path is not a valid install or data folder: $custom"}
$DataRoot=$customData
if(-not$DataRoot-and$customInstall){foreach($r in $records){$ri=InstallAt $r.InstallDir;if($ri-and(Same $ri $customInstall)){$DataRoot=RecordData $r;if($DataRoot){break}}}}
if(-not$DataRoot){foreach($r in $records){$DataRoot=RecordData $r;if($DataRoot){break}}}
if(-not$DataRoot){throw 'No validated BlueStacks data folder was found in the registry; use option 8 in blueStackRoot.cmd first.'}
$Install=$customInstall
if(-not$Install){foreach($r in $records){$rd=RecordData $r;$ri=InstallAt $r.InstallDir;if($ri-and$rd-and(Same $rd $DataRoot)){$Install=$ri;break}}}
if(-not$Install){
$candidates=@((@(Get-RuntimeInstallRoots)+@($records | ForEach-Object {InstallAt $_.InstallDir})) | Where-Object {$_} | Select-Object -Unique)
if($candidates.Count -gt 1){throw 'Several installations were found with no match to the selected data folder. Set the intended install folder with option 8.'}
if($candidates.Count -eq 1){$Install=$candidates[0]}
}
if(-not$Install){throw 'No validated BlueStacks install folder was found; use option 8 in blueStackRoot.cmd first.'}
$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'
function Quote-NativeArgument([string]$value){ Quote-BsrNativeArgument $value }
function Adb([string[]]$a){
try{
$result=Invoke-BsrNative $AdbExe $a 8
if($result.ExitCode){
Log "ADB exit=$($result.ExitCode); command=$($a -join ' '); $(Compact $result.Output 800)" DarkYellow
if($script:CaptureGuestErrors){$script:GuestAdbFailures++}
}
$result.Output
}catch{if($script:CaptureGuestErrors){$script:GuestAdbFailures++};Log-Failure 'ADB invocation failed' $_;"ERR: $($_.Exception.Message)"}
}
function State($serial){
$o=Adb @('-s',$serial,'get-state')
$state=@($o -split "`r?`n" | ForEach-Object {$_.Trim()} | Where-Object {$_ -match '^(device|offline|unauthorized|unknown)$'} | Select-Object -Last 1)
if($state.Count){$state[0]}else{'unavailable'}
}
# ----------------------------- instance selection -----------------------------
function Get-ConfInstances {
if(-not (Test-Path -LiteralPath $Conf)){ return @() }
try{ $ct=[IO.File]::ReadAllText($Conf) }catch{Log-Failure 'Instance configuration read' $_;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 -LiteralPath $eng){
$pick = Get-ChildItem -LiteralPath $eng -Directory -EA Stop | 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
}
if($Instance -cnotmatch '^[A-Za-z0-9_]+$' -or $allInst -notcontains $Instance){throw 'Selected instance is not a registered BlueStacks instance. Check its internal name in bluestacks.conf.'}
function Get-ConfPort($name,$key){
if(-not (Test-Path -LiteralPath $Conf)){ return $null }
try{ $ct=[IO.File]::ReadAllText($Conf) }catch{Log-Failure 'ADB port configuration read' $_;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){
$listeners=@(Get-BsrTcpListeners | Where-Object {$_.LocalPort -ge $lo -and $_.LocalPort -le $hi} |
ForEach-Object {[pscustomobject]@{Port=$_.LocalPort;PID=$_.OwningProcess;Proc=(Get-Process -Id $_.OwningProcess -EA SilentlyContinue).Name}})
,@($listeners | Sort-Object Port,PID -Unique)
}
function Fmt-Band($b){ ($b | ForEach-Object { "$($_.Port)/$($_.Proc)" }) -join ',' }
function Test-CmdLine($cmdLine,$name){ Test-HdPlayerInstance $cmdLine $name }
function Get-ExactPlayers($name){
$players=@(Get-BsrInstanceProcesses $name $PlayerLog)
if($script:launchedPid){
$launched=Get-Process -Id $script:launchedPid -EA SilentlyContinue
if($launched -and $launched.Name -eq 'HD-Player' -and $launched.StartTime -eq $script:launchedStart){$players+= $launched}
}
@($players | Sort-Object Id -Unique)
}
function Get-BsrDiagnosticTargetPort($listeners,$ids,$preferred){
@($listeners | Where-Object {$ids -contains $_.PID} |
Sort-Object @{Expression={if($_.Port -eq [int]$preferred){0}else{1}}},Port |
Select-Object -First 1 -ExpandProperty Port)
}
function Probe-Player($name){
$r = [ordered]@{ proc=0; exact=0; ids=@(); wmi_total=0; wmi_match=0; cmds=@() }
$r.proc = @(Get-Process -Name 'HD-Player' -EA SilentlyContinue).Count
$exact=@(Get-ExactPlayers $name);$r.exact=$exact.Count;$r.ids=@($exact | Select-Object -ExpandProperty Id)
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 | Where-Object {Test-CmdLine $_.CommandLine $name} | ForEach-Object { $_.CommandLine })
}catch{ $r.cmds = @("WMI ERROR: $($_.Exception.Message)") }
$r
}
$script:logOffset = 0
function Snapshot-Log {
$script:PlayerLogReadFailureLogged=$false
$script:logOffset=0
if(Test-Path -LiteralPath $PlayerLog){try{$script:logOffset=(Get-Item -LiteralPath $PlayerLog -Force).Length}catch{Log-Failure 'Player.log initial inspection' $_}}
}
function Read-NewLog($name){
if(-not (Test-Path -LiteralPath $PlayerLog)){ return @() }
try{
$fs=[IO.File]::Open($PlayerLog,'Open','Read','ReadWrite')
try{
if($fs.Length -lt $script:logOffset){ $script:logOffset = 0 } # rotated / shrank
if($fs.Length - $script:logOffset -gt 256KB){$script:logOffset=$fs.Length-256KB;Log 'Player.log burst limited to the newest 256 KB.'}
$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{if(-not $script:PlayerLogReadFailureLogged){Log-Failure 'Player.log read' $_;$script:PlayerLogReadFailureLogged=$true};@()}
}
function Phase-Of($line){ $m=[regex]::Match($line, [regex]::Escape($Instance)+'\s+\[([A-Za-z]+)\]'); if($m.Success){ $m.Groups[1].Value } }
function Test-DiagnosticPlayerLine([string]$line){
# Keep host startup/disk evidence, not unrelated app inventories or input/telemetry payloads.
$line -match '\b(?:PLR|VMMGR|VBOX)\s+\S+\s+\[' -and
$line -match '(?i)\]\s+[WE]:|integrity|Root\.vhd|Player state\s*:|VERR_|VBOX_E_|failed to start|crashed'
}
# ----------------------------- report environment -----------------------------
Section 'ENVIRONMENT'
Log "Install : $(Redact $Install) exists=$([bool](Test-Path -LiteralPath $Install))"
Log "DataRoot : $(Redact $DataRoot)"
Log "Conf : $(Redact $Conf) exists=$([bool](Test-Path -LiteralPath $Conf))"
Log "Player.log: $(Redact $PlayerLog) exists=$([bool](Test-Path -LiteralPath $PlayerLog))"
Log "HD-Player : exists=$([bool](Test-Path -LiteralPath $Player))"
Get-BsrPlayerDiagnostics $Player | ForEach-Object { Log $_ }
Report-HostDetails
foreach($disk in @(Get-ChildItem -LiteralPath (Join-Path $DataRoot 'Engine') -Filter Root.vhd -Recurse -File -EA SilentlyContinue)){
try { Log "disk: $($disk.FullName); format=$(Get-BsrDiskFormat $disk.FullName); bytes=$($disk.Length); attributes=$($disk.Attributes)" } catch { Log "disk inspection: $($_.Exception.Message)" Yellow }
}
Log "HD-Adb : exists=$([bool](Test-Path -LiteralPath $AdbExe)) version=[$(Compact (Adb @('version')))]"
if(-not (Test-Path -LiteralPath $Player) -or -not (Test-Path -LiteralPath $AdbExe)){throw 'HD-Player.exe or HD-Adb.exe not found -- cannot continue.'}
Section 'CONF PORTS'
Log "instances in conf : $($allInst -join ', ')"
Log "TARGET instance : $Instance" Yellow
Log "status.adb_port : $statusPort"
Log "adb_port : $adbPort"
Log "Configured port : $PrimaryPort (live listener ownership takes precedence)" Yellow
# ----------------------------- private adb server port -----------------------------
$serverBand = Get-BandListeners 15037 15057
$serverPort = $null
$used = @{}; foreach($b in $serverBand){ $used[[int]$b.Port] = $true }
foreach($p in 15037..15057){ if(-not $used.ContainsKey($p)){ $serverPort = "$p"; break } }
if(-not $serverPort){throw 'No free private ADB server port in 15037-15057; other ADB servers were left alone.'}
$oldAdbServerPort=$env:ANDROID_ADB_SERVER_PORT
$env:ANDROID_ADB_SERVER_PORT = $serverPort
Log "ADB server port : $serverPort (current 15037-15057 listeners: $(Fmt-Band $serverBand))"
# ----------------------------- clean cold start -----------------------------
try {
Section 'CLEAN START'
$script:launchedPid=$null
$targets=@(Get-ExactPlayers $Instance)
Log "restarting selected instance only: $Instance; matched PIDs=$($targets.Id -join ',')"
$targets | Stop-Process -Force -EA SilentlyContinue
Start-Sleep 3
$base=$Instance -replace '_\d+$',''
$rootImage=Join-Path $DataRoot "Engine\$base\Root.vhd"
if(-not (Probe-RootDisk $rootImage)){Report-PolicyEvents;return}
Log "start-server -> $(Compact (Adb @('start-server')))"
Snapshot-Log
Section 'LAUNCH + WATCH'
Log "launch: HD-Player.exe --instance $Instance"
try{ $script:launchedPid=Start-BsrPlayer $Player $Instance; $script:launchedStart=(Get-Process -Id $script:launchedPid -EA Stop).StartTime }catch{
Log-Failure 'HD-Player launch failed' $_
Report-PolicyEvents
Section 'VERDICT'
Log 'HOST LAUNCH FAILED: Windows rejected HD-Player before Android/ADB started. No ADB reconnect loop is needed.' Red
Log "Full log: $(Redact $LogFile)" Cyan
return
}
$sw = [Diagnostics.Stopwatch]::StartNew()
# timing knobs (reasonable, but fail fast when it is clearly NOT just slow boot)
$HARD_CAP = 480 # observation limit; individual ADB commands have an 8-second timeout
$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; $diagSerial=$null
$identityLogged=$false; $nextHeal=20; $done=$false; $verdict='(inconclusive)'
$recentPlayerLines=New-Object System.Collections.Generic.Queue[string]
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.exact -gt 0){ $sawProc = $true }
foreach($l in (Read-NewLog $Instance)){
if(Test-DiagnosticPlayerLine $l){$recentPlayerLines.Enqueue($l);while($recentPlayerLines.Count -gt 40){$null=$recentPlayerLines.Dequeue()}}
$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
$port=@(Get-BsrDiagnosticTargetPort $bl $pp.ids $PrimaryPort)
if(-not $port.Count){
Log "t=${el}s exactPlayers=$($pp.exact); phase=$lastPhase; no listener owned by the target; band=[$(Fmt-Band $bl)]"
if($sawProc -and -not $pp.exact){$verdict='PLAYER EXITED: selected instance crashed or was closed';break}
if($el -ge $NOPROGRESS){$verdict='NO TARGET LISTENER: selected instance did not expose ADB';break}
continue
}
$cand = "127.0.0.1:$($port[0])"
$conn = Adb @('connect',$cand)
$state= State $cand
$bc = if($state -eq 'device'){ Adb @('-s',$cand,'shell','getprop','sys.boot_completed') } else { '' }
Log ("t=${el}s proc[total=$($pp.proc) exact=$($pp.exact) wmi_match=$($pp.wmi_match)] phase=$lastPhase target=$cand band=[$(Fmt-Band $bl)] connect=[$(Compact $conn 80)] state=[$state] boot=[$(Compact $bc 20)]")
if($pp.exact -gt 0 -and $pp.wmi_match -eq 0){
Log ' WMI hides the instance command line; target identified by launch PID / Player.log.' DarkGray
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 }
$diagSerial=$cand
$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'){
$diagSerial=$cand
$verdict = "SUCCESS via reconnect: $cand online+boot_completed=1 at ${el}s. Connection recovered during this diagnostic; this does not explain an earlier payload failure."; $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.exact -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-Failure 'Boot observation iteration' $_
}
}
Section 'ROOT + SELINUX DIAGNOSTICS'
if($diagSerial){
$script:GuestAdbFailures=0;$script:CaptureGuestErrors=$true
try{
$confLines = [IO.File]::ReadAllLines($Conf) | Where-Object {
$_ -match ('^bst\.instance\.'+[regex]::Escape($Instance)+'\.enable_root_access=') -or
$_ -match '^bst\.(feature\.rooting|enable_adb_access)='
}
foreach($line in $confLines){Log "host conf: $line"}
}catch{Log "host conf read failed: $($_.Exception.Message)" DarkYellow}
Log "guest getenforce : $(Compact (Adb @('-s',$diagSerial,'shell','getenforce')) 120)"
Log "guest selinux fs : $(Compact (Adb @('-s',$diagSerial,'shell','if [ -d /sys/fs/selinux ]; then ls -ld /sys/fs/selinux; cat /sys/fs/selinux/enforce 2>/dev/null; else echo MISSING; fi')) 180)"
Log "guest selinux props : $(Compact (Adb @('-s',$diagSerial,'shell','getprop ro.boot.selinux; getprop ro.build.selinux')) 120)"
Log "guest kernel cmdline : $(Compact (Adb @('-s',$diagSerial,'shell','cat /proc/cmdline')) 240)"
Log "guest bindmount prop : $(Compact (Adb @('-s',$diagSerial,'shell','getprop bst.config.bindmount')) 80)"
Log "guest Magisk root : $(Compact (Adb @('-s',$diagSerial,'shell','su -c id; readlink /system/bin/su; su -c "magisk -c"')) 500)"
Log "guest bootstrap files : $(Compact (Adb @('-s',$diagSerial,'shell','ls -l /system/etc/bsr_su /system/xbin/su /system/xbin/bstk/su /system/bin/bindmount 2>&1')) 360)"
Log "guest bootstrap hashes: $(Compact (Adb @('-s',$diagSerial,'shell','sha256sum /system/etc/bsr_su /system/xbin/su /system/xbin/bstk/su 2>&1')) 360)"
Log "guest xbin mount : $(Compact (Adb @('-s',$diagSerial,'shell','mount | grep " /system/xbin "')) 300)"
$bootstrapId=Adb @('-s',$diagSerial,'shell','/system/etc/bsr_su -c id 2>&1')
Log "guest direct bsr_su id: $(Compact $bootstrapId 180)" $(if($bootstrapId-match'uid=0'){'Green'}else{'Yellow'})
if($bootstrapId-match'uid=0'){
Log "guest bsr kernel log : $(Compact (Adb @('-s',$diagSerial,'shell','/system/etc/bsr_su -c "dmesg | grep -i bsr | tail -20"')) 500)"
}
$script:CaptureGuestErrors=$false
if($script:GuestAdbFailures){
$script:DiagnosticFailures++
Set-DiagnosticCheck 'Guest evidence' 'INCOMPLETE' "$script:GuestAdbFailures commands failed after initial ADB readiness; do not infer root state from missing replies"
}else{Set-DiagnosticCheck 'Guest evidence' 'PASS' 'guest commands returned; see their individual results for root state'}
}else{
Set-DiagnosticCheck 'Guest evidence' 'SKIP' 'ADB was never ready'
Log 'Guest was never adb-ready, so root/SELinux guest probes were skipped.' Yellow
}
Section 'TARGET PLAYER.LOG STARTUP/DISK EVIDENCE (up to 40 lines)'
foreach($line in $recentPlayerLines){Log (Compact $line 1200)}
Report-PolicyEvents
Section 'ADB VERDICT'
Log $verdict $(if($verdict -match '^SUCCESS'){'Green'}else{'Red'})
if($verdict -match '^SUCCESS'){Set-DiagnosticCheck 'ADB readiness' 'PASS' $verdict}else{$script:DiagnosticFailures++;Set-DiagnosticCheck 'ADB readiness' 'FAIL' $verdict}
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
} finally {
try { Stop-BsrAdbServer $AdbExe } catch { Log ("ADB cleanup: " + $_.Exception.Message) Yellow }
$env:ANDROID_ADB_SERVER_PORT=$oldAdbServerPort
}
} catch {
Log-Failure 'Diagnostic stopped' $_
} finally {
Report-DiagnosticSummary
if($script:DiagnosticFailures -gt 0 -or $script:LogWriteFailed){exit 1}
}
<#
__BSR_HOST_BEGIN__
# Shared Windows disk and process helpers. Embedded in both single-file launchers.
# This file only defines functions; loading it never changes the host.
function Read-BsrDeviceBytes([string]$Device, [long]$Offset, [int]$Count) {
if ($Offset -lt 0 -or $Count -le 0) { throw 'Invalid disk read offset or size.' }
$sectorStart = $Offset - ($Offset % 512)
$delta = [int]($Offset - $sectorStart)
$needed = [long]$delta + $Count
$aligned = $needed + ((512 - ($needed % 512)) % 512)
if ($aligned -gt [int]::MaxValue) { throw 'Disk read is too large.' }
$stream = [IO.File]::Open($Device, 'Open', 'Read', 'ReadWrite')
try {
$stream.Position = $sectorStart
$buffer = New-Object byte[] ([int]$aligned)
$filled = 0
while ($filled -lt $buffer.Length) {
$read = $stream.Read($buffer, $filled, $buffer.Length - $filled)
if ($read -le 0) { throw "Short disk read at offset $Offset in '$Device'." }
$filled += $read
}
$result = New-Object byte[] $Count
[Array]::Copy($buffer, $delta, $result, 0, $Count)
return ,$result
} finally { $stream.Dispose() }
}
# ProcessStartInfo avoids PowerShell 5.1's lossy native argument conversion and
# stderr-as-errors. Drain both pipes concurrently, and bound every native call.
function Quote-BsrNativeArgument([AllowEmptyString()][string]$Value) {
$escaped = [regex]::Replace($Value, '(\\*)"', '$1$1\"')
'"' + [regex]::Replace($escaped, '(\\+)$', '$1$1') + '"'
}
function Invoke-BsrNative([string]$FilePath, [string[]]$Arguments,
[ValidateRange(1,3600)][int]$TimeoutSec=30,
[switch]$ReturnOnTimeout) {
$process = $null
try {
$info = New-Object Diagnostics.ProcessStartInfo
$info.FileName = $FilePath
$info.Arguments = ($Arguments | ForEach-Object { Quote-BsrNativeArgument $_ }) -join ' '
$info.UseShellExecute = $false
$info.CreateNoWindow = $true
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$info.StandardOutputEncoding = [Text.Encoding]::UTF8
$info.StandardErrorEncoding = [Text.Encoding]::UTF8
$process = [Diagnostics.Process]::Start($info)
$stdout = $process.StandardOutput.ReadToEndAsync()
$stderr = $process.StandardError.ReadToEndAsync()
$timedOut = -not $process.WaitForExit($TimeoutSec * 1000)
if ($timedOut) {
$process.Kill()
if (-not $ReturnOnTimeout) {
throw "Native command timed out after ${TimeoutSec}s: $([IO.Path]::GetFileName($FilePath)) $($Arguments -join ' ')"
}
}
if (-not $stdout.Wait(2000) -or -not $stderr.Wait(2000)) {
throw "Native output pipe did not close: $FilePath"
}
[pscustomobject]@{
ExitCode = if($timedOut){-1}else{$process.ExitCode}
TimedOut = $timedOut
Output = ($stdout.Result + [Environment]::NewLine + $stderr.Result).Trim()
}
} finally { if ($process) { $process.Dispose() } }
}
function Get-BsrFileHash([string]$Path) {
$stream = [IO.File]::OpenRead($Path)
$hash = [Security.Cryptography.SHA256]::Create()
try { [BitConverter]::ToString($hash.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() }
finally { $hash.Dispose(); $stream.Dispose() }
}
function Get-BsrBytesHash([byte[]]$Bytes) {
$hash = [Security.Cryptography.SHA256]::Create()
try { [BitConverter]::ToString($hash.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant() }
finally { $hash.Dispose() }
}
function Expand-BsrGzip([byte[]]$Bytes) {
$inputStream = New-Object IO.MemoryStream(,$Bytes)
$outputStream = New-Object IO.MemoryStream
$gzip = $null
try {
$gzip = New-Object IO.Compression.GZipStream($inputStream,[IO.Compression.CompressionMode]::Decompress)
$gzip.CopyTo($outputStream)
return ,$outputStream.ToArray()
} finally {
if ($gzip) { $gzip.Dispose() }
$inputStream.Dispose(); $outputStream.Dispose()
}
}
function Expand-BsrZip([string]$Archive, [string]$Destination) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$root = [IO.Path]::GetFullPath($Destination).TrimEnd('\','/') + [IO.Path]::DirectorySeparatorChar
$zip = [IO.Compression.ZipFile]::OpenRead($Archive)
try {
$files = New-Object System.Collections.Generic.List[object]
foreach ($entry in $zip.Entries) {
if (-not $entry.Name) { continue }
$path = [IO.Path]::GetFullPath([IO.Path]::Combine($root, $entry.FullName))
if (-not $path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -or $entry.FullName -match ':') {
throw "Archive entry escapes its extraction directory: $($entry.FullName)"
}
$files.Add(@{ Entry=$entry; Path=$path })
}
foreach ($file in $files) {
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($file.Path))
[IO.Compression.ZipFileExtensions]::ExtractToFile($file.Entry, $file.Path, $true)
}
} finally { $zip.Dispose() }
}
function Get-BsrMagiskFileMap([switch]$IncludeUninstaller) {
$map = [ordered]@{
'lib/x86_64/libbusybox.so'='busybox'; 'lib/x86_64/libmagisk64.so'='magisk64'
'lib/x86_64/libmagiskboot.so'='magiskboot'; 'lib/x86_64/libmagiskinit.so'='magiskinit'
'lib/x86_64/libmagiskpolicy.so'='magiskpolicy'; 'lib/x86/libmagisk32.so'='magisk32'
'assets/stub.apk'='stub.apk'; 'assets/util_functions.sh'='util_functions.sh'
'assets/boot_patch.sh'='boot_patch.sh'; 'assets/addon.d.sh'='addon.d.sh'
}
if ($IncludeUninstaller) { $map['assets/uninstaller.sh']='uninstaller.sh' }
return $map
}
function Expand-BsrMagiskApk([string]$Apk, [string]$Destination, [switch]$IncludeUninstaller) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$map = Get-BsrMagiskFileMap -IncludeUninstaller:$IncludeUninstaller
$zip = [IO.Compression.ZipFile]::OpenRead($Apk)
try {
# Validate every required entry BEFORE replacing any existing staging file.
foreach ($name in $map.Keys) {
$entry = $zip.GetEntry($name)
if (-not $entry -or $entry.Length -le 0) { throw "APK missing expected member: $name" }
}
[void][IO.Directory]::CreateDirectory($Destination)
foreach ($name in $map.Keys) {
[IO.Compression.ZipFileExtensions]::ExtractToFile($zip.GetEntry($name), (Join-Path $Destination $map[$name]), $true)
}
} finally { $zip.Dispose() }
}
# Each disk/config backup appears at its final name only after the entire copy
# succeeds. A full disk or interrupted copy must never become a trusted backup.
function Copy-BsrFileAtomically([string]$Source, [string]$Destination, [switch]$Replace) {
$temporary = $Destination + '.' + [guid]::NewGuid().ToString('N') + '.tmp'
try {
[IO.File]::Copy($Source, $temporary, $false)
if($Replace -and [IO.File]::Exists($Destination)){[IO.File]::Replace($temporary,$Destination,[NullString]::Value)}
else{[IO.File]::Move($temporary, $Destination)}
} finally {
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue }
}
}
function Copy-BsrBackupOnce([string]$Source, [string]$Destination) {
if (Test-Path -LiteralPath $Destination -PathType Leaf) { return }
Copy-BsrFileAtomically $Source $Destination
}
function Write-BsrTextFile([string]$Path, [string]$Text) {
$Path = [IO.Path]::GetFullPath($Path)
$temporary = $Path + '.' + [guid]::NewGuid().ToString('N') + '.tmp'
try {
[IO.File]::WriteAllText($temporary, $Text, (New-Object Text.UTF8Encoding($false)))
if ([IO.File]::Exists($Path)) { [IO.File]::Replace($temporary, $Path, [NullString]::Value) }
else { [IO.File]::Move($temporary, $Path) }
} finally {
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue }
}
}
# Modify only existing keys, retain each line ending, and treat replacement data
# literally (a '$1' in an instance name/value is not a regex backreference).
function Set-BsrConfValues([string]$Path, [System.Collections.IDictionary]$Values) {
$raw = [IO.File]::ReadAllText($Path)
$updated = $raw
$missing = New-Object System.Collections.Generic.List[string]
foreach ($key in $Values.Keys) {
$pattern = '(?m)^[\t ]*' + [regex]::Escape($key) + '[\t ]*=[^\r\n]*'
if (-not [regex]::IsMatch($updated, $pattern)) { $missing.Add($key); continue }
$replacement = $key + '="' + $Values[$key] + '"'
$updated = [regex]::Replace($updated, $pattern, [Text.RegularExpressions.MatchEvaluator]{ param($match) $replacement })
}
if ($updated -cne $raw) { Write-BsrTextFile $Path $updated }
return $missing.ToArray()
}
function ConvertTo-BsrDebugfsPath([string]$Path) {
if ($Path -match '["\r\n]') { throw 'Invalid character in debugfs path.' }
'"' + $Path.Replace('\','/') + '"'
}
function Get-BsrDebugfsStat([string]$Output, [string]$Path) {
# Match only this stat command's response. An inode from the next command
# must never validate a failed write earlier in the same command file.
$pattern = '(?ms)^debugfs:\s+stat\s+' + [regex]::Escape($Path) + '[\t ]*\r?\n(.*?)(?=^debugfs:|\z)'
$match = [regex]::Match($Output, $pattern)
if ($match.Success) { return $match.Groups[1].Value }
return ''
}
function Test-BsrDebugfsFile([string]$Output, [string]$Path, [long]$Length,
[int]$Mode, [int]$Uid=0, [int]$Gid=0) {
$stat = Get-BsrDebugfsStat $Output $Path
$permission = [regex]::Match($stat, 'Mode:\s*([0-7]+)')
return ($stat -match '\bInode:\s*\d+' -and $stat -match ('\bSize:\s*' + $Length + '\b') -and
$permission.Success -and [Convert]::ToInt32($permission.Groups[1].Value,8) -eq ($Mode -band 0xFFF) -and
$stat -match ('\bUser:\s*'+$Uid+'\b') -and $stat -match ('\bGroup:\s*'+$Gid+'\b'))
}
function Test-HdPlayerInstance([string]$cmdLine, [string]$name) {
if ([string]::IsNullOrWhiteSpace($cmdLine) -or [string]::IsNullOrWhiteSpace($name)) { return $false }
$escaped = [regex]::Escape($name)
$cmdLine -match "(?i)(^|\s)--instance(?:\s+|=)(`"$escaped`"|$escaped)(?=\s|$)"
}
function Get-BsrInstanceProcesses([string]$Instance, [string]$PlayerLog) {
$ids = New-Object 'System.Collections.Generic.HashSet[int]'
try {
foreach ($process in @(Get-CimInstance Win32_Process -Filter "Name='HD-Player.exe'" -ErrorAction Stop)) {
if (Test-HdPlayerInstance $process.CommandLine $Instance) { [void]$ids.Add([int]$process.ProcessId) }
}
} catch { }
# Some BlueStacks builds hide their WMI command line. Player.log is a second
# exact-instance source; reject old log records whose PID has been reused.
if ($PlayerLog -and (Test-Path -LiteralPath $PlayerLog)) {
try {
$tail = (Get-Content -LiteralPath $PlayerLog -Tail 6000 -ErrorAction Stop) -join "`n"
$hits = [regex]::Matches($tail, '(?m)^(\S+\s+\S+)\s+(\d+)\s+\d+\s+\S+\s+' + [regex]::Escape($Instance) + '\s+\[')
if ($hits.Count) {
$last = $hits[$hits.Count - 1]
$process = Get-Process -Id ([int]$last.Groups[2].Value) -ErrorAction Stop
$logged = [datetimeoffset]::Parse($last.Groups[1].Value, [Globalization.CultureInfo]::InvariantCulture)
if ($process.Name -eq 'HD-Player' -and $logged.UtcDateTime -ge $process.StartTime.ToUniversalTime().AddSeconds(-2)) {
[void]$ids.Add($process.Id)
}
}
} catch { }
}
foreach ($processId in $ids) {
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
if ($process -and $process.Name -eq 'HD-Player') { $process }