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.
installer/manage-sunshine-instance.ps127.9 KB
1
[CmdletBinding()]2
param(3
[Parameter(Mandatory)]4
[ValidateSet('Start', 'Restart', 'Stop', 'StopAll')]5
[string]$Action,6
7
[string]$GroupId = '',8
9
[AllowNull()]10
[AllowEmptyString()]11
[string]$DisplayId,12
13
[ValidateRange(0, 65514)]14
[int]$Port = 0,15
16
[ValidateSet('auto', 'ddx', 'wgc')]17
[string]$Capture = 'auto'18
)19
20
$ErrorActionPreference = 'Stop'21
$ProgressPreference = 'SilentlyContinue'22
$WarningPreference = 'SilentlyContinue'23
Set-StrictMode -Version Latest24
25
$script:Utf8NoBom = [Text.UTF8Encoding]::new($false)26
$script:ManagerName = 'SBMS'27
$script:ManifestSchema = 128
29
function Write-JsonResult {30
param(31
[Parameter(Mandatory)]32
[hashtable]$Value33
)34
35
$Value | ConvertTo-Json -Compress -Depth 536
}37
38
function Assert-GroupId {39
param(40
[AllowNull()]41
[AllowEmptyString()]42
[string]$Id43
)44
45
if (46
[string]::IsNullOrWhiteSpace($Id) -or47
$Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'48
) {49
throw 'GroupId must contain 1-64 ASCII letters, digits, dots, underscores, or hyphens.'50
}51
}52
53
function Get-FullPath {54
param(55
[Parameter(Mandatory)]56
[string]$Path57
)58
59
return [IO.Path]::GetFullPath($Path)60
}61
62
function Test-SamePath {63
param(64
[Parameter(Mandatory)]65
[string]$Left,66
67
[Parameter(Mandatory)]68
[string]$Right69
)70
71
$leftPath = Get-FullPath -Path $Left72
$rightPath = Get-FullPath -Path $Right73
return [StringComparer]::OrdinalIgnoreCase.Equals($leftPath, $rightPath)74
}75
76
function Get-InstanceLayout {77
param(78
[Parameter(Mandatory)]79
[string]$Id80
)81
82
if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {83
throw 'LOCALAPPDATA is unavailable; the Sunshine instance root cannot be resolved.'84
}85
86
$root = Get-FullPath -Path (Join-Path $env:LOCALAPPDATA 'SBMS\sunshine')87
$instance = Get-FullPath -Path (Join-Path $root "group-$Id")88
$rootPrefix = $root.TrimEnd(89
[IO.Path]::DirectorySeparatorChar,90
[IO.Path]::AltDirectorySeparatorChar91
) + [IO.Path]::DirectorySeparatorChar92
93
if (-not $instance.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {94
throw "The resolved instance path is outside the SBMS Sunshine root: $instance"95
}96
97
$credentials = Join-Path $instance 'credentials'98
return [pscustomobject]@{99
Root = $root100
Instance = $instance101
Credentials = $credentials102
Config = Join-Path $instance 'sunshine.conf'103
State = Join-Path $instance 'sunshine_state.json'104
WebCredentials = Join-Path $instance 'web_credentials.json'105
PrivateKey = Join-Path $credentials 'cakey.pem'106
Certificate = Join-Path $credentials 'cacert.pem'107
Apps = Join-Path $instance 'apps.json'108
Log = Join-Path $instance 'sunshine.log'109
Manifest = Join-Path $instance 'instance.json'110
}111
}112
113
function Resolve-SunshineExecutable {114
if (-not [string]::IsNullOrWhiteSpace($env:SBMS_SUNSHINE_EXE)) {115
$override = Get-FullPath -Path $env:SBMS_SUNSHINE_EXE116
if (-not (Test-Path -LiteralPath $override -PathType Leaf)) {117
throw "SBMS_SUNSHINE_EXE does not point to a file: $override"118
}119
return $override120
}121
122
$programFiles = [Environment]::GetFolderPath(123
[Environment+SpecialFolder]::ProgramFiles124
)125
$candidates = @(126
(Join-Path $programFiles 'Sunshine\sunshine.exe')127
)128
129
if (-not [string]::IsNullOrWhiteSpace(${env:ProgramFiles(x86)})) {130
$candidates += Join-Path ${env:ProgramFiles(x86)} 'Sunshine\sunshine.exe'131
}132
133
foreach ($candidate in $candidates) {134
if (Test-Path -LiteralPath $candidate -PathType Leaf) {135
return Get-FullPath -Path $candidate136
}137
}138
139
throw 'Sunshine was not found under Program Files.'140
}141
142
function ConvertTo-SunshinePath {143
param(144
[Parameter(Mandatory)]145
[string]$Path146
)147
148
return (Get-FullPath -Path $Path).Replace('\', '/')149
}150
151
function Write-ManagedTextFile {152
param(153
[Parameter(Mandatory)]154
[string]$Path,155
156
[Parameter(Mandatory)]157
[string]$Contents158
)159
160
[IO.File]::WriteAllText($Path, $Contents, $script:Utf8NoBom)161
}162
163
function Ensure-AppsConfiguration {164
param(165
[Parameter(Mandatory)]166
[pscustomobject]$Layout,167
168
[Parameter(Mandatory)]169
[string]$SunshineExecutable170
)171
172
if (Test-Path -LiteralPath $Layout.Apps -PathType Leaf) {173
return174
}175
176
$sunshineRoot = Split-Path -Parent $SunshineExecutable177
$source = Join-Path $sunshineRoot 'config\apps.json'178
if (Test-Path -LiteralPath $source -PathType Leaf) {179
Copy-Item -LiteralPath $source -Destination $Layout.Apps180
return181
}182
183
Write-ManagedTextFile -Path $Layout.Apps -Contents '{"env":{},"apps":[]}'184
}185
186
function Write-InstanceConfiguration {187
param(188
[Parameter(Mandatory)]189
[pscustomobject]$Layout,190
191
[Parameter(Mandatory)]192
[string]$NormalizedDisplayId,193
194
[Parameter(Mandatory)]195
[int]$BasePort,196
197
[Parameter(Mandatory)]198
[string]$Id199
)200
201
$outputLabel = $Id202
$numericId = 0203
if ([int]::TryParse($Id, [ref]$numericId)) {204
$outputLabel = ($numericId + 1).ToString()205
}206
$lines = @(207
'# Managed by SBMS. Runtime state is isolated per mapping group.'208
"port = $BasePort"209
"sunshine_name = SBMS Output $outputLabel"210
"output_name = $NormalizedDisplayId"211
'upnp = disabled'212
'dd_configuration_option = disabled'213
'system_tray = disabled'214
"file_state = $(ConvertTo-SunshinePath -Path $Layout.State)"215
"credentials_file = $(ConvertTo-SunshinePath -Path $Layout.WebCredentials)"216
"pkey = $(ConvertTo-SunshinePath -Path $Layout.PrivateKey)"217
"cert = $(ConvertTo-SunshinePath -Path $Layout.Certificate)"218
"file_apps = $(ConvertTo-SunshinePath -Path $Layout.Apps)"219
"log_path = $(ConvertTo-SunshinePath -Path $Layout.Log)"220
)221
$contents = ($lines -join [Environment]::NewLine) +222
[Environment]::NewLine223
Write-ManagedTextFile -Path $Layout.Config -Contents $contents224
}225
226
function Write-InstanceManifest {227
param(228
[Parameter(Mandatory)]229
[pscustomobject]$Layout,230
231
[Parameter(Mandatory)]232
[string]$Id,233
234
[Parameter(Mandatory)]235
[string]$NormalizedDisplayId,236
237
[Parameter(Mandatory)]238
[int]$BasePort,239
240
[Parameter(Mandatory)]241
[int]$ProcessId,242
243
[Parameter(Mandatory)]244
[string]$SunshineExecutable245
)246
247
$manifest = [ordered]@{248
schema_version = $script:ManifestSchema249
managed_by = $script:ManagerName250
group_id = $Id251
display_id = $NormalizedDisplayId252
port = $BasePort253
pid = $ProcessId254
sunshine_exe = Get-FullPath -Path $SunshineExecutable255
config_path = Get-FullPath -Path $Layout.Config256
log_path = Get-FullPath -Path $Layout.Log257
started_at = [DateTimeOffset]::Now.ToString('o')258
}259
$json = $manifest | ConvertTo-Json -Compress -Depth 4260
Write-ManagedTextFile -Path $Layout.Manifest -Contents $json261
}262
263
function Read-InstanceManifest {264
param(265
[Parameter(Mandatory)]266
[pscustomobject]$Layout,267
268
[Parameter(Mandatory)]269
[string]$Id270
)271
272
if (-not (Test-Path -LiteralPath $Layout.Manifest -PathType Leaf)) {273
return $null274
}275
276
try {277
$json = [IO.File]::ReadAllText(278
$Layout.Manifest,279
[Text.Encoding]::UTF8280
)281
$manifest = $json | ConvertFrom-Json282
} catch {283
throw "The Sunshine instance manifest is invalid: $($_.Exception.Message)"284
}285
286
if (287
$null -eq $manifest.schema_version -or288
[int]$manifest.schema_version -ne $script:ManifestSchema -or289
[string]$manifest.managed_by -ne $script:ManagerName -or290
[string]$manifest.group_id -cne $Id291
) {292
throw 'The Sunshine instance manifest is not owned by this SBMS group.'293
}294
295
if (296
[string]::IsNullOrWhiteSpace([string]$manifest.config_path) -or297
-not (Test-SamePath -Left ([string]$manifest.config_path) -Right $Layout.Config)298
) {299
throw 'The Sunshine instance manifest points outside this group configuration.'300
}301
302
if (303
$null -eq $manifest.pid -or304
[int64]$manifest.pid -lt 1 -or305
[int64]$manifest.pid -gt [int]::MaxValue -or306
[string]::IsNullOrWhiteSpace([string]$manifest.sunshine_exe)307
) {308
throw 'The Sunshine instance manifest does not contain a valid managed process.'309
}310
311
return $manifest312
}313
314
function Get-ManagedProcessRecord {315
param(316
[Parameter(Mandatory)]317
[pscustomobject]$Manifest,318
319
[Parameter(Mandatory)]320
[string]$ExpectedConfig321
)322
323
$processId = [int]$Manifest.pid324
$record = Get-CimInstance -ClassName Win32_Process `325
-Filter "ProcessId = $processId" `326
-ErrorAction Stop |327
Select-Object -First 1328
if ($null -eq $record) {329
return $null330
}331
332
$actualExecutable = [string]$record.ExecutablePath333
if ([string]::IsNullOrWhiteSpace($actualExecutable)) {334
try {335
$actualExecutable = (336
Get-Process -Id $processId -ErrorAction Stop337
).MainModule.FileName338
} catch {339
throw "PID $processId exists, but its executable path cannot be verified."340
}341
}342
343
if (344
-not (Test-SamePath `345
-Left $actualExecutable `346
-Right ([string]$Manifest.sunshine_exe))347
) {348
throw "PID $processId is not the Sunshine executable recorded for this group."349
}350
351
$commandLine = [string]$record.CommandLine352
if ([string]::IsNullOrWhiteSpace($commandLine)) {353
throw "PID $processId exists, but its command line cannot be verified."354
}355
356
$escapedConfig = [regex]::Escape((Get-FullPath -Path $ExpectedConfig))357
$configArgument = "(?i)(?:^|\s)`"?$escapedConfig`"?(?=\s|$)"358
if ($commandLine -notmatch $configArgument) {359
throw "PID $processId is not using this group's Sunshine configuration."360
}361
362
return $record363
}364
365
function Get-CaptureBackendFromCommandLine {366
param(367
[Parameter(Mandatory)]368
[string]$CommandLine369
)370
371
$captureArguments = [regex]::Matches(372
$CommandLine,373
'(?i)(?:^|\s)"?capture=([^\s"]+)"?(?=\s|$)'374
)375
if ($captureArguments.Count -eq 0) {376
return 'auto'377
}378
if ($captureArguments.Count -ne 1) {379
throw 'The managed Sunshine process has multiple capture overrides.'380
}381
382
$capture = $captureArguments[0].Groups[1].Value.ToLowerInvariant()383
if ($capture -notin @('auto', 'ddx', 'wgc')) {384
throw "The managed Sunshine process has an unknown capture override: $capture"385
}386
return $capture387
}388
389
function Remove-StaleManifest {390
param(391
[Parameter(Mandatory)]392
[pscustomobject]$Layout393
)394
395
if (Test-Path -LiteralPath $Layout.Manifest -PathType Leaf) {396
Remove-Item -LiteralPath $Layout.Manifest -Force397
}398
}399
400
function Stop-ManagedInstance {401
param(402
[Parameter(Mandatory)]403
[pscustomobject]$Layout,404
405
[Parameter(Mandatory)]406
[string]$Id407
)408
409
$manifest = Read-InstanceManifest -Layout $Layout -Id $Id410
if ($null -eq $manifest) {411
return [ordered]@{412
status = 'not_running'413
pid = $null414
port = $null415
display_id = $null416
log_path = Get-FullPath -Path $Layout.Log417
}418
}419
420
$record = Get-ManagedProcessRecord `421
-Manifest $manifest `422
-ExpectedConfig $Layout.Config423
if ($null -eq $record) {424
Remove-StaleManifest -Layout $Layout425
return [ordered]@{426
status = 'not_running'427
pid = $null428
port = [int]$manifest.port429
display_id = [string]$manifest.display_id430
log_path = [string]$manifest.log_path431
}432
}433
434
$processId = [int]$manifest.pid435
$process = Get-Process -Id $processId -ErrorAction Stop436
$closed = $false437
try {438
$closed = $process.CloseMainWindow()439
} catch {440
$closed = $false441
}442
443
if ($closed) {444
$null = $process.WaitForExit(2000)445
}446
if (-not $process.HasExited) {447
Stop-Process -Id $processId -Force -ErrorAction Stop448
$null = $process.WaitForExit(10000)449
}450
if (-not $process.HasExited) {451
throw "Managed Sunshine PID $processId did not stop within 10 seconds."452
}453
454
Remove-StaleManifest -Layout $Layout455
return [ordered]@{456
status = 'stopped'457
pid = $processId458
port = [int]$manifest.port459
display_id = [string]$manifest.display_id460
log_path = [string]$manifest.log_path461
}462
}463
464
function Stop-AllManagedInstances {465
if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {466
throw 'LOCALAPPDATA is unavailable; the Sunshine instance root cannot be resolved.'467
}468
469
$root = Get-FullPath -Path (Join-Path $env:LOCALAPPDATA 'SBMS\sunshine')470
if (-not (Test-Path -LiteralPath $root -PathType Container)) {471
return [ordered]@{472
status = 'not_running'473
pid = $null474
port = $null475
display_id = $null476
log_path = $root477
stoppedCount = 0478
}479
}480
481
$stoppedCount = 0482
$failures = [Collections.Generic.List[string]]::new()483
$directories = @(484
Get-ChildItem `485
-LiteralPath $root `486
-Directory `487
-Filter 'group-*' `488
-ErrorAction Stop489
)490
foreach ($directory in $directories) {491
$id = $directory.Name.Substring('group-'.Length)492
if ($id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$') {493
continue494
}495
496
$layout = Get-InstanceLayout -Id $id497
if (-not (Test-Path -LiteralPath $layout.Manifest -PathType Leaf)) {498
continue499
}500
501
try {502
$operation = Stop-ManagedInstance -Layout $layout -Id $id503
if ($operation.status -eq 'stopped') {504
$stoppedCount++505
}506
} catch {507
$failures.Add("$id`: $($_.Exception.Message)")508
}509
}510
511
if ($failures.Count -gt 0) {512
$details = $failures -join '; '513
throw "Stopped $stoppedCount managed Sunshine instance(s), but some could not be verified or stopped: $details"514
}515
516
return [ordered]@{517
status = if ($stoppedCount -gt 0) {518
'stopped_all'519
} else {520
'not_running'521
}522
pid = $null523
port = $null524
display_id = $null525
log_path = $root526
stoppedCount = $stoppedCount527
}528
}529
530
function Test-TcpPortAvailable {531
param(532
[Parameter(Mandatory)]533
[int]$CandidatePort534
)535
536
$listener = $null537
try {538
$listener = [Net.Sockets.TcpListener]::new(539
[Net.IPAddress]::Any,540
$CandidatePort541
)542
$listener.Server.ExclusiveAddressUse = $true543
$listener.Start()544
return $true545
} catch {546
return $false547
} finally {548
if ($null -ne $listener) {549
$listener.Stop()550
}551
}552
}553
554
function Test-UdpPortAvailable {555
param(556
[Parameter(Mandatory)]557
[int]$CandidatePort558
)559
560
$client = $null561
try {562
$client = [Net.Sockets.UdpClient]::new()563
$client.Client.ExclusiveAddressUse = $true564
$endpoint = [Net.IPEndPoint]::new(565
[Net.IPAddress]::Any,566
$CandidatePort567
)568
$client.Client.Bind($endpoint)569
return $true570
} catch {571
return $false572
} finally {573
if ($null -ne $client) {574
$client.Dispose()575
}576
}577
}578
579
function Test-PortFamilyAvailable {580
param(581
[Parameter(Mandatory)]582
[int]$BasePort583
)584
585
$tcpOffsets = @(-5, 0, 1, 21)586
foreach ($offset in $tcpOffsets) {587
$candidate = $BasePort + $offset588
if (-not (Test-TcpPortAvailable -CandidatePort $candidate)) {589
return $false590
}591
}592
593
$udpOffsets = @(9, 10, 11)594
foreach ($offset in $udpOffsets) {595
$candidate = $BasePort + $offset596
if (-not (Test-UdpPortAvailable -CandidatePort $candidate)) {597
return $false598
}599
}600
601
return $true602
}603
604
function Find-AvailableSunshinePort {605
param(606
[Parameter(Mandatory)]607
[int]$PreferredPort608
)609
610
$candidate = [int64]$PreferredPort611
while ($candidate -le 65514) {612
if (Test-PortFamilyAvailable -BasePort ([int]$candidate)) {613
return [int]$candidate614
}615
$candidate += 27616
}617
618
throw "No complete Sunshine port family is available from base port $PreferredPort in +27 increments."619
}620
621
function Format-SunshineArguments {622
param(623
[Parameter(Mandatory)]624
[string]$Configuration,625
626
[Parameter(Mandatory)]627
[ValidateSet('auto', 'ddx', 'wgc')]628
[string]$CaptureBackend629
)630
631
if ($Configuration.Contains('"')) {632
throw 'The Sunshine configuration path contains an unsupported quote.'633
}634
635
$arguments = '"' + $Configuration + '"'636
if ($CaptureBackend -ne 'auto') {637
$arguments += ' capture=' + $CaptureBackend638
}639
return $arguments640
}641
642
function Start-HiddenSunshine {643
param(644
[Parameter(Mandatory)]645
[string]$SunshineExecutable,646
647
[Parameter(Mandatory)]648
[string]$Configuration,649
650
[Parameter(Mandatory)]651
[string]$WorkingDirectory,652
653
[Parameter(Mandatory)]654
[ValidateSet('auto', 'ddx', 'wgc')]655
[string]$CaptureBackend656
)657
658
$startInfo = [Diagnostics.ProcessStartInfo]::new()659
$startInfo.FileName = $SunshineExecutable660
$startInfo.WorkingDirectory = $WorkingDirectory661
$startInfo.UseShellExecute = $true662
$startInfo.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden663
$startInfo.ErrorDialog = $false664
665
# Windows PowerShell 5.1 has no ProcessStartInfo.ArgumentList. Build the666
# native argument string once so the configuration remains one exact arg.667
$startInfo.Arguments = Format-SunshineArguments `668
-Configuration $Configuration `669
-CaptureBackend $CaptureBackend670
671
$process = [Diagnostics.Process]::Start($startInfo)672
if ($null -eq $process) {673
throw 'Windows did not return a Sunshine process handle.'674
}675
return $process676
}677
678
function Test-TcpEndpoint {679
param(680
[Parameter(Mandatory)]681
[int]$EndpointPort,682
683
[int]$TimeoutMilliseconds = 250684
)685
686
$client = [Net.Sockets.TcpClient]::new()687
$asyncResult = $null688
try {689
$asyncResult = $client.BeginConnect(690
[Net.IPAddress]::Loopback,691
$EndpointPort,692
$null,693
$null694
)695
if (-not $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)) {696
return $false697
}698
$client.EndConnect($asyncResult)699
return $client.Connected700
} catch {701
return $false702
} finally {703
if ($null -ne $asyncResult) {704
$asyncResult.AsyncWaitHandle.Close()705
}706
$client.Dispose()707
}708
}709
710
function Wait-SunshineReady {711
param(712
[Parameter(Mandatory)]713
[Diagnostics.Process]$Process,714
715
[Parameter(Mandatory)]716
[int]$BasePort,717
718
[Parameter(Mandatory)]719
[string]$LogPath720
)721
722
$deadline = [DateTimeOffset]::UtcNow.AddSeconds(20)723
do {724
$Process.Refresh()725
if ($Process.HasExited) {726
throw "Sunshine exited before its port opened (exit code $($Process.ExitCode)); see $LogPath"727
}728
729
if (730
(Test-TcpEndpoint -EndpointPort ($BasePort + 1)) -or731
(Test-TcpEndpoint -EndpointPort $BasePort)732
) {733
return734
}735
736
Start-Sleep -Milliseconds 250737
} while ([DateTimeOffset]::UtcNow -lt $deadline)738
739
throw "Sunshine did not open TCP port $BasePort or $($BasePort + 1) within 20 seconds; see $LogPath"740
}741
742
function Normalize-DisplayId {743
param(744
[AllowNull()]745
[AllowEmptyString()]746
[string]$Value747
)748
749
if ([string]::IsNullOrWhiteSpace($Value)) {750
throw 'DisplayId is required for Start and Restart.'751
}752
753
$parsed = [Guid]::Empty754
if (-not [Guid]::TryParseExact($Value, 'B', [ref]$parsed)) {755
throw 'DisplayId must be a brace-enclosed GUID.'756
}757
return $parsed.ToString('B')758
}759
760
function Start-ManagedInstance {761
param(762
[Parameter(Mandatory)]763
[pscustomobject]$Layout,764
765
[Parameter(Mandatory)]766
[string]$Id,767
768
[Parameter(Mandatory)]769
[string]$NormalizedDisplayId,770
771
[Parameter(Mandatory)]772
[int]$BasePort,773
774
[Parameter(Mandatory)]775
[string]$SunshineExecutable,776
777
[Parameter(Mandatory)]778
[ValidateSet('auto', 'ddx', 'wgc')]779
[string]$CaptureBackend,780
781
[switch]$AllowAlreadyRunning782
)783
784
$existing = Read-InstanceManifest -Layout $Layout -Id $Id785
if ($null -ne $existing) {786
$record = Get-ManagedProcessRecord `787
-Manifest $existing `788
-ExpectedConfig $Layout.Config789
if ($null -ne $record) {790
$sameDisplay = [string]$existing.display_id -ieq $NormalizedDisplayId791
$existingPort = [int]$existing.port792
$isRequestedSequence = (793
$existingPort -ge $BasePort -and794
(($existingPort - $BasePort) % 27) -eq 0795
)796
$existingCapture = Get-CaptureBackendFromCommandLine `797
-CommandLine ([string]$record.CommandLine)798
$sameCapture = $existingCapture -eq $CaptureBackend799
if (800
$AllowAlreadyRunning -and801
$sameDisplay -and802
$isRequestedSequence -and803
$sameCapture804
) {805
return [ordered]@{806
status = 'already_running'807
pid = [int]$existing.pid808
port = [int]$existing.port809
display_id = [string]$existing.display_id810
config_path = [string]$existing.config_path811
log_path = [string]$existing.log_path812
}813
}814
throw 'A managed Sunshine instance is already running for this group; use Restart.'815
}816
Remove-StaleManifest -Layout $Layout817
}818
819
$actualPort = Find-AvailableSunshinePort -PreferredPort $BasePort820
New-Item -ItemType Directory -Path $Layout.Credentials -Force | Out-Null821
Ensure-AppsConfiguration `822
-Layout $Layout `823
-SunshineExecutable $SunshineExecutable824
Write-InstanceConfiguration `825
-Layout $Layout `826
-NormalizedDisplayId $NormalizedDisplayId `827
-BasePort $actualPort `828
-Id $Id829
830
$process = $null831
try {832
$process = Start-HiddenSunshine `833
-SunshineExecutable $SunshineExecutable `834
-Configuration $Layout.Config `835
-WorkingDirectory (Split-Path -Parent $SunshineExecutable) `836
-CaptureBackend $CaptureBackend837
Write-InstanceManifest `838
-Layout $Layout `839
-Id $Id `840
-NormalizedDisplayId $NormalizedDisplayId `841
-BasePort $actualPort `842
-ProcessId $process.Id `843
-SunshineExecutable $SunshineExecutable844
Wait-SunshineReady `845
-Process $process `846
-BasePort $actualPort `847
-LogPath $Layout.Log848
} catch {849
if ($null -ne $process) {850
try {851
$process.Refresh()852
if (-not $process.HasExited) {853
$process.Kill()854
$null = $process.WaitForExit(5000)855
}856
} catch {857
# The original startup error is more useful than cleanup noise.858
}859
}860
Remove-StaleManifest -Layout $Layout861
throw862
}863
864
return [ordered]@{865
status = 'started'866
pid = $process.Id867
port = $actualPort868
display_id = $NormalizedDisplayId869
config_path = Get-FullPath -Path $Layout.Config870
log_path = Get-FullPath -Path $Layout.Log871
}872
}873
874
$layout = $null875
$mutex = $null876
$lockTaken = $false877
$exitCode = 0878
$result = $null879
$normalizedAction = $Action.ToLowerInvariant()880
try {881
$mutex = [Threading.Mutex]::new(882
$false,883
'Local\SBMSSunshineInstanceManager-v1'884
)885
try {886
$lockTaken = $mutex.WaitOne([TimeSpan]::FromSeconds(30))887
} catch [Threading.AbandonedMutexException] {888
$lockTaken = $true889
}890
if (-not $lockTaken) {891
throw 'Timed out waiting for the SBMS Sunshine instance manager lock.'892
}893
894
if ($Action -eq 'StopAll') {895
$operation = Stop-AllManagedInstances896
} else {897
Assert-GroupId -Id $GroupId898
$layout = Get-InstanceLayout -Id $GroupId899
900
if ($Action -eq 'Stop') {901
$operation = Stop-ManagedInstance -Layout $layout -Id $GroupId902
} else {903
if ($Port -lt 1029) {904
throw 'Port must be in Sunshine''s supported base-port range 1029-65514.'905
}906
$normalizedDisplayId = Normalize-DisplayId -Value $DisplayId907
$sunshineExecutable = Resolve-SunshineExecutable908
909
if ($Action -eq 'Restart') {910
$null = Stop-ManagedInstance -Layout $layout -Id $GroupId911
}912
913
$startParameters = @{914
Layout = $layout915
Id = $GroupId916
NormalizedDisplayId = $normalizedDisplayId917
BasePort = $Port918
SunshineExecutable = $sunshineExecutable919
CaptureBackend = $Capture.ToLowerInvariant()920
AllowAlreadyRunning = ($Action -eq 'Start')921
}922
$operation = Start-ManagedInstance @startParameters923
}924
}925
926
$message = switch ($operation.status) {927
'started' {928
"Sunshine instance started on base port $($operation.port)."929
}930
'already_running' {931
"Sunshine instance is already running on base port $($operation.port)."932
}933
'stopped' {934
'Sunshine instance stopped.'935
}936
'stopped_all' {937
"Stopped $($operation.stoppedCount) managed Sunshine instance(s)."938
}939
default {940
'No managed Sunshine instance was running.'941
}942
}943
944
$configPath = if (945
$null -ne $layout -and946
$operation.Contains('config_path')947
) {948
$operation.config_path949
} elseif ($null -ne $layout) {950
Get-FullPath -Path $layout.Config951
} else {952
$null953
}954
$result = [ordered]@{955
ok = $true956
action = $normalizedAction957
status = $operation.status958
groupId = if ($Action -eq 'StopAll') { $null } else { $GroupId }959
displayId = $operation.display_id960
port = $operation.port961
pid = $operation.pid962
message = $message963
configPath = $configPath964
logPath = $operation.log_path965
stoppedCount = if ($operation.Contains('stoppedCount')) {966
$operation.stoppedCount967
} else {968
$null969
}970
}971
} catch {972
$exitCode = 1973
$logPath = if ($null -ne $layout) {974
Get-FullPath -Path $layout.Log975
} else {976
$null977
}978
$result = [ordered]@{979
ok = $false980
action = $normalizedAction981
groupId = if ([string]::IsNullOrWhiteSpace($GroupId)) {982
$null983
} else {984
$GroupId985
}986
displayId = $null987
port = $null988
pid = $null989
message = $_.Exception.Message990
configPath = if ($null -ne $layout) {991
Get-FullPath -Path $layout.Config992
} else {993
$null994
}995
logPath = $logPath996
}997
} finally {998
if ($lockTaken -and $null -ne $mutex) {999
$mutex.ReleaseMutex()1000
}1001
if ($null -ne $mutex) {1002
$mutex.Dispose()1003
}1004
}1005
1006
Write-JsonResult -Value $result1007
exit $exitCode1008