[CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('127.0.0.1', '::1')] [string] $PlayoutHost, [Parameter(Mandatory)] [ValidateRange(1, 65535)] [int] $PlayoutPort, [Parameter(Mandatory)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string] $NativeSha256, [Parameter(Mandatory)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string] $InteropSha256, [ValidateRange(0, 2147483647)] [Nullable[int]] $OutputChannel = $null, [switch] $Force, [string] $ConfigurationDirectory = ( Join-Path $env:LOCALAPPDATA 'MBN_STOCK_WEBVIEW\Config') ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $requiredAuthorization = 'I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH' $sceneAllowlist = @( '5001', '5006', '5011', '5016', '50160', '5023', '5024', '5025', '5026', '5029', '5032', '5037', '5068', '5070', '5072', '5074', '5076', '5077', '5078', '5079', '5080', '5081', '5082', '5083', '5084', '5085', '5086', '50860', '5087', '5088', '6001', '6067', '8001', '8002', '8003', '8018', '8032', '8035', '8040', '8046', '8051', '8056', '8061', '8067', 'N5001' ) function Assert-LocalAppDataPath { param([Parameter(Mandatory)][string] $Path) $localRoot = [IO.Path]::GetFullPath( [Environment]::GetFolderPath( [Environment+SpecialFolder]::LocalApplicationData)) $fullPath = [IO.Path]::GetFullPath($Path) $rootPrefix = $localRoot.TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar if (-not $fullPath.StartsWith( $rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'Development Live configuration must stay under LocalAppData.' } return $fullPath } function Assert-NoReparsePointChain { param( [Parameter(Mandatory)][string] $Directory, [Parameter(Mandatory)][string] $StopDirectory ) $current = [IO.Path]::GetFullPath($Directory) $stop = [IO.Path]::GetFullPath($StopDirectory) while (-not [string]::Equals( $current, $stop, [StringComparison]::OrdinalIgnoreCase)) { $item = Get-Item -LiteralPath $current -Force if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Development Live configuration directories cannot be reparse points.' } $parent = [IO.Path]::GetDirectoryName($current) if ([string]::IsNullOrWhiteSpace($parent) -or [string]::Equals( $parent, $current, [StringComparison]::OrdinalIgnoreCase)) { throw 'The LocalAppData configuration directory chain is invalid.' } $current = $parent } } function Assert-WritableTarget { param([Parameter(Mandatory)][string] $Path) if (-not (Test-Path -LiteralPath $Path)) { return } $item = Get-Item -LiteralPath $Path -Force if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Development Live configuration targets must be ordinary files.' } if (-not $Force) { throw "Configuration already exists. Review it and rerun with -Force: $Path" } } function Set-CurrentUserOnlyAcl { param([Parameter(Mandatory)][string] $Path) $identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name $acl = [Security.AccessControl.FileSecurity]::new() $acl.SetAccessRuleProtection($true, $false) $rule = [Security.AccessControl.FileSystemAccessRule]::new( $identity, [Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow) $acl.AddAccessRule($rule) [IO.File]::SetAccessControl($Path, $acl) } function Invalidate-ExistingAuthorization { param([Parameter(Mandatory)][string] $Path) if (-not [IO.File]::Exists($Path)) { return } $invalidatedPath = ( $Path + '.invalidated.' + [Guid]::NewGuid().ToString('N')) [IO.File]::Move($Path, $invalidatedPath) try { Set-CurrentUserOnlyAcl $invalidatedPath [IO.File]::Delete($invalidatedPath) } catch { throw ( 'The previous Development Live authorization was invalidated but ' + "could not be removed. Remove this protected file before retrying: $invalidatedPath") } if ([IO.File]::Exists($Path)) { throw 'The previous Development Live authorization could not be invalidated.' } } function Write-ProtectedJson { param( [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $Json ) $temporaryPath = "$Path.tmp.$([Guid]::NewGuid().ToString('N'))" try { [IO.File]::WriteAllText( $temporaryPath, $Json, [Text.UTF8Encoding]::new($false)) Set-CurrentUserOnlyAcl $temporaryPath Move-Item -LiteralPath $temporaryPath -Destination $Path -Force } finally { if ([IO.File]::Exists($temporaryPath)) { Remove-Item -LiteralPath $temporaryPath -Force } } } $configurationRoot = Assert-LocalAppDataPath $ConfigurationDirectory [IO.Directory]::CreateDirectory($configurationRoot) | Out-Null $localApplicationData = [IO.Path]::GetFullPath( [Environment]::GetFolderPath( [Environment+SpecialFolder]::LocalApplicationData)) Assert-NoReparsePointChain $configurationRoot $localApplicationData $playoutPath = Join-Path $configurationRoot 'playout.local.json' $authorizationPath = Join-Path $configurationRoot 'playout.development-live.local.json' Assert-WritableTarget $playoutPath Assert-WritableTarget $authorizationPath $playout = [ordered]@{ mode = 'DryRun' host = $PlayoutHost port = $PlayoutPort tcpMode = 1 clientPort = 0 sceneDirectory = $null outputChannel = $OutputChannel layoutIndex = 10 legacySceneFadeDuration = 6 legacySceneBackgroundKind = 'None' legacySceneBackgroundAssetPath = $null legacySceneBackgroundVideoLoopCount = 2004 legacySceneBackgroundVideoLoopInfinite = $true legacyBackgroundDirectory = $null testProcessWindowTitlePattern = $null testSceneAllowlist = $sceneAllowlist trustedLiveOutputEnabled = $true queueCapacity = 64 connectTimeoutMilliseconds = 5000 operationTimeoutMilliseconds = 5000 disconnectTimeoutMilliseconds = 3000 processPollIntervalMilliseconds = 1000 reconnectDelayMilliseconds = 1000 maximumReconnectAttempts = 0 reconnectEnabled = $false maximumAutomaticRefreshesPerTakeIn = 0 } $authorization = [ordered]@{ schemaVersion = 1 mode = 'Live' authorization = $requiredAuthorization nativeSha256 = $NativeSha256.ToUpperInvariant() interopSha256 = $InteropSha256.ToUpperInvariant() } $playoutJson = $playout | ConvertTo-Json -Depth 6 $authorizationJson = $authorization | ConvertTo-Json -Depth 3 # In Force mode, invalidate the old authorization before changing the DryRun # base. Any later failure therefore leaves no valid authorization at the exact # path consumed by the Debug-only bootstrap. Invalidate-ExistingAuthorization -Path $authorizationPath # Write the safe DryRun base first and the new one-launch authorization last. # Merely creating these files does not connect: Debug, the exact # --development-live argument, vendor hash verification and runtime gates remain # mandatory in the application. Write-ProtectedJson $playoutPath $playoutJson Write-ProtectedJson $authorizationPath $authorizationJson Write-Host "Development Live base configuration created at: $playoutPath" Write-Host "Development Live launch authorization created at: $authorizationPath" Write-Warning ( 'The base file remains DryRun. Use only Debug|x64 with the exact ' + 'Development Live (Package) profile after confirming the local development PGM target.')