Evan's Blog
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.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / tests
a37d6c4b· 固定源码快照
tests/manage-sunshine-instance.Tests.ps117.1 KB
1$ErrorActionPreference = 'Stop'
2Set-StrictMode -Version Latest
3
4$script:RepositoryRoot = Split-Path -Parent $PSScriptRoot
5$script:ManagerScript = Join-Path `
6 $script:RepositoryRoot `
7 'installer\manage-sunshine-instance.ps1'
8$script:HostExecutable = (Get-Process -Id $PID).Path
9$script:OriginalLocalAppData = $env:LOCALAPPDATA
10$script:OriginalSunshineExe = $env:SBMS_SUNSHINE_EXE
11$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
20function Invoke-Manager {
21 param(
22 [Parameter(Mandatory)]
23 [string[]]$ManagerArguments
24 )
25
26 $nativeArguments = @(
27 '-NoLogo'
28 '-NoProfile'
29 '-NonInteractive'
30 '-File'
31 $script:ManagerScript
32 ) + $ManagerArguments
33 $output = & $script:HostExecutable @nativeArguments
34 $exitCode = $LASTEXITCODE
35 $outputLines = @($output)
36 return [pscustomobject]@{
37 ExitCode = $exitCode
38 Text = ($outputLines -join [Environment]::NewLine)
39 Json = if ($outputLines.Count -gt 0) {
40 ($outputLines -join [Environment]::NewLine) | ConvertFrom-Json
41 } else {
42 $null
43 }
44 }
45}
46
47function Get-FreeSunshineBasePort {
48 foreach ($basePort in 56000..62000) {
49 if ($basePort -gt 65514) {
50 break
51 }
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 = $true
62 $listener.Start()
63 $listeners += $listener
64 }
65 foreach ($offset in @(9, 10, 11)) {
66 $client = [Net.Sockets.UdpClient]::new()
67 $client.Client.ExclusiveAddressUse = $true
68 $client.Client.Bind(
69 [Net.IPEndPoint]::new(
70 [Net.IPAddress]::Any,
71 ($basePort + $offset)
72 )
73 )
74 $udpClients += $client
75 }
76 return $basePort
77 } 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
92New-Item -ItemType Directory `
93 -Path (Join-Path $script:FakeRoot 'config') `
94 -Force |
95 Out-Null
96
97$fakeSourcePath = Join-Path $script:TestRoot 'fake-sunshine.rs'
98$fakeSource = @'
99use std::env;
100use std::fs;
101use std::net::TcpListener;
102use std::path::Path;
103use std::thread;
104use std::time::Duration;
105
106fn 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 = contents
124 .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 None
131 }
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).Source
150$compilerArguments = @(
151 '--edition=2021'
152 $fakeSourcePath
153 '-o'
154 $script:FakeExecutable
155)
156& $rustCompiler @compilerArguments
157$compilerExitCode = $LASTEXITCODE
158if ($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:FakeExecutable
175
176Describe '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-FreeSunshineBasePort
181
182 $start = Invoke-Manager -ManagerArguments @(
183 '-Action'
184 'Start'
185 '-GroupId'
186 $groupId
187 '-DisplayId'
188 $displayId
189 '-Port'
190 [string]$port
191 )
192
193 $start.ExitCode | Should Be 0
194 $start.Json.ok | Should Be $true
195 $start.Json.status | Should Be 'started'
196 $start.Json.port | Should Be $port
197 (Get-Process -Id ([int]$start.Json.pid) -ErrorAction Stop).HasExited |
198 Should Be $false
199
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]::UTF8
206 )
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]::UTF8
216 ) | Should Be 'global-config-sentinel'
217
218 $stop = Invoke-Manager -ManagerArguments @(
219 '-Action'
220 'Stop'
221 '-GroupId'
222 $groupId
223 )
224 if ($stop.ExitCode -ne 0) {
225 throw "Stop failed: $($stop.Text)"
226 }
227 $stop.ExitCode | Should Be 0
228 $stop.Json.ok | Should Be $true
229 $stop.Json.status | Should Be 'stopped'
230 $remaining = Get-CimInstance `
231 -ClassName Win32_Process `
232 -Filter "ProcessId = $([int]$start.Json.pid)" `
233 -ErrorAction Stop
234 $remaining | Should BeNullOrEmpty
235 }
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-FreeSunshineBasePort
241 $arguments = @(
242 '-Action'
243 'Start'
244 '-GroupId'
245 $groupId
246 '-DisplayId'
247 $displayId
248 '-Port'
249 [string]$port
250 )
251
252 $first = Invoke-Manager -ManagerArguments $arguments
253 $second = Invoke-Manager -ManagerArguments $arguments
254
255 $first.ExitCode | Should Be 0
256 $second.ExitCode | Should Be 0
257 $second.Json.status | Should Be 'already_running'
258 $second.Json.pid | Should Be $first.Json.pid
259
260 $null = Invoke-Manager -ManagerArguments @(
261 '-Action'
262 'Stop'
263 '-GroupId'
264 $groupId
265 )
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-FreeSunshineBasePort
279 $start = Invoke-Manager -ManagerArguments @(
280 '-Action'
281 'Start'
282 '-GroupId'
283 $groupId
284 '-DisplayId'
285 $displayId
286 '-Port'
287 [string]$port
288 '-Capture'
289 $case.Capture
290 )
291
292 $start.ExitCode | Should Be 0
293 $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 UTF8
300 )
301 $arguments[0] | Should Be (Join-Path $instance 'sunshine.conf')
302 if ($null -eq $case.Expected) {
303 $arguments.Count | Should Be 1
304 } else {
305 $arguments.Count | Should Be 2
306 $arguments[1] | Should Be $case.Expected
307 }
308
309 $configuration = Get-Content `
310 -LiteralPath (Join-Path $instance 'sunshine.conf') `
311 -Encoding UTF8 `
312 -Raw
313 $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-Json
319 ($manifest.PSObject.Properties.Name -contains 'capture') |
320 Should Be $false
321 [IO.File]::ReadAllText(
322 $script:FakeGlobalConfiguration,
323 [Text.Encoding]::UTF8
324 ) | Should Be 'global-config-sentinel'
325
326 $stop = Invoke-Manager -ManagerArguments @(
327 '-Action'
328 'Stop'
329 '-GroupId'
330 $groupId
331 )
332 $stop.ExitCode | Should Be 0
333 }
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-FreeSunshineBasePort
340 $wgcArguments = @(
341 '-Action'
342 'Start'
343 '-GroupId'
344 $groupId
345 '-DisplayId'
346 $displayId
347 '-Port'
348 [string]$port
349 '-Capture'
350 'wgc'
351 )
352 $first = Invoke-Manager -ManagerArguments $wgcArguments
353 $matching = Invoke-Manager -ManagerArguments $wgcArguments
354
355 $first.ExitCode | Should Be 0
356 $matching.ExitCode | Should Be 0
357 $matching.Json.status | Should Be 'already_running'
358 $matching.Json.pid | Should Be $first.Json.pid
359
360 $mismatch = Invoke-Manager -ManagerArguments @(
361 '-Action'
362 'Start'
363 '-GroupId'
364 $groupId
365 '-DisplayId'
366 $displayId
367 '-Port'
368 [string]$port
369 '-Capture'
370 'auto'
371 )
372 $mismatch.ExitCode | Should Not Be 0
373 $mismatch.Json.message | Should Match 'use Restart'
374 (Get-Process -Id ([int]$first.Json.pid) -ErrorAction Stop).HasExited |
375 Should Be $false
376
377 $restart = Invoke-Manager -ManagerArguments @(
378 '-Action'
379 'Restart'
380 '-GroupId'
381 $groupId
382 '-DisplayId'
383 $displayId
384 '-Port'
385 [string]$port
386 '-Capture'
387 'auto'
388 )
389 $restart.ExitCode | Should Be 0
390 $restart.Json.pid | Should Not Be $first.Json.pid
391 $restart.Json.port | Should Be $first.Json.port
392 Get-CimInstance `
393 -ClassName Win32_Process `
394 -Filter "ProcessId = $([int]$first.Json.pid)" `
395 -ErrorAction Stop |
396 Should BeNullOrEmpty
397 (Get-Process -Id ([int]$restart.Json.pid) -ErrorAction Stop).HasExited |
398 Should Be $false
399 $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 1
404
405 $null = Invoke-Manager -ManagerArguments @(
406 '-Action'
407 'Stop'
408 '-GroupId'
409 $groupId
410 )
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-FreeSunshineBasePort
418
419 $first = Invoke-Manager -ManagerArguments @(
420 '-Action'
421 'Start'
422 '-GroupId'
423 $firstGroup
424 '-DisplayId'
425 $displayId
426 '-Port'
427 [string]$preferredPort
428 )
429 $second = Invoke-Manager -ManagerArguments @(
430 '-Action'
431 'Start'
432 '-GroupId'
433 $secondGroup
434 '-DisplayId'
435 $displayId
436 '-Port'
437 [string]$preferredPort
438 )
439
440 $first.ExitCode | Should Be 0
441 $second.ExitCode | Should Be 0
442 $first.Json.port | Should Be $preferredPort
443 $second.Json.port | Should Not Be $preferredPort
444 (([int]$second.Json.port - $preferredPort) % 27) | Should Be 0
445
446 $stopAll = Invoke-Manager -ManagerArguments @(
447 '-Action'
448 'StopAll'
449 )
450 $stopAll.ExitCode | Should Be 0
451 $stopAll.Json.ok | Should Be $true
452 $stopAll.Json.status | Should Be 'stopped_all'
453 $stopAll.Json.stoppedCount | Should Be 2
454
455 $firstRemaining = Get-CimInstance `
456 -ClassName Win32_Process `
457 -Filter "ProcessId = $([int]$first.Json.pid)" `
458 -ErrorAction Stop
459 $secondRemaining = Get-CimInstance `
460 -ClassName Win32_Process `
461 -Filter "ProcessId = $([int]$second.Json.pid)" `
462 -ErrorAction Stop
463 $firstRemaining | Should BeNullOrEmpty
464 $secondRemaining | Should BeNullOrEmpty
465 }
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-Null
473 $manifest = [ordered]@{
474 schema_version = 1
475 managed_by = 'SBMS'
476 group_id = $groupId
477 display_id = '{23f8cce0-fefe-4a5b-9e14-abcdef012345}'
478 port = 54321
479 pid = $PID
480 sunshine_exe = $script:FakeExecutable
481 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 $groupId
496 )
497
498 $stop.ExitCode | Should Not Be 0
499 $stop.Json.ok | Should Be $false
500 $stop.Json.message | Should Match 'not the Sunshine executable'
501 (Get-Process -Id $PID -ErrorAction Stop).HasExited | Should Be $false
502 }
503}
504
505try {
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-Json
519 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:OriginalLocalAppData
533 $env:SBMS_SUNSHINE_EXE = $script:OriginalSunshineExe
534 if (Test-Path -LiteralPath $script:TestRoot) {
535 Remove-Item -LiteralPath $script:TestRoot -Recurse -Force
536 }
537}
538