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 / installer
a37d6c4b· 固定源码快照
installer/maintenance.ps118.2 KB
1[CmdletBinding()]
2param(
3 [Parameter(Mandatory)]
4 [ValidateSet('Install', 'PrepareUpgrade', 'Stop', 'PreflightUninstall', 'Uninstall')]
5 [string]$Action,
6
7 [Parameter(Mandatory)]
8 [string]$InstallRoot
9)
10
11$ErrorActionPreference = 'Stop'
12Set-StrictMode -Version Latest
13
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 = 5
24$backupOwnershipMarker = '.sbms-upgrade-snapshot-v1'
25$backupOwnershipContent = 'SBMS upgrade snapshot v1'
26
27function 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
43function Get-InteractiveIdentity {
44 $sessionId = (Get-Process -Id $PID).SessionId
45 $explorer = Get-Process -Name 'explorer' -ErrorAction SilentlyContinue |
46 Where-Object { $_.SessionId -eq $sessionId } |
47 Select-Object -First 1
48 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 GetOwner
54 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.User
62 }
63 $account = [Security.Principal.NTAccount]::new($name)
64 $sid = $account.Translate([Security.Principal.SecurityIdentifier])
65 [pscustomobject]@{
66 Name = $name
67 Sid = $sid.Value
68 }
69}
70
71function Assert-InstallIdentity {
72 $interactive = Get-InteractiveIdentity
73 $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 $interactive
86}
87
88function 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.Path
98 }
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 = $false
107 foreach ($target in $targets) {
108 if ($target.Equals(
109 $fullPath,
110 [StringComparison]::OrdinalIgnoreCase)) {
111 $matchesInstalledPath = $true
112 break
113 }
114 }
115 if ($matchesInstalledPath) {
116 $found += $process
117 }
118 }
119 $found
120}
121
122function Stop-Sbms {
123 $running = @(Get-InstalledSbmsProcesses)
124 if ($running.Count -eq 0) {
125 return
126 }
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-Null
132 $shutdownExit = $LASTEXITCODE
133 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 return
144 }
145 Start-Sleep -Milliseconds 200
146 }
147 throw 'An installed SBMS process did not stop within 30 seconds.'
148}
149
150function Stop-ManagedSunshineInstances {
151 $manager = Join-Path $InstallRoot 'installer\manage-sunshine-instance.ps1'
152 if (-not (Test-Path -LiteralPath $manager -PathType Leaf)) {
153 return
154 }
155
156 $powershell = Join-Path $PSHOME 'powershell.exe'
157 $managerArgs = @(
158 '-NoLogo'
159 '-NoProfile'
160 '-NonInteractive'
161 '-ExecutionPolicy'
162 'Bypass'
163 '-File'
164 $manager
165 '-Action'
166 'StopAll'
167 )
168 $output = @(& $powershell @managerArgs 2>&1)
169 $exitCode = $LASTEXITCODE
170 $jsonLine = $output |
171 Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } |
172 Select-Object -Last 1
173 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-Json
179 } 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
187function Test-OwnedUpgradeSnapshot {
188 param(
189 [Parameter(Mandatory)]
190 [IO.DirectoryInfo]$Snapshot,
191
192 [Parameter(Mandatory)]
193 [string]$BackupRoot
194 )
195
196 if ($Snapshot.Name -notmatch '^\d{8}T\d{9}Z$' -or
197 ($Snapshot.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
198 return $false
199 }
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 -or
205 -not [string]::Equals(
206 [IO.Path]::GetFullPath($parent.FullName),
207 $rootPath,
208 [StringComparison]::OrdinalIgnoreCase) -or
209 -not [string]::Equals(
210 $snapshotPath,
211 [IO.Path]::GetFullPath((Join-Path $rootPath $Snapshot.Name)),
212 [StringComparison]::OrdinalIgnoreCase)) {
213 return $false
214 }
215
216 $markerPath = Join-Path $snapshotPath $backupOwnershipMarker
217 $marker = Get-Item -LiteralPath $markerPath -Force -ErrorAction SilentlyContinue
218 if (-not $marker -or
219 $marker.PSIsContainer -or
220 ($marker.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
221 return $false
222 }
223
224 try {
225 [IO.File]::ReadAllText($marker.FullName, [Text.Encoding]::UTF8) -ceq
226 $backupOwnershipContent
227 }
228 catch {
229 $false
230 }
231}
232
233function 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 return
247 }
248
249 $backupRoot = Join-Path $configRoot 'upgrade-backups'
250 $stamp = [DateTime]::UtcNow.ToString(
251 'yyyyMMddTHHmmssfffZ',
252 [Globalization.CultureInfo]::InvariantCulture)
253 $snapshot = Join-Path $backupRoot $stamp
254 New-Item -ItemType Directory -Path $snapshot -Force | Out-Null
255
256 foreach ($source in $sources) {
257 $name = Split-Path -Leaf $source
258 $destination = Join-Path $snapshot $name
259 Copy-Item -LiteralPath $source -Destination $destination
260 $sourceHash = Get-Sha256Hex -LiteralPath $source
261 $backupHash = Get-Sha256Hex -LiteralPath $destination
262 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 $backupRoot
276 } |
277 Sort-Object Name -Descending |
278 Select-Object -Skip $backupRetention
279 )
280 foreach ($staleSnapshot in $staleSnapshots) {
281 $current = Get-Item -LiteralPath $staleSnapshot.FullName -Force `
282 -ErrorAction SilentlyContinue
283 if (-not $current -or
284 -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 -Force
290 }
291}
292
293function Prepare-SbmsUpgrade {
294 Assert-InstallIdentity | Out-Null
295 Stop-Sbms
296 Backup-SbmsConfiguration
297}
298
299function Get-VerifiedTask {
300 param(
301 [Parameter(Mandatory)]
302 [string]$Path,
303
304 [Parameter(Mandatory)]
305 [string]$Name
306 )
307
308 $task = Get-ScheduledTask -TaskPath $Path -TaskName $Name `
309 -ErrorAction SilentlyContinue
310 if (-not $task) {
311 return $null
312 }
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) -or
318 $action.Arguments -ceq '--background'
319 if (-not [string]::Equals(
320 $action.Execute,
321 $tray,
322 [StringComparison]::OrdinalIgnoreCase) -or
323 -not $ownedArguments -or
324 -not [string]::Equals(
325 $action.WorkingDirectory,
326 $InstallRoot,
327 [StringComparison]::OrdinalIgnoreCase)) {
328 throw "Scheduled task collision at $Path$Name."
329 }
330 $task
331}
332
333function Get-OwnedTask {
334 Get-VerifiedTask -Path $taskPath -Name $taskName
335}
336
337function Get-LegacyOwnedTask {
338 Get-VerifiedTask -Path $legacyTaskPath -Name $legacyTaskName
339}
340
341function Get-OwnedTaskEntries {
342 @(
343 [pscustomobject]@{
344 Task = Get-OwnedTask
345 Path = $taskPath
346 Name = $taskName
347 },
348 [pscustomobject]@{
349 Task = Get-LegacyOwnedTask
350 Path = $legacyTaskPath
351 Name = $legacyTaskName
352 }
353 )
354}
355
356function Remove-SbmsTask {
357 foreach ($entry in (Get-OwnedTaskEntries)) {
358 if (-not $entry.Task) {
359 continue
360 }
361 Stop-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name `
362 -ErrorAction SilentlyContinue
363 Unregister-ScheduledTask -TaskPath $entry.Path -TaskName $entry.Name `
364 -Confirm:$false
365 }
366}
367
368function Get-OwnedTaskSnapshots {
369 foreach ($entry in (Get-OwnedTaskEntries)) {
370 if (-not $entry.Task) {
371 continue
372 }
373 [pscustomobject]@{
374 Path = $entry.Path
375 Name = $entry.Name
376 Xml = Export-ScheduledTask `
377 -TaskPath $entry.Path `
378 -TaskName $entry.Name
379 WasRunning = $entry.Task.State -eq 'Running'
380 }
381 }
382}
383
384function Restore-SbmsTasks {
385 param([object[]]$Snapshots)
386
387 Remove-SbmsTask
388 foreach ($snapshot in $Snapshots) {
389 Register-ScheduledTask `
390 -TaskPath $snapshot.Path `
391 -TaskName $snapshot.Name `
392 -Xml $snapshot.Xml |
393 Out-Null
394 if ($snapshot.WasRunning) {
395 Start-ScheduledTask `
396 -TaskPath $snapshot.Path `
397 -TaskName $snapshot.Name
398 }
399 }
400}
401
402function Install-SbmsTask {
403 $interactive = Assert-InstallIdentity
404 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 $InstallRoot
410 $trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactive.Sid
411 $principal = New-ScheduledTaskPrincipal `
412 -UserId $interactive.Sid `
413 -LogonType Interactive `
414 -RunLevel Highest
415 $settings = New-ScheduledTaskSettingsSet `
416 -AllowStartIfOnBatteries `
417 -DontStopIfGoingOnBatteries `
418 -ExecutionTimeLimit ([TimeSpan]::Zero) `
419 -MultipleInstances IgnoreNew
420 # 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-Null
435 Start-ScheduledTask -TaskPath $taskPath -TaskName $taskName
436 $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 return
444 }
445 Start-Sleep -Milliseconds 200
446 }
447 throw 'The SBMS tray did not start from its registered logon task.'
448}
449
450function 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' -and
455 $text -match '(?i)Provider\s*=\s*"SBMS"' -and
456 $text -match '(?i)4D36E968-E325-11CE-BFC1-08002BE10318') {
457 $file
458 }
459 }
460}
461
462function 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 -or
480 $packageMatch.Groups['value'].Value.Trim() -ne $currentVersion) {
481 $package
482 }
483 }
484}
485
486function 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 @nativeArgs
492 if ($LASTEXITCODE -ne 0) {
493 throw "pnputil add-driver failed with exit code $LASTEXITCODE"
494 }
495}
496
497function Remove-DriverPackages {
498 param([IO.FileInfo[]]$Packages)
499
500 foreach ($package in $Packages) {
501 $nativeArgs = @('/delete-driver', $package.Name, '/uninstall')
502 & pnputil.exe @nativeArgs
503 if ($LASTEXITCODE -ne 0) {
504 throw "pnputil delete-driver $($package.Name) failed with exit code $LASTEXITCODE"
505 }
506 }
507}
508
509function Remove-SbmsDriver {
510 $device = Get-PnpDevice -InstanceId $deviceInstance `
511 -ErrorAction SilentlyContinue
512 if ($device) {
513 & pnputil.exe /remove-device $deviceInstance
514 if ($LASTEXITCODE -ne 0) {
515 throw "pnputil remove-device failed with exit code $LASTEXITCODE"
516 }
517 }
518 Remove-DriverPackages -Packages @(Find-SbmsDriverPackages)
519}
520
521function Test-UninstallPreflight {
522 Assert-InstallIdentity | Out-Null
523 Get-InstalledSbmsProcesses | Out-Null
524 Get-OwnedTask | Out-Null
525 Get-LegacyOwnedTask | Out-Null
526 Find-SbmsDriverPackages | Out-Null
527}
528
529function Install-Sbms {
530 Assert-InstallIdentity | Out-Null
531 $taskSnapshots = @(Get-OwnedTaskSnapshots)
532 Remove-SbmsTask
533 $before = @(
534 Find-SbmsDriverPackages |
535 ForEach-Object { $_.Name }
536 )
537 try {
538 Install-SbmsDriver
539 Install-SbmsTask
540 }
541 catch {
542 $failure = $_
543 Remove-SbmsTask
544 $added = @(
545 @(Find-SbmsDriverPackages) |
546 Where-Object { $_.Name -notin $before }
547 )
548 if ($added.Count -gt 0) {
549 Remove-DriverPackages -Packages $added
550 }
551 if ($taskSnapshots.Count -gt 0) {
552 try {
553 Restore-SbmsTasks -Snapshots $taskSnapshots
554 }
555 catch {
556 throw (
557 "$failure Startup-task compensation also failed: " +
558 $_.Exception.Message
559 )
560 }
561 }
562 throw $failure
563 }
564
565 # The new package and startup task are working. Old SBMS packages are no
566 # longer useful for rollback and otherwise accumulate in DriverStore.
567 Remove-DriverPackages -Packages @(Find-ObsoleteSbmsDriverPackages)
568}
569
570function Uninstall-Sbms {
571 Test-UninstallPreflight
572 Stop-Sbms
573 Stop-ManagedSunshineInstances
574 $taskSnapshots = @(Get-OwnedTaskSnapshots)
575 Remove-SbmsTask
576 try {
577 Remove-SbmsDriver
578 }
579 catch {
580 $failure = $_
581 try {
582 Install-SbmsDriver
583 if ($taskSnapshots.Count -gt 0) {
584 Restore-SbmsTasks -Snapshots $taskSnapshots
585 }
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
596switch ($Action) {
597 'PrepareUpgrade' {
598 Prepare-SbmsUpgrade
599 }
600 'Stop' {
601 Stop-Sbms
602 }
603 'PreflightUninstall' {
604 Test-UninstallPreflight
605 }
606 'Install' {
607 Install-Sbms
608 }
609 'Uninstall' {
610 Uninstall-Sbms
611 }
612}
613