EvanZhu0721 / 公开仓库
SBMS
SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.
tests/manage-sunshine-instance.Tests.ps117.1 KB
1
$ErrorActionPreference = 'Stop'2
Set-StrictMode -Version Latest3
4
$script:RepositoryRoot = Split-Path -Parent $PSScriptRoot5
$script:ManagerScript = Join-Path `6
$script:RepositoryRoot `7
'installer\manage-sunshine-instance.ps1'8
$script:HostExecutable = (Get-Process -Id $PID).Path9
$script:OriginalLocalAppData = $env:LOCALAPPDATA10
$script:OriginalSunshineExe = $env:SBMS_SUNSHINE_EXE11
$script:TestRoot = Join-Path `12
([IO.Path]::GetTempPath()) `13
"sbms-sunshine-pester-$([Guid]::NewGuid().ToString('N'))"14
$script:FakeRoot = Join-Path $script:TestRoot 'Fake Sunshine'15
$script:FakeExecutable = Join-Path $script:FakeRoot 'sunshine.exe'16
$script:FakeGlobalConfiguration = Join-Path `17
$script:FakeRoot `18
'config\sunshine.conf'19
20
function Invoke-Manager {21
param(22
[Parameter(Mandatory)]23
[string[]]$ManagerArguments24
)25
26
$nativeArguments = @(27
'-NoLogo'28
'-NoProfile'29
'-NonInteractive'30
'-File'31
$script:ManagerScript32
) + $ManagerArguments33
$output = & $script:HostExecutable @nativeArguments34
$exitCode = $LASTEXITCODE35
$outputLines = @($output)36
return [pscustomobject]@{37
ExitCode = $exitCode38
Text = ($outputLines -join [Environment]::NewLine)39
Json = if ($outputLines.Count -gt 0) {40
($outputLines -join [Environment]::NewLine) | ConvertFrom-Json41
} else {42
$null43
}44
}45
}46
47
function Get-FreeSunshineBasePort {48
foreach ($basePort in 56000..62000) {49
if ($basePort -gt 65514) {50
break51
}52
53
$listeners = @()54
$udpClients = @()55
try {56
foreach ($offset in @(-5, 0, 1, 21)) {57
$listener = [Net.Sockets.TcpListener]::new(58
[Net.IPAddress]::Any,59
($basePort + $offset)60
)61
$listener.Server.ExclusiveAddressUse = $true62
$listener.Start()63
$listeners += $listener64
}65
foreach ($offset in @(9, 10, 11)) {66
$client = [Net.Sockets.UdpClient]::new()67
$client.Client.ExclusiveAddressUse = $true68
$client.Client.Bind(69
[Net.IPEndPoint]::new(70
[Net.IPAddress]::Any,71
($basePort + $offset)72
)73
)74
$udpClients += $client75
}76
return $basePort77
} catch {78
# Continue scanning after releasing any partially acquired family.79
} finally {80
foreach ($listener in $listeners) {81
$listener.Stop()82
}83
foreach ($client in $udpClients) {84
$client.Dispose()85
}86
}87
}88
89
throw 'No free Sunshine test port family was found.'90
}91
92
New-Item -ItemType Directory `93
-Path (Join-Path $script:FakeRoot 'config') `94
-Force |95
Out-Null96
97
$fakeSourcePath = Join-Path $script:TestRoot 'fake-sunshine.rs'98
$fakeSource = @'99
use std::env;100
use std::fs;101
use std::net::TcpListener;102
use std::path::Path;103
use std::thread;104
use std::time::Duration;105
106
fn main() {107
let arguments: Vec<String> = env::args().skip(1).collect();108
let configuration = arguments.first().cloned().unwrap_or_else(|| {109
std::process::exit(2);110
});111
let argument_log = Path::new(&configuration)112
.parent()113
.unwrap_or_else(|| {114
std::process::exit(2);115
})116
.join("argv.txt");117
fs::write(argument_log, arguments.join("\n")).unwrap_or_else(|_| {118
std::process::exit(2);119
});120
let contents = fs::read_to_string(configuration).unwrap_or_else(|_| {121
std::process::exit(2);122
});123
let port = contents124
.lines()125
.find_map(|line| {126
let (name, value) = line.split_once('=')?;127
if name.trim().eq_ignore_ascii_case("port") {128
value.trim().parse::<u16>().ok()129
} else {130
None131
}132
})133
.unwrap_or_else(|| {134
std::process::exit(3);135
});136
let _listener = TcpListener::bind(("127.0.0.1", port + 1)).unwrap_or_else(|_| {137
std::process::exit(4);138
});139
loop {140
thread::sleep(Duration::from_secs(60));141
}142
}143
'@144
[IO.File]::WriteAllText(145
$fakeSourcePath,146
$fakeSource,147
[Text.UTF8Encoding]::new($false)148
)149
$rustCompiler = (Get-Command rustc -ErrorAction Stop).Source150
$compilerArguments = @(151
'--edition=2021'152
$fakeSourcePath153
'-o'154
$script:FakeExecutable155
)156
& $rustCompiler @compilerArguments157
$compilerExitCode = $LASTEXITCODE158
if ($compilerExitCode -ne 0) {159
throw "rustc failed to build the fake Sunshine process (exit $compilerExitCode)."160
}161
162
[IO.File]::WriteAllText(163
(Join-Path $script:FakeRoot 'config\apps.json'),164
'{"env":{},"apps":[]}',165
[Text.UTF8Encoding]::new($false)166
)167
[IO.File]::WriteAllText(168
$script:FakeGlobalConfiguration,169
'global-config-sentinel',170
[Text.UTF8Encoding]::new($false)171
)172
173
$env:LOCALAPPDATA = Join-Path $script:TestRoot 'Local AppData'174
$env:SBMS_SUNSHINE_EXE = $script:FakeExecutable175
176
Describe 'manage-sunshine-instance.ps1' {177
It 'starts an isolated instance, reports JSON, and stops only that PID' {178
$groupId = "pester-$([Guid]::NewGuid().ToString('N'))"179
$displayId = '{23f8cce0-fefe-4a5b-9e14-0123456789ab}'180
$port = Get-FreeSunshineBasePort181
182
$start = Invoke-Manager -ManagerArguments @(183
'-Action'184
'Start'185
'-GroupId'186
$groupId187
'-DisplayId'188
$displayId189
'-Port'190
[string]$port191
)192
193
$start.ExitCode | Should Be 0194
$start.Json.ok | Should Be $true195
$start.Json.status | Should Be 'started'196
$start.Json.port | Should Be $port197
(Get-Process -Id ([int]$start.Json.pid) -ErrorAction Stop).HasExited |198
Should Be $false199
200
$instance = Join-Path `201
$env:LOCALAPPDATA `202
"SBMS\sunshine\group-$groupId"203
$configuration = [IO.File]::ReadAllText(204
(Join-Path $instance 'sunshine.conf'),205
[Text.Encoding]::UTF8206
)207
$configuration | Should Match "port = $port"208
$configuration | Should Match 'upnp = disabled'209
$configuration | Should Match 'dd_configuration_option = disabled'210
$configuration | Should Match ([regex]::Escape(211
(Join-Path $instance 'sunshine_state.json').Replace('\', '/')212
))213
[IO.File]::ReadAllText(214
$script:FakeGlobalConfiguration,215
[Text.Encoding]::UTF8216
) | Should Be 'global-config-sentinel'217
218
$stop = Invoke-Manager -ManagerArguments @(219
'-Action'220
'Stop'221
'-GroupId'222
$groupId223
)224
if ($stop.ExitCode -ne 0) {225
throw "Stop failed: $($stop.Text)"226
}227
$stop.ExitCode | Should Be 0228
$stop.Json.ok | Should Be $true229
$stop.Json.status | Should Be 'stopped'230
$remaining = Get-CimInstance `231
-ClassName Win32_Process `232
-Filter "ProcessId = $([int]$start.Json.pid)" `233
-ErrorAction Stop234
$remaining | Should BeNullOrEmpty235
}236
237
It 'is idempotent for an already-running matching Start request' {238
$groupId = "pester-$([Guid]::NewGuid().ToString('N'))"239
$displayId = '{23f8cce0-fefe-4a5b-9e14-abcdef012345}'240
$port = Get-FreeSunshineBasePort241
$arguments = @(242
'-Action'243
'Start'244
'-GroupId'245
$groupId246
'-DisplayId'247
$displayId248
'-Port'249
[string]$port250
)251
252
$first = Invoke-Manager -ManagerArguments $arguments253
$second = Invoke-Manager -ManagerArguments $arguments254
255
$first.ExitCode | Should Be 0256
$second.ExitCode | Should Be 0257
$second.Json.status | Should Be 'already_running'258
$second.Json.pid | Should Be $first.Json.pid259
260
$null = Invoke-Manager -ManagerArguments @(261
'-Action'262
'Stop'263
'-GroupId'264
$groupId265
)266
}267
268
It 'passes capture only as an exact runtime argument' {269
$cases = @(270
[pscustomobject]@{ Capture = 'auto'; Expected = $null }271
[pscustomobject]@{ Capture = 'ddx'; Expected = 'capture=ddx' }272
[pscustomobject]@{ Capture = 'wgc'; Expected = 'capture=wgc' }273
)274
275
foreach ($case in $cases) {276
$groupId = "pester-$([Guid]::NewGuid().ToString('N'))"277
$displayId = '{23f8cce0-fefe-4a5b-9e14-aabbccddeeff}'278
$port = Get-FreeSunshineBasePort279
$start = Invoke-Manager -ManagerArguments @(280
'-Action'281
'Start'282
'-GroupId'283
$groupId284
'-DisplayId'285
$displayId286
'-Port'287
[string]$port288
'-Capture'289
$case.Capture290
)291
292
$start.ExitCode | Should Be 0293
$instance = Join-Path `294
$env:LOCALAPPDATA `295
"SBMS\sunshine\group-$groupId"296
$arguments = @(297
Get-Content `298
-LiteralPath (Join-Path $instance 'argv.txt') `299
-Encoding UTF8300
)301
$arguments[0] | Should Be (Join-Path $instance 'sunshine.conf')302
if ($null -eq $case.Expected) {303
$arguments.Count | Should Be 1304
} else {305
$arguments.Count | Should Be 2306
$arguments[1] | Should Be $case.Expected307
}308
309
$configuration = Get-Content `310
-LiteralPath (Join-Path $instance 'sunshine.conf') `311
-Encoding UTF8 `312
-Raw313
$configuration | Should Not Match '(?m)^\s*capture\s*='314
$manifest = Get-Content `315
-LiteralPath (Join-Path $instance 'instance.json') `316
-Encoding UTF8 `317
-Raw |318
ConvertFrom-Json319
($manifest.PSObject.Properties.Name -contains 'capture') |320
Should Be $false321
[IO.File]::ReadAllText(322
$script:FakeGlobalConfiguration,323
[Text.Encoding]::UTF8324
) | Should Be 'global-config-sentinel'325
326
$stop = Invoke-Manager -ManagerArguments @(327
'-Action'328
'Stop'329
'-GroupId'330
$groupId331
)332
$stop.ExitCode | Should Be 0333
}334
}335
336
It 'uses capture mode when matching or restarting a managed instance' {337
$groupId = "pester-$([Guid]::NewGuid().ToString('N'))"338
$displayId = '{23f8cce0-fefe-4a5b-9e14-ffeeddccbbaa}'339
$port = Get-FreeSunshineBasePort340
$wgcArguments = @(341
'-Action'342
'Start'343
'-GroupId'344
$groupId345
'-DisplayId'346
$displayId347
'-Port'348
[string]$port349
'-Capture'350
'wgc'351
)352
$first = Invoke-Manager -ManagerArguments $wgcArguments353
$matching = Invoke-Manager -ManagerArguments $wgcArguments354
355
$first.ExitCode | Should Be 0356
$matching.ExitCode | Should Be 0357
$matching.Json.status | Should Be 'already_running'358
$matching.Json.pid | Should Be $first.Json.pid359
360
$mismatch = Invoke-Manager -ManagerArguments @(361
'-Action'362
'Start'363
'-GroupId'364
$groupId365
'-DisplayId'366
$displayId367
'-Port'368
[string]$port369
'-Capture'370
'auto'371
)372
$mismatch.ExitCode | Should Not Be 0373
$mismatch.Json.message | Should Match 'use Restart'374
(Get-Process -Id ([int]$first.Json.pid) -ErrorAction Stop).HasExited |375
Should Be $false376
377
$restart = Invoke-Manager -ManagerArguments @(378
'-Action'379
'Restart'380
'-GroupId'381
$groupId382
'-DisplayId'383
$displayId384
'-Port'385
[string]$port386
'-Capture'387
'auto'388
)389
$restart.ExitCode | Should Be 0390
$restart.Json.pid | Should Not Be $first.Json.pid391
$restart.Json.port | Should Be $first.Json.port392
Get-CimInstance `393
-ClassName Win32_Process `394
-Filter "ProcessId = $([int]$first.Json.pid)" `395
-ErrorAction Stop |396
Should BeNullOrEmpty397
(Get-Process -Id ([int]$restart.Json.pid) -ErrorAction Stop).HasExited |398
Should Be $false399
$instance = Join-Path `400
$env:LOCALAPPDATA `401
"SBMS\sunshine\group-$groupId"402
@(Get-Content -LiteralPath (Join-Path $instance 'argv.txt') -Encoding UTF8).Count |403
Should Be 1404
405
$null = Invoke-Manager -ManagerArguments @(406
'-Action'407
'Stop'408
'-GroupId'409
$groupId410
)411
}412
413
It 'scans complete port families by 27 and StopAll needs no GroupId' {414
$firstGroup = "pester-$([Guid]::NewGuid().ToString('N'))"415
$secondGroup = "pester-$([Guid]::NewGuid().ToString('N'))"416
$displayId = '{23f8cce0-fefe-4a5b-9e14-fedcba987654}'417
$preferredPort = Get-FreeSunshineBasePort418
419
$first = Invoke-Manager -ManagerArguments @(420
'-Action'421
'Start'422
'-GroupId'423
$firstGroup424
'-DisplayId'425
$displayId426
'-Port'427
[string]$preferredPort428
)429
$second = Invoke-Manager -ManagerArguments @(430
'-Action'431
'Start'432
'-GroupId'433
$secondGroup434
'-DisplayId'435
$displayId436
'-Port'437
[string]$preferredPort438
)439
440
$first.ExitCode | Should Be 0441
$second.ExitCode | Should Be 0442
$first.Json.port | Should Be $preferredPort443
$second.Json.port | Should Not Be $preferredPort444
(([int]$second.Json.port - $preferredPort) % 27) | Should Be 0445
446
$stopAll = Invoke-Manager -ManagerArguments @(447
'-Action'448
'StopAll'449
)450
$stopAll.ExitCode | Should Be 0451
$stopAll.Json.ok | Should Be $true452
$stopAll.Json.status | Should Be 'stopped_all'453
$stopAll.Json.stoppedCount | Should Be 2454
455
$firstRemaining = Get-CimInstance `456
-ClassName Win32_Process `457
-Filter "ProcessId = $([int]$first.Json.pid)" `458
-ErrorAction Stop459
$secondRemaining = Get-CimInstance `460
-ClassName Win32_Process `461
-Filter "ProcessId = $([int]$second.Json.pid)" `462
-ErrorAction Stop463
$firstRemaining | Should BeNullOrEmpty464
$secondRemaining | Should BeNullOrEmpty465
}466
467
It 'refuses to stop a PID whose executable or command line is not owned' {468
$groupId = "pester-$([Guid]::NewGuid().ToString('N'))"469
$instance = Join-Path `470
$env:LOCALAPPDATA `471
"SBMS\sunshine\group-$groupId"472
New-Item -ItemType Directory -Path $instance -Force | Out-Null473
$manifest = [ordered]@{474
schema_version = 1475
managed_by = 'SBMS'476
group_id = $groupId477
display_id = '{23f8cce0-fefe-4a5b-9e14-abcdef012345}'478
port = 54321479
pid = $PID480
sunshine_exe = $script:FakeExecutable481
config_path = Join-Path $instance 'sunshine.conf'482
log_path = Join-Path $instance 'sunshine.log'483
started_at = [DateTimeOffset]::Now.ToString('o')484
}485
[IO.File]::WriteAllText(486
(Join-Path $instance 'instance.json'),487
($manifest | ConvertTo-Json -Compress),488
[Text.UTF8Encoding]::new($false)489
)490
491
$stop = Invoke-Manager -ManagerArguments @(492
'-Action'493
'Stop'494
'-GroupId'495
$groupId496
)497
498
$stop.ExitCode | Should Not Be 0499
$stop.Json.ok | Should Be $false500
$stop.Json.message | Should Match 'not the Sunshine executable'501
(Get-Process -Id $PID -ErrorAction Stop).HasExited | Should Be $false502
}503
}504
505
try {506
Get-ChildItem `507
-LiteralPath (Join-Path $env:LOCALAPPDATA 'SBMS\sunshine') `508
-Filter 'instance.json' `509
-Recurse `510
-File `511
-ErrorAction SilentlyContinue |512
ForEach-Object {513
try {514
$manifest = Get-Content `515
-LiteralPath $_.FullName `516
-Encoding UTF8 `517
-Raw |518
ConvertFrom-Json519
if ([string]$manifest.managed_by -eq 'SBMS') {520
$null = Invoke-Manager -ManagerArguments @(521
'-Action'522
'Stop'523
'-GroupId'524
([string]$manifest.group_id)525
)526
}527
} catch {528
# Test cleanup is best-effort; the temporary root is unique.529
}530
}531
} finally {532
$env:LOCALAPPDATA = $script:OriginalLocalAppData533
$env:SBMS_SUNSHINE_EXE = $script:OriginalSunshineExe534
if (Test-Path -LiteralPath $script:TestRoot) {535
Remove-Item -LiteralPath $script:TestRoot -Recurse -Force536
}537
}538