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/maintenance.ps118.2 KB
1
[CmdletBinding()]2
param(3
[Parameter(Mandatory)]4
[ValidateSet('Install', 'PrepareUpgrade', 'Stop', 'PreflightUninstall', 'Uninstall')]5
[string]$Action,6
7
[Parameter(Mandatory)]8
[string]$InstallRoot9
)10
11
$ErrorActionPreference = 'Stop'12
Set-StrictMode -Version Latest13
14
$taskPath = '\SBMS\'15
$taskName = 'Tray-7EB4D7A8-16A9-4B6F-82E3-31A77BC81B6A'16
$legacyTaskPath = '\'17
$legacyTaskName = 'SBMS Tray'18
$tray = Join-Path $InstallRoot 'sbms-tray.exe'19
$cli = Join-Path $InstallRoot 'sbms.exe'20
$driverInf = Join-Path $InstallRoot 'driver\SBMSIndirectDisplay.inf'21
$deviceInstance = 'SWD\SBMS\VirtualDisplay-01'22
$configRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'SBMS'23
$backupRetention = 524
$backupOwnershipMarker = '.sbms-upgrade-snapshot-v1'25
$backupOwnershipContent = 'SBMS upgrade snapshot v1'26
27
function Get-Sha256Hex {28
param([Parameter(Mandatory)][string]$LiteralPath)29
30
$algorithm = [Security.Cryptography.SHA256]::Create()31
$stream = [IO.File]::OpenRead($LiteralPath)32
try {33
return [BitConverter]::ToString(34
$algorithm.ComputeHash($stream)35
).Replace('-', '')36
}37
finally {38
$stream.Dispose()39
$algorithm.Dispose()40
}41
}42
43
function Get-InteractiveIdentity {44
$sessionId = (Get-Process -Id $PID).SessionId45
$explorer = Get-Process -Name 'explorer' -ErrorAction SilentlyContinue |46
Where-Object { $_.SessionId -eq $sessionId } |47
Select-Object -First 148
if (-not $explorer) {49
throw 'No interactive Explorer process exists in the installer session.'50
}51
$process = Get-CimInstance -ClassName Win32_Process `52
-Filter "ProcessId = $($explorer.Id)"53
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwner54
if ($owner.ReturnValue -ne 0 -or -not $owner.User) {55
throw 'The interactive user identity could not be resolved.'56
}57
$name = if ($owner.Domain) {58
"$($owner.Domain)\$($owner.User)"59
}60
else {61
$owner.User62
}63
$account = [Security.Principal.NTAccount]::new($name)64
$sid = $account.Translate([Security.Principal.SecurityIdentifier])65
[pscustomobject]@{66
Name = $name67
Sid = $sid.Value68
}69
}70
71
function Assert-InstallIdentity {72
$interactive = Get-InteractiveIdentity73
$current = [Security.Principal.WindowsIdentity]::GetCurrent()74
if ($interactive.Sid -ne $current.User.Value) {75
throw (76
'SBMS must be installed from the intended administrator account. ' +77
'Over-the-shoulder credentials would register auto-start for the wrong user.'78
)79
}80
$principal = [Security.Principal.WindowsPrincipal]::new($current)81
if (-not $principal.IsInRole(82
[Security.Principal.WindowsBuiltInRole]::Administrator)) {83
throw 'SBMS setup maintenance requires an elevated administrator token.'84
}85
$interactive86
}87
88
function Get-InstalledSbmsProcesses {89
$targets = @(90
[IO.Path]::GetFullPath($tray),91
[IO.Path]::GetFullPath($cli)92
)93
$found = @()94
foreach ($process in Get-Process -Name 'sbms-tray', 'sbms' `95
-ErrorAction SilentlyContinue) {96
try {97
$path = $process.Path98
}99
catch {100
throw "Cannot verify path for SBMS-named process $($process.Id)."101
}102
if (-not $path) {103
throw "Cannot verify path for SBMS-named process $($process.Id)."104
}105
$fullPath = [IO.Path]::GetFullPath($path)106
$matchesInstalledPath = $false107
foreach ($target in $targets) {108
if ($target.Equals(109
$fullPath,110
[StringComparison]::OrdinalIgnoreCase)) {111
$matchesInstalledPath = $true112
break113
}114
}115
if ($matchesInstalledPath) {116
$found += $process117
}118
}119
$found120
}121
122
function Stop-Sbms {123
$running = @(Get-InstalledSbmsProcesses)124
if ($running.Count -eq 0) {125
return126
}127
if (-not (Test-Path -LiteralPath $cli)) {128
throw 'An installed SBMS process is running but its shutdown CLI is missing.'129
}130
131
& $cli shutdown | Out-Null132
$shutdownExit = $LASTEXITCODE133
if ($shutdownExit -ne 0) {134
throw (135
'The installed SBMS version has no compatible graceful shutdown channel. ' +136
'Stop the mapping and exit its tray before upgrading.'137
)138
}139
140
$deadline = [DateTime]::UtcNow.AddSeconds(30)141
while ([DateTime]::UtcNow -lt $deadline) {142
if (@(Get-InstalledSbmsProcesses).Count -eq 0) {143
return144
}145
Start-Sleep -Milliseconds 200146
}147
throw 'An installed SBMS process did not stop within 30 seconds.'148
}149
150
function Stop-ManagedSunshineInstances {151
$manager = Join-Path $InstallRoot 'installer\manage-sunshine-instance.ps1'152
if (-not (Test-Path -LiteralPath $manager -PathType Leaf)) {153
return154
}155
156
$powershell = Join-Path $PSHOME 'powershell.exe'157
$managerArgs = @(158
'-NoLogo'159
'-NoProfile'160
'-NonInteractive'161
'-ExecutionPolicy'162
'Bypass'163
'-File'164
$manager165
'-Action'166
'StopAll'167
)168
$output = @(& $powershell @managerArgs 2>&1)169
$exitCode = $LASTEXITCODE170
$jsonLine = $output |171
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } |172
Select-Object -Last 1173
if ($exitCode -ne 0 -or $null -eq $jsonLine) {174
throw "Managed Sunshine cleanup failed with exit code $exitCode."175
}176
177
try {178
$result = [string]$jsonLine | ConvertFrom-Json179
} catch {180
throw "Managed Sunshine cleanup returned an invalid result: $jsonLine"181
}182
if (-not [bool]$result.ok) {183
throw "Managed Sunshine cleanup failed: $($result.message)"184
}185
}186
187
function Test-OwnedUpgradeSnapshot {188
param(189
[Parameter(Mandatory)]190
[IO.DirectoryInfo]$Snapshot,191
192
[Parameter(Mandatory)]193
[string]$BackupRoot194
)195
196
if ($Snapshot.Name -notmatch '^\d{8}T\d{9}Z$' -or197
($Snapshot.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {198
return $false199
}200
201
$rootPath = [IO.Path]::GetFullPath((Join-Path $BackupRoot '.'))202
$snapshotPath = [IO.Path]::GetFullPath($Snapshot.FullName)203
$parent = [IO.Directory]::GetParent($snapshotPath)204
if (-not $parent -or205
-not [string]::Equals(206
[IO.Path]::GetFullPath($parent.FullName),207
$rootPath,208
[StringComparison]::OrdinalIgnoreCase) -or209
-not [string]::Equals(210
$snapshotPath,211
[IO.Path]::GetFullPath((Join-Path $rootPath $Snapshot.Name)),212
[StringComparison]::OrdinalIgnoreCase)) {213
return $false214
}215
216
$markerPath = Join-Path $snapshotPath $backupOwnershipMarker217
$marker = Get-Item -LiteralPath $markerPath -Force -ErrorAction SilentlyContinue218
if (-not $marker -or219
$marker.PSIsContainer -or220
($marker.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {221
return $false222
}223
224
try {225
[IO.File]::ReadAllText($marker.FullName, [Text.Encoding]::UTF8) -ceq226
$backupOwnershipContent227
}228
catch {229
$false230
}231
}232
233
function Backup-SbmsConfiguration {234
$persistentNames = @(235
'config-v1.json'236
'config-v2.json'237
'config-profiles-v1.json'238
'display-overrides-v1.json'239
)240
$sources = @(241
$persistentNames |242
ForEach-Object { Join-Path $configRoot $_ } |243
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf }244
)245
if ($sources.Count -eq 0) {246
return247
}248
249
$backupRoot = Join-Path $configRoot 'upgrade-backups'250
$stamp = [DateTime]::UtcNow.ToString(251
'yyyyMMddTHHmmssfffZ',252
[Globalization.CultureInfo]::InvariantCulture)253
$snapshot = Join-Path $backupRoot $stamp254
New-Item -ItemType Directory -Path $snapshot -Force | Out-Null255
256
foreach ($source in $sources) {257
$name = Split-Path -Leaf $source258
$destination = Join-Path $snapshot $name259
Copy-Item -LiteralPath $source -Destination $destination260
$sourceHash = Get-Sha256Hex -LiteralPath $source261
$backupHash = Get-Sha256Hex -LiteralPath $destination262
if ($sourceHash -ne $backupHash) {263
throw "Configuration snapshot verification failed for $name."264
}265
}266
267
[IO.File]::WriteAllText(268
(Join-Path $snapshot $backupOwnershipMarker),269
$backupOwnershipContent,270
[Text.UTF8Encoding]::new($false))271
272
$staleSnapshots = @(273
Get-ChildItem -LiteralPath $backupRoot -Directory -Force |274
Where-Object {275
Test-OwnedUpgradeSnapshot -Snapshot $_ -BackupRoot $backupRoot276
} |277
Sort-Object Name -Descending |278
Select-Object -Skip $backupRetention279
)280
foreach ($staleSnapshot in $staleSnapshots) {281
$current = Get-Item -LiteralPath $staleSnapshot.FullName -Force `282
-ErrorAction SilentlyContinue283
if (-not $current -or284
-not (Test-OwnedUpgradeSnapshot `285
-Snapshot $current `286
-BackupRoot $backupRoot)) {287
throw "Upgrade snapshot ownership changed before retention cleanup."288
}289
Remove-Item -LiteralPath $current.FullName -Recurse -Force290
}291
}292
293
function Prepare-SbmsUpgrade {294
Assert-InstallIdentity | Out-Null295
Stop-Sbms296
Backup-SbmsConfiguration297
}298
299
function Get-VerifiedTask {300
param(301
[Parameter(Mandatory)]302
[string]$Path,303
304
[Parameter(Mandatory)]305
[string]$Name306
)307
308
$task = Get-ScheduledTask -TaskPath $Path -TaskName $Name `309
-ErrorAction SilentlyContinue310
if (-not $task) {311
return $null312
}313
if ($task.Actions.Count -ne 1) {314
throw "Scheduled task collision at $Path$Name."315
}316
$action = $task.Actions[0]317
$ownedArguments = [string]::IsNullOrEmpty($action.Arguments) -or318
$action.Arguments -ceq '--background'319
if (-not [string]::Equals(320
$action.Execute,321
$tray,322
[StringComparison]::OrdinalIgnoreCase) -or323
-not $ownedArguments -or324
-not [string]::Equals(325
$action.WorkingDirectory,326
$InstallRoot,327
[StringComparison]::OrdinalIgnoreCase)) {328
throw "Scheduled task collision at $Path$Name."329
}330
$task331
}332
333
function Get-OwnedTask {334
Get-VerifiedTask -Path $taskPath -Name $taskName335
}336
337
function Get-LegacyOwnedTask {338
Get-VerifiedTask -Path $legacyTaskPath -Name $legacyTaskName339
}340
341
function Get-OwnedTaskEntries {342
@(343
[pscustomobject]@{344
Task = Get-OwnedTask345
Path = $taskPath346
Name = $taskName347
},348
[pscustomobject]@{349
Task = Get-LegacyOwnedTask350
Path = $legacyTaskPath351
Name = $legacyTaskName352
}353
)354
}355
356
function Remove-SbmsTask {357
foreach ($entry in (Get-OwnedTaskEntries)) {358
if (-not $entry.Task) {359
continue360
}361
Stop-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name `362
-ErrorAction SilentlyContinue363
Unregister-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name `364
-Confirm:$false365
}366
}367
368
function Get-OwnedTaskSnapshots {369
foreach ($entry in (Get-OwnedTaskEntries)) {370
if (-not $entry.Task) {371
continue372
}373
[pscustomobject]@{374
Path = $entry.Path375
Name = $entry.Name376
Xml = Export-ScheduledTask `377
-TaskPath $entry.Path `378
-TaskName $entry.Name379
WasRunning = $entry.Task.State -eq 'Running'380
}381
}382
}383
384
function Restore-SbmsTasks {385
param([object[]]$Snapshots)386
387
Remove-SbmsTask388
foreach ($snapshot in $Snapshots) {389
Register-ScheduledTask `390
-TaskPath $snapshot.Path `391
-TaskName $snapshot.Name `392
-Xml $snapshot.Xml |393
Out-Null394
if ($snapshot.WasRunning) {395
Start-ScheduledTask `396
-TaskPath $snapshot.Path `397
-TaskName $snapshot.Name398
}399
}400
}401
402
function Install-SbmsTask {403
$interactive = Assert-InstallIdentity404
if (Get-OwnedTask) {405
throw "Owned scheduled task already exists at $taskPath$taskName."406
}407
$taskAction = New-ScheduledTaskAction -Execute $tray `408
-Argument '--background' `409
-WorkingDirectory $InstallRoot410
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactive.Sid411
$principal = New-ScheduledTaskPrincipal `412
-UserId $interactive.Sid `413
-LogonType Interactive `414
-RunLevel Highest415
$settings = New-ScheduledTaskSettingsSet `416
-AllowStartIfOnBatteries `417
-DontStopIfGoingOnBatteries `418
-ExecutionTimeLimit ([TimeSpan]::Zero) `419
-MultipleInstances IgnoreNew420
# A current-session logon trigger may fire as soon as the task is registered.421
# Snapshot existing processes first so that immediate start counts as the new tray.422
$existingProcessIds = @(423
Get-InstalledSbmsProcesses |424
ForEach-Object { $_.Id }425
)426
Register-ScheduledTask `427
-TaskPath $taskPath `428
-TaskName $taskName `429
-Action $taskAction `430
-Trigger $trigger `431
-Principal $principal `432
-Settings $settings `433
-Description 'Starts the SBMS tray with the privileges required by its protected session gate.' |434
Out-Null435
Start-ScheduledTask -TaskPath $taskPath -TaskName $taskName436
$deadline = [DateTime]::UtcNow.AddSeconds(10)437
while ([DateTime]::UtcNow -lt $deadline) {438
$started = @(439
Get-InstalledSbmsProcesses |440
Where-Object { $_.Id -notin $existingProcessIds }441
)442
if ($started.Count -gt 0) {443
return444
}445
Start-Sleep -Milliseconds 200446
}447
throw 'The SBMS tray did not start from its registered logon task.'448
}449
450
function Find-SbmsDriverPackages {451
$infRoot = Join-Path $env:SystemRoot 'INF'452
foreach ($file in Get-ChildItem -LiteralPath $infRoot -Filter 'oem*.inf' -File) {453
$text = [IO.File]::ReadAllText($file.FullName)454
if ($text -match '(?i)SBMS\\IndirectDisplay' -and455
$text -match '(?i)Provider\s*=\s*"SBMS"' -and456
$text -match '(?i)4D36E968-E325-11CE-BFC1-08002BE10318') {457
$file458
}459
}460
}461
462
function Find-ObsoleteSbmsDriverPackages {463
$sourceText = [IO.File]::ReadAllText($driverInf)464
$sourceMatch = [regex]::Match(465
$sourceText,466
'(?im)^\s*DriverVer\s*=\s*(?<value>.+?)\s*$'467
)468
if (-not $sourceMatch.Success) {469
throw "DriverVer is missing from $driverInf"470
}471
$currentVersion = $sourceMatch.Groups['value'].Value.Trim()472
473
foreach ($package in Find-SbmsDriverPackages) {474
$packageText = [IO.File]::ReadAllText($package.FullName)475
$packageMatch = [regex]::Match(476
$packageText,477
'(?im)^\s*DriverVer\s*=\s*(?<value>.+?)\s*$'478
)479
if (-not $packageMatch.Success -or480
$packageMatch.Groups['value'].Value.Trim() -ne $currentVersion) {481
$package482
}483
}484
}485
486
function Install-SbmsDriver {487
if (-not (Test-Path -LiteralPath $driverInf)) {488
throw "Driver package is missing: $driverInf"489
}490
$nativeArgs = @('/add-driver', $driverInf, '/install')491
& pnputil.exe @nativeArgs492
if ($LASTEXITCODE -ne 0) {493
throw "pnputil add-driver failed with exit code $LASTEXITCODE"494
}495
}496
497
function Remove-DriverPackages {498
param([IO.FileInfo[]]$Packages)499
500
foreach ($package in $Packages) {501
$nativeArgs = @('/delete-driver', $package.Name, '/uninstall')502
& pnputil.exe @nativeArgs503
if ($LASTEXITCODE -ne 0) {504
throw "pnputil delete-driver $($package.Name) failed with exit code $LASTEXITCODE"505
}506
}507
}508
509
function Remove-SbmsDriver {510
$device = Get-PnpDevice -InstanceId $deviceInstance `511
-ErrorAction SilentlyContinue512
if ($device) {513
& pnputil.exe /remove-device $deviceInstance514
if ($LASTEXITCODE -ne 0) {515
throw "pnputil remove-device failed with exit code $LASTEXITCODE"516
}517
}518
Remove-DriverPackages -Packages @(Find-SbmsDriverPackages)519
}520
521
function Test-UninstallPreflight {522
Assert-InstallIdentity | Out-Null523
Get-InstalledSbmsProcesses | Out-Null524
Get-OwnedTask | Out-Null525
Get-LegacyOwnedTask | Out-Null526
Find-SbmsDriverPackages | Out-Null527
}528
529
function Install-Sbms {530
Assert-InstallIdentity | Out-Null531
$taskSnapshots = @(Get-OwnedTaskSnapshots)532
Remove-SbmsTask533
$before = @(534
Find-SbmsDriverPackages |535
ForEach-Object { $_.Name }536
)537
try {538
Install-SbmsDriver539
Install-SbmsTask540
}541
catch {542
$failure = $_543
Remove-SbmsTask544
$added = @(545
@(Find-SbmsDriverPackages) |546
Where-Object { $_.Name -notin $before }547
)548
if ($added.Count -gt 0) {549
Remove-DriverPackages -Packages $added550
}551
if ($taskSnapshots.Count -gt 0) {552
try {553
Restore-SbmsTasks -Snapshots $taskSnapshots554
}555
catch {556
throw (557
"$failure Startup-task compensation also failed: " +558
$_.Exception.Message559
)560
}561
}562
throw $failure563
}564
565
# The new package and startup task are working. Old SBMS packages are no566
# longer useful for rollback and otherwise accumulate in DriverStore.567
Remove-DriverPackages -Packages @(Find-ObsoleteSbmsDriverPackages)568
}569
570
function Uninstall-Sbms {571
Test-UninstallPreflight572
Stop-Sbms573
Stop-ManagedSunshineInstances574
$taskSnapshots = @(Get-OwnedTaskSnapshots)575
Remove-SbmsTask576
try {577
Remove-SbmsDriver578
}579
catch {580
$failure = $_581
try {582
Install-SbmsDriver583
if ($taskSnapshots.Count -gt 0) {584
Restore-SbmsTasks -Snapshots $taskSnapshots585
}586
}587
catch {588
throw (589
"$failure Compensation also failed: $($_.Exception.Message)"590
)591
}592
throw "$failure External state was restored; application files were retained."593
}594
}595
596
switch ($Action) {597
'PrepareUpgrade' {598
Prepare-SbmsUpgrade599
}600
'Stop' {601
Stop-Sbms602
}603
'PreflightUninstall' {604
Test-UninstallPreflight605
}606
'Install' {607
Install-Sbms608
}609
'Uninstall' {610
Uninstall-Sbms611
}612
}613