param( [string] $RavenHost = "raven.Home", [string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe", [string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc", [string] $ActivateStorePath = "C:\Meta\Odin\state\muninn.activate.cc", [string] $LogRoot = "C:\Meta\Odin\logs\muninn", [string] $LocalLoopbackScript = (Join-Path $PSScriptRoot "wasapi-loopback-capture.ps1"), [string] $LoopbackScript = "C:\Meta\Odin\Muninn\scripts\wasapi-loopback-capture.ps1", [string] $Ffmpeg = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe", [string] $MediaTargetUri = $env:MUNINN_MEDIA_TARGET_URI, [string] $AudioDevice = "Realtek", [string[]] $VideoSources = @(), [string[]] $AudioSources = @(), [string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH, [string] $IdunnDaemon = "muninn", [string] $IdunnHealthContract = "muninn.cultnet-rudp-remote-telemetry-health", [string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }), [string] $OdinCultMeshRudpEndpoint = $env:CULTMESH_URI_ODIN_RUDP, [string[]] $MoveStates = @("xbox-raven=xinput://0"), [string] $HidControllerRudpTarget = "", [string] $HidControllerRudpBind = "0.0.0.0:17887", [string] $HidControllerRudpAdvertise = $env:MUNINN_HID_CONTROLLER_RUDP_ADVERTISE, [int] $ConnectTimeoutSeconds = 10, [int] $ServeStartTimeoutSeconds = 20, [string] $SshUser = "madman's lullaby", [string] $IdentityFile = "C:\Users\Meta\.ssh\id_ed25519_192_168_1_84" ) $ErrorActionPreference = "Stop" if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") { throw "restart-muninn.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path." } if (-not (Test-Path -LiteralPath $LocalLoopbackScript)) { throw "Local Muninn loopback script not found at $LocalLoopbackScript" } if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) { throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed." } if ([string]::IsNullOrWhiteSpace($MediaTargetUri) -or -not $MediaTargetUri.StartsWith("cultmesh://", [System.StringComparison]::OrdinalIgnoreCase)) { throw "Muninn media target URI must be supplied by -MediaTargetUri or MUNINN_MEDIA_TARGET_URI and must start with cultmesh://." } if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri) -and $OdinCultMeshUri.StartsWith("cultmesh://", [System.StringComparison]::OrdinalIgnoreCase) -and [string]::IsNullOrWhiteSpace($OdinCultMeshRudpEndpoint)) { throw "Odin CultMesh RUDP endpoint must be supplied by -OdinCultMeshRudpEndpoint or CULTMESH_URI_ODIN_RUDP when -OdinCultMeshUri is a cultmesh:// URI." } if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) { throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Raven LAN default is allowed." } function Set-AsciiFile { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Content ) [System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`r`n"), [System.Text.Encoding]::ASCII) } function ConvertTo-PowerShellStringLiteral { param( [Parameter(Mandatory = $true)] [string] $Value ) return "'" + $Value.Replace("'", "''") + "'" } function ConvertTo-PowerShellArrayLiteral { param( [Parameter(Mandatory = $true)] [string[]] $Values ) $lines = $Values | ForEach-Object { " {0}" -f (ConvertTo-PowerShellStringLiteral $_) } return "@(`r`n{0}`r`n)" -f ($lines -join ",`r`n") } function ConvertTo-PowerShellEnvAssignmentLines { param( [Parameter(Mandatory = $true)] [hashtable] $Environment ) $lines = @() foreach ($key in ($Environment.Keys | Sort-Object)) { $name = [string] $key if ($name -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { throw "Invalid environment variable name for Muninn launcher: $name" } $value = [string] $Environment[$key] $lines += '$env:{0} = {1}' -f $name, (ConvertTo-PowerShellStringLiteral $value) } return ($lines -join "`r`n") } function Get-SshCommonArgs { $args = @( "-o", "BatchMode=yes", "-o", "ConnectTimeout=$ConnectTimeoutSeconds", "-o", "ConnectionAttempts=1" ) if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) { $args += @("-i", $IdentityFile) } return $args } function Get-SshTarget { param([Parameter(Mandatory = $true)] [string] $Target) if ([string]::IsNullOrWhiteSpace($SshUser)) { return $Target } return "${SshUser}@${Target}" } function Test-LikelyVirtualDisplayToken { param([Parameter(Mandatory = $true)] [string] $Token) $trimmed = $Token.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { return $true } return ( $trimmed.StartsWith('MSBDD_', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('MSNIL', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('NOEDID', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('UGD', [System.StringComparison]::OrdinalIgnoreCase) ) } function Get-GraphicsDriverDisplayTokens { $connectivityRoot = 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity' if (-not (Test-Path -LiteralPath $connectivityRoot)) { return @() } $bestTokens = @() $bestScore = [int]::MinValue foreach ($key in Get-ChildItem -LiteralPath $connectivityRoot -ErrorAction SilentlyContinue) { $item = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue if ($null -eq $item) { continue } foreach ($property in $item.PSObject.Properties) { if ($property.Name -like 'PS*' -or $property.Value -isnot [string]) { continue } $tokens = @( $property.Value.Split('+') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } ) if ($tokens.Count -lt 1) { continue } $realCount = @($tokens | Where-Object { -not (Test-LikelyVirtualDisplayToken $_) }).Count $score = ($realCount * 100) + $tokens.Count if ($score -gt $bestScore) { $bestScore = $score $bestTokens = $tokens } } } return @($bestTokens | Select-Object -Unique) } function Get-DefaultMuninnVideoSourceLabels { param([Parameter(Mandatory = $true)] [string] $HostLabel) $displayNames = @( Get-CimInstance Win32_DesktopMonitor -ErrorAction SilentlyContinue | Where-Object { [string]::IsNullOrWhiteSpace($_.Status) -or $_.Status -eq 'OK' } | ForEach-Object { if ([string]::IsNullOrWhiteSpace($_.Name)) { 'Display' } else { $_.Name.Trim() } } ) $useRegistryFallback = ( $displayNames.Count -lt 1 -or ($displayNames.Count -eq 1 -and ( $displayNames[0] -match '^\s*WinDisc\s*$' -or $displayNames[0] -match '^\s*Generic PnP Monitor\s*$' -or $displayNames[0] -match '^\s*Display\s*$' )) ) if ($useRegistryFallback) { $displayTokens = @(Get-GraphicsDriverDisplayTokens) if ($displayTokens.Count -gt 0) { return @(for ($index = 0; $index -lt $displayTokens.Count; $index++) { '{0} display {1}' -f $HostLabel, ($index + 1) }) } } if ($displayNames.Count -lt 1) { $displayNames = @('Display') } return @(for ($index = 0; $index -lt $displayNames.Count; $index++) { $monitorName = $displayNames[$index] if ($monitorName -eq 'Display') { '{0} display {1}' -f $HostLabel, ($index + 1) } else { '{0} display {1} ({2})' -f $HostLabel, ($index + 1), $monitorName } }) } function New-HiddenNativeLauncherPowerShellContent { param( [Parameter(Mandatory = $false)] [string] $StorePath, [Parameter(Mandatory = $true)] [string] $LogRoot, [Parameter(Mandatory = $true)] [string[]] $Arguments, [Parameter(Mandatory = $true)] [string] $FilePath, [Parameter(Mandatory = $true)] [string] $StdoutPath, [Parameter(Mandatory = $true)] [string] $StderrPath, [Parameter(Mandatory = $true)] [string] $PidPath, [Parameter(Mandatory = $false)] [string] $WorkingDirectory, [Parameter(Mandatory = $false)] [hashtable] $Environment = @{}, [Parameter(Mandatory = $false)] [string] $DynamicArgumentsScript, [Parameter(Mandatory = $false)] [switch] $WaitForExit ) $template = @' $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" __STORE_DIR_LINE__ New-Item -ItemType Directory -Force -Path __LOG_ROOT__ | Out-Null $launcherLog = __PID_PATH__ + '.launcher.log' ('launcher-start ' + [DateTime]::UtcNow.ToString('o')) | Set-Content -Encoding ASCII -LiteralPath $launcherLog __ENVIRONMENT_LINES__ $arguments = __ARGUMENTS__ __DYNAMIC_ARGUMENTS_SCRIPT__ function Quote-NativeArgument([string] $Value) { if ($Value -match '[\s"]') { return '"' + $Value.Replace('"', '\"') + '"' } return $Value } $argumentLine = ($arguments | ForEach-Object { Quote-NativeArgument $_ }) -join ' ' ('launcher-args ' + $argumentLine) | Add-Content -Encoding ASCII -LiteralPath $launcherLog if (__RUN_INLINE__) { $PID | Set-Content -Encoding ASCII -LiteralPath __PID_PATH__ ('launcher-pid ' + $PID + ' powershell-inline') | Add-Content -Encoding ASCII -LiteralPath $launcherLog $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { & __FILE_PATH__ @arguments > __STDOUT_PATH__ 2> __STDERR_PATH__ $exitCode = $LASTEXITCODE } catch { ('launcher-error ' + $_.Exception.Message) | Add-Content -Encoding ASCII -LiteralPath $launcherLog throw } finally { $ErrorActionPreference = $previousErrorActionPreference } ('launcher-exit ' + $exitCode) | Add-Content -Encoding ASCII -LiteralPath $launcherLog exit $exitCode } try { $process = Start-Process -FilePath __FILE_PATH__ -ArgumentList $argumentLine __WORKING_DIRECTORY_CLAUSE__-WindowStyle Hidden -PassThru -RedirectStandardOutput __STDOUT_PATH__ -RedirectStandardError __STDERR_PATH__ $process.Id | Set-Content -Encoding ASCII -LiteralPath __PID_PATH__ ('launcher-pid ' + $process.Id) | Add-Content -Encoding ASCII -LiteralPath $launcherLog } catch { ('launcher-error ' + $_.Exception.Message) | Add-Content -Encoding ASCII -LiteralPath $launcherLog throw } __WAIT_FOR_EXIT_LINE__ '@ $storeDirLine = if ([string]::IsNullOrWhiteSpace($StorePath)) { "" } else { "New-Item -ItemType Directory -Force -Path (Split-Path -Parent {0}) | Out-Null" -f (ConvertTo-PowerShellStringLiteral $StorePath) } $workingDirectoryClause = if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) { "" } else { "-WorkingDirectory {0} " -f (ConvertTo-PowerShellStringLiteral $WorkingDirectory) } return $template. Replace("__STORE_DIR_LINE__", $storeDirLine). Replace("__LOG_ROOT__", (ConvertTo-PowerShellStringLiteral $LogRoot)). Replace("__ENVIRONMENT_LINES__", (ConvertTo-PowerShellEnvAssignmentLines $Environment)). Replace("__ARGUMENTS__", (ConvertTo-PowerShellArrayLiteral $Arguments)). Replace("__DYNAMIC_ARGUMENTS_SCRIPT__", $DynamicArgumentsScript). Replace("__FILE_PATH__", (ConvertTo-PowerShellStringLiteral $FilePath)). Replace("__WORKING_DIRECTORY_CLAUSE__", $workingDirectoryClause). Replace("__STDOUT_PATH__", (ConvertTo-PowerShellStringLiteral $StdoutPath)). Replace("__STDERR_PATH__", (ConvertTo-PowerShellStringLiteral $StderrPath)). Replace("__PID_PATH__", (ConvertTo-PowerShellStringLiteral $PidPath)). Replace("__RUN_INLINE__", $(if ($WaitForExit) { '$true' } else { '$false' })). Replace("__WAIT_FOR_EXIT_LINE__", $(if ($WaitForExit) { '$process.WaitForExit(); exit $process.ExitCode' } else { "" })) } function New-HiddenPowerShellVbsLauncherContent { param( [Parameter(Mandatory = $true)] [string] $PsPath, [Parameter(Mandatory = $false)] [switch] $WaitForExit ) return (@' Set fso = CreateObject("Scripting.FileSystemObject") scriptDir = fso.GetParentFolderName(WScript.ScriptFullName) psLauncher = "{0}" logPath = psLauncher & ".vbs.log" Set logFile = fso.OpenTextFile(logPath, 8, True) logFile.WriteLine "vbs-start " & Now logFile.Close Set shell = CreateObject("WScript.Shell") shell.CurrentDirectory = scriptDir exitCode = shell.Run("powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & psLauncher & """", 0, {1}) Set logFile = fso.OpenTextFile(logPath, 8, True) logFile.WriteLine "vbs-exit " & exitCode & " " & Now logFile.Close '@) -f $PsPath, ($(if ($WaitForExit) { "True" } else { "False" })) } function New-WscriptCmdLauncherContent { param( [Parameter(Mandatory = $true)] [string] $WorkingDirectory, [Parameter(Mandatory = $true)] [string] $VbsPath ) return (@' @echo off cd /d "{0}" wscript.exe //B //Nologo "{1}" '@) -f $WorkingDirectory, $VbsPath } function Invoke-RavenUploadedPowerShell { param( [Parameter(Mandatory = $true)] [string] $RavenHost, [Parameter(Mandatory = $true)] [string] $RemoteScriptContent, [Parameter(Mandatory = $true)] [object[]] $UploadSpecs, [Parameter(Mandatory = $true)] [string] $TempPrefix ) $uploadId = [guid]::NewGuid().ToString("N") $localTempRoot = Join-Path $env:TEMP "$TempPrefix-$uploadId" $localRemoteScript = Join-Path $localTempRoot "$TempPrefix-$uploadId.ps1" $localSftpBatch = Join-Path $localTempRoot "$TempPrefix-$uploadId.sftp" $remoteSftpPath = "C:/Windows/Temp/$TempPrefix-$uploadId.ps1" $remotePsPath = "C:\Windows\Temp\$TempPrefix-$uploadId.ps1" try { New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null Set-AsciiFile -Path $localRemoteScript -Content $RemoteScriptContent $batchLines = @() foreach ($spec in $UploadSpecs) { $batchLines += 'put "{0}" "{1}"' -f $spec.LocalPath, $spec.RemotePath } $batchLines += 'put "{0}" "{1}"' -f $localRemoteScript, $remoteSftpPath Set-AsciiFile -Path $localSftpBatch -Content ($batchLines -join "`r`n") $commonArgs = Get-SshCommonArgs $sshTarget = Get-SshTarget -Target $RavenHost & sftp.exe @commonArgs -b $localSftpBatch $sshTarget if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $remoteRunner = @" `$ErrorActionPreference = "Stop" `$ProgressPreference = "SilentlyContinue" try { & "$remotePsPath" exit 0 } finally { Remove-Item -LiteralPath "$remotePsPath" -Force -ErrorAction SilentlyContinue } "@ $encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner)) $commonArgs = Get-SshCommonArgs $sshTarget = Get-SshTarget -Target $RavenHost & ssh.exe @commonArgs $sshTarget "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand $encodedRunner" } finally { Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue } } $muninnDir = Split-Path -Parent $MuninnExe $serveCmdPath = Join-Path $muninnDir "start-muninn-serve.cmd" $servePsPath = Join-Path $muninnDir "start-muninn-serve.ps1" $serveVbsPath = Join-Path $muninnDir "start-muninn-serve-hidden.vbs" $serveArguments = @( "serve", "--store", $StorePath, "--activate-store", $ActivateStorePath, "--log-root", $LogRoot, "--host", "raven", "--stream", "muninn.raven.av.rudp", "--target-host", $MediaTargetUri, "--media-transport", "rudp" ) if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri)) { $serveArguments += @("--odin-cultmesh-uri", $OdinCultMeshUri) } if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpTarget)) { $serveArguments += @("--hid-controller-rudp-target", $HidControllerRudpTarget) } if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind)) { $serveArguments += @("--hid-controller-rudp-bind", $HidControllerRudpBind) } if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) { $serveArguments += @("--hid-controller-rudp-advertise", $HidControllerRudpAdvertise) } foreach ($videoSource in $VideoSources) { $serveArguments += @("--video-source", $videoSource) } foreach ($audioSource in $AudioSources) { $serveArguments += @("--audio-source", $audioSource) } foreach ($moveState in $MoveStates) { if (-not [string]::IsNullOrWhiteSpace($moveState)) { $serveArguments += @("--move-state", $moveState) } } $serveArguments += @( "--audio-device", $AudioDevice, "--ffmpeg", $Ffmpeg, "--loopback-script", $LoopbackScript, "--interval-seconds", "15", "--idunn-rudp-health", $IdunnRudpHealth, "--idunn-daemon", $IdunnDaemon, "--idunn-health-contract", $IdunnHealthContract ) $serveDynamicArgumentsScript = @' function Test-LikelyVirtualDisplayToken { param([Parameter(Mandatory = $true)] [string] $Token) $trimmed = $Token.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { return $true } return ( $trimmed.StartsWith('MSBDD_', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('MSNIL', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('NOEDID', [System.StringComparison]::OrdinalIgnoreCase) -or $trimmed.StartsWith('UGD', [System.StringComparison]::OrdinalIgnoreCase) ) } function Get-GraphicsDriverDisplayTokens { $connectivityRoot = 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity' if (-not (Test-Path -LiteralPath $connectivityRoot)) { return @() } $bestTokens = @() $bestScore = [int]::MinValue foreach ($key in Get-ChildItem -LiteralPath $connectivityRoot -ErrorAction SilentlyContinue) { $item = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue if ($null -eq $item) { continue } foreach ($property in $item.PSObject.Properties) { if ($property.Name -like 'PS*' -or $property.Value -isnot [string]) { continue } $tokens = @( $property.Value.Split('+') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } ) if ($tokens.Count -lt 1) { continue } $realCount = @($tokens | Where-Object { -not (Test-LikelyVirtualDisplayToken $_) }).Count $score = ($realCount * 100) + $tokens.Count if ($score -gt $bestScore) { $bestScore = $score $bestTokens = $tokens } } } return @($bestTokens | Select-Object -Unique) } function Get-DefaultMuninnVideoSourceLabels { param([Parameter(Mandatory = $true)] [string] $HostLabel) $displayNames = @( Get-CimInstance Win32_DesktopMonitor -ErrorAction SilentlyContinue | Where-Object { [string]::IsNullOrWhiteSpace($_.Status) -or $_.Status -eq 'OK' } | ForEach-Object { if ([string]::IsNullOrWhiteSpace($_.Name)) { 'Display' } else { $_.Name.Trim() } } ) $useRegistryFallback = ( $displayNames.Count -lt 1 -or ($displayNames.Count -eq 1 -and ( $displayNames[0] -match '^\s*WinDisc\s*$' -or $displayNames[0] -match '^\s*Generic PnP Monitor\s*$' -or $displayNames[0] -match '^\s*Display\s*$' )) ) if ($useRegistryFallback) { $displayTokens = @(Get-GraphicsDriverDisplayTokens) if ($displayTokens.Count -gt 0) { return @(for ($index = 0; $index -lt $displayTokens.Count; $index++) { '{0} display {1}' -f $HostLabel, ($index + 1) }) } } if ($displayNames.Count -lt 1) { $displayNames = @('Display') } return @(for ($index = 0; $index -lt $displayNames.Count; $index++) { $monitorName = $displayNames[$index] if ($monitorName -eq 'Display') { '{0} display {1}' -f $HostLabel, ($index + 1) } else { '{0} display {1} ({2})' -f $HostLabel, ($index + 1), $monitorName } }) } $hostArgumentIndex = [Array]::IndexOf($arguments, '--host') $hostLabel = if ($hostArgumentIndex -ge 0 -and ($hostArgumentIndex + 1) -lt $arguments.Count) { $arguments[$hostArgumentIndex + 1] } else { 'host' } $hasExplicitVideoSources = @($arguments | Where-Object { $_ -eq '--video-source' }).Count -gt 0 $hasExplicitAudioSources = @($arguments | Where-Object { $_ -eq '--audio-source' }).Count -gt 0 if (-not ('Muninn.AudioEndpointDiscovery' -as [type])) { Add-Type -TypeDefinition @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace Muninn { public static class AudioEndpointDiscovery { const int eRender = 0; const int eCapture = 1; const uint DEVICE_STATE_ACTIVE = 0x00000001; static readonly PROPERTYKEY DeviceFriendlyNameKey = new PROPERTYKEY( new Guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"), 14); [StructLayout(LayoutKind.Sequential)] struct PROPERTYKEY { public Guid fmtid; public uint pid; public PROPERTYKEY(Guid formatId, uint propertyId) { fmtid = formatId; pid = propertyId; } } [StructLayout(LayoutKind.Sequential)] struct PROPVARIANT { public ushort vt; public ushort wReserved1; public ushort wReserved2; public ushort wReserved3; public IntPtr pointerValue; public int intValue; } [ComImport] [Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumerator { } [ComImport] [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IMMDeviceEnumerator { [PreserveSig] int EnumAudioEndpoints(int dataFlow, uint stateMask, out IMMDeviceCollection devices); } [ComImport] [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IMMDeviceCollection { [PreserveSig] int GetCount(out uint count); [PreserveSig] int Item(uint index, out IMMDevice device); } [ComImport] [Guid("D666063F-1587-4E43-81F1-B948E807363F")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IMMDevice { [PreserveSig] int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, out object audioClient); [PreserveSig] int OpenPropertyStore(uint access, out IPropertyStore properties); } [ComImport] [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IPropertyStore { [PreserveSig] int GetCount(out uint propertyCount); [PreserveSig] int GetAt(uint propertyIndex, out PROPERTYKEY key); [PreserveSig] int GetValue(ref PROPERTYKEY key, out PROPVARIANT value); [PreserveSig] int SetValue(ref PROPERTYKEY key, ref PROPVARIANT value); [PreserveSig] int Commit(); } [DllImport("ole32.dll")] static extern int PropVariantClear(ref PROPVARIANT variant); public static string[] FriendlyNames(int dataFlow) { var names = new List(); IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator()); IMMDeviceCollection devices; Marshal.ThrowExceptionForHR(enumerator.EnumAudioEndpoints(dataFlow, DEVICE_STATE_ACTIVE, out devices)); try { uint count; Marshal.ThrowExceptionForHR(devices.GetCount(out count)); for (uint index = 0; index < count; index++) { IMMDevice device; Marshal.ThrowExceptionForHR(devices.Item(index, out device)); try { var name = DeviceFriendlyName(device); if (!String.IsNullOrWhiteSpace(name)) { names.Add(name.Trim()); } } finally { Marshal.ReleaseComObject(device); } } } finally { Marshal.ReleaseComObject(devices); Marshal.ReleaseComObject(enumerator); } return names.ToArray(); } static string DeviceFriendlyName(IMMDevice device) { IPropertyStore store; Marshal.ThrowExceptionForHR(device.OpenPropertyStore(0, out store)); try { PROPERTYKEY key = DeviceFriendlyNameKey; PROPVARIANT value; Marshal.ThrowExceptionForHR(store.GetValue(ref key, out value)); try { if (value.vt == 31 && value.pointerValue != IntPtr.Zero) { return Marshal.PtrToStringUni(value.pointerValue) ?? ""; } return ""; } finally { PropVariantClear(ref value); } } finally { Marshal.ReleaseComObject(store); } } } } "@ } if (-not $hasExplicitVideoSources) { $videoSourceLabels = @(Get-DefaultMuninnVideoSourceLabels -HostLabel $hostLabel) for ($index = 0; $index -lt $videoSourceLabels.Count; $index++) { $arguments += @('--video-source', ('display:{0}={1}' -f $index, $videoSourceLabels[$index])) } } if (-not $hasExplicitAudioSources) { $loopbackNames = @([Muninn.AudioEndpointDiscovery]::FriendlyNames(0) | Sort-Object -Unique) $inputNames = @([Muninn.AudioEndpointDiscovery]::FriendlyNames(1) | Sort-Object -Unique) if ($loopbackNames.Count -lt 1 -and $inputNames.Count -lt 1) { $audioDeviceIndex = [Array]::IndexOf($arguments, '--audio-device') if ($audioDeviceIndex -ge 0 -and ($audioDeviceIndex + 1) -lt $arguments.Count) { $loopbackNames = @($arguments[$audioDeviceIndex + 1]) } } foreach ($audioName in $loopbackNames) { $arguments += @('--audio-source', ('wasapi-loopback:{0}={1} loopback ({0})' -f $audioName.Trim(), $hostLabel)) } foreach ($audioName in $inputNames) { $arguments += @('--audio-source', ('wasapi-input:{0}={1} input ({0})' -f $audioName.Trim(), $hostLabel)) } } '@ $servePsContent = New-HiddenNativeLauncherPowerShellContent ` -StorePath $StorePath ` -LogRoot $LogRoot ` -Arguments $serveArguments ` -FilePath $MuninnExe ` -StdoutPath (Join-Path $LogRoot "muninn-serve.out.log") ` -StderrPath (Join-Path $LogRoot "muninn-serve.err.log") ` -PidPath (Join-Path $LogRoot "muninn-serve.pid") ` -Environment @{ CULTMESH_URI_ODIN_RUDP = $OdinCultMeshRudpEndpoint } ` -DynamicArgumentsScript $serveDynamicArgumentsScript $uploadRoot = Join-Path $env:TEMP ("odin-raven-muninn-launchers-" + [guid]::NewGuid().ToString("N")) try { New-Item -ItemType Directory -Force -Path $uploadRoot | Out-Null $localServePs = Join-Path $uploadRoot "start-muninn-serve.ps1" $localServeVbs = Join-Path $uploadRoot "start-muninn-serve-hidden.vbs" $localServeCmd = Join-Path $uploadRoot "start-muninn-serve.cmd" Set-AsciiFile -Path $localServePs -Content $servePsContent Set-AsciiFile -Path $localServeVbs -Content (New-HiddenPowerShellVbsLauncherContent -PsPath $servePsPath) Set-AsciiFile -Path $localServeCmd -Content (New-WscriptCmdLauncherContent -WorkingDirectory $muninnDir -VbsPath $serveVbsPath) $remoteScript = @" `$ErrorActionPreference = "Stop" `$ProgressPreference = "SilentlyContinue" if (-not (Test-Path -LiteralPath "$MuninnExe")) { throw "Muninn executable not found at $MuninnExe" } if (-not (Test-Path -LiteralPath "$LoopbackScript")) { throw "Muninn loopback script not found at $LoopbackScript" } if (-not (Test-Path -LiteralPath "$Ffmpeg")) { throw "FFmpeg executable not found at $Ffmpeg" } foreach (`$path in @( "$servePsPath", "$serveVbsPath" )) { if (-not (Test-Path -LiteralPath `$path)) { throw "Required launcher path not found at `$path" } } `$obsoleteActivateTask = Get-ScheduledTask -TaskName "GameCult-Muninn-Activate" -ErrorAction SilentlyContinue if (`$null -ne `$obsoleteActivateTask) { Unregister-ScheduledTask -TaskName "GameCult-Muninn-Activate" -Confirm:`$false | Out-Null } `$obsoleteVideoProofTask = Get-ScheduledTask -TaskName "GameCult-Muninn-VideoProof" -ErrorAction SilentlyContinue if (`$null -ne `$obsoleteVideoProofTask) { Unregister-ScheduledTask -TaskName "GameCult-Muninn-VideoProof" -Confirm:`$false | Out-Null } Get-CimInstance Win32_Process | Where-Object { `$_.Name -like "muninn*.exe" } | ForEach-Object { & cmd.exe /c ("taskkill /PID {0} /T /F >nul 2>&1" -f `$_.ProcessId) | Out-Null } New-Item -ItemType Directory -Force -Path "$LogRoot" | Out-Null New-Item -ItemType Directory -Force -Path (Split-Path -Parent "$StorePath") | Out-Null & netsh.exe advfirewall firewall delete rule name="GameCult Muninn Capture Command RUDP" | Out-Null 2>`$null if (-not [string]::IsNullOrWhiteSpace("$HidControllerRudpBind")) { `$hidControllerUdpPort = ([string] "$HidControllerRudpBind").Split(':')[-1] & netsh.exe advfirewall firewall delete rule name="GameCult Muninn HID Controller RUDP" | Out-Null 2>`$null & netsh.exe advfirewall firewall add rule name="GameCult Muninn HID Controller RUDP" dir=in action=allow protocol=UDP localport=`$hidControllerUdpPort | Out-Null } function Register-HiddenVbsTask { param( [Parameter(Mandatory = `$true)] [string] `$TaskName, [Parameter(Mandatory = `$true)] [string] `$VbsPath ) `$taskAction = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\wscript.exe" -Argument "//B //Nologo ""`$VbsPath""" -WorkingDirectory (Split-Path -Parent `$VbsPath) `$taskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Today.AddHours(23).AddMinutes(59)) `$taskPrincipal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest `$taskSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew Register-ScheduledTask -TaskName `$TaskName -Action `$taskAction -Trigger `$taskTrigger -Principal `$taskPrincipal -Settings `$taskSettings -Force | Out-Null } function Register-HiddenPowerShellTask { param( [Parameter(Mandatory = `$true)] [string] `$TaskName, [Parameter(Mandatory = `$true)] [string] `$PsPath ) `$taskArguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + `$PsPath + '"' `$taskAction = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -Argument `$taskArguments -WorkingDirectory (Split-Path -Parent `$PsPath) `$taskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Today.AddHours(23).AddMinutes(59)) `$taskPrincipal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest `$taskSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew Register-ScheduledTask -TaskName `$TaskName -Action `$taskAction -Trigger `$taskTrigger -Principal `$taskPrincipal -Settings `$taskSettings -Force | Out-Null } function Assert-HiddenVbsTask { param( [Parameter(Mandatory = `$true)] [string] `$TaskName, [Parameter(Mandatory = `$true)] [string] `$VbsPath, [Parameter(Mandatory = `$true)] [string] `$PsPath ) `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction Stop `$action = @(`$task.Actions)[0] if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') { throw "`$TaskName action executes `$(`$action.Execute), expected wscript.exe" } if (`$action.Arguments -notlike "*`$VbsPath*") { throw "`$TaskName action arguments `$(`$action.Arguments) do not reference `$VbsPath" } if (`$action.Arguments -notlike "*//B*" -or `$action.Arguments -notlike "*//Nologo*") { throw "`$TaskName action arguments `$(`$action.Arguments) do not force background WScript execution" } if (-not (Test-Path -LiteralPath `$PsPath)) { throw "`$TaskName PowerShell launcher not found at `$PsPath" } `$vbs = Get-Content -LiteralPath `$VbsPath -Raw if (`$vbs -match 'cmdPath\s*=') { throw "`$TaskName hidden launcher at `$VbsPath still routes through a cmdPath trampoline" } if (`$vbs -notmatch '\.ps1') { throw "`$TaskName hidden launcher at `$VbsPath does not reference a PowerShell launcher" } } function Assert-HiddenPowerShellTask { param( [Parameter(Mandatory = `$true)] [string] `$TaskName, [Parameter(Mandatory = `$true)] [string] `$PsPath ) `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction Stop `$action = @(`$task.Actions)[0] if (`$action.Execute -notmatch 'powershell\.exe$') { throw "`$TaskName action executes `$(`$action.Execute), expected powershell.exe" } if (`$action.Arguments -notlike "*`$PsPath*") { throw "`$TaskName action arguments `$(`$action.Arguments) do not reference `$PsPath" } if (`$action.Arguments -notlike "*-File*") { throw "`$TaskName action arguments `$(`$action.Arguments) do not execute a PowerShell launcher" } if (-not (Test-Path -LiteralPath `$PsPath)) { throw "`$TaskName PowerShell launcher not found at `$PsPath" } } Register-HiddenPowerShellTask -TaskName "GameCult-Muninn" -PsPath "$servePsPath" Assert-HiddenPowerShellTask -TaskName "GameCult-Muninn" -PsPath "$servePsPath" Start-ScheduledTask -TaskName "GameCult-Muninn" `$deadline = [DateTime]::UtcNow.AddSeconds($ServeStartTimeoutSeconds) `$process = `$null do { Start-Sleep -Milliseconds 500 `$process = Get-CimInstance Win32_Process | Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" -and `$_.CommandLine -like "*--host*raven*" } | Select-Object -First 1 } while (`$null -eq `$process -and [DateTime]::UtcNow -lt `$deadline) if (`$null -eq `$process) { throw "Muninn serve process did not start on Raven within $ServeStartTimeoutSeconds seconds" } foreach (`$pattern in @( "--idunn-rudp-health", "$IdunnRudpHealth", "--idunn-daemon", "$IdunnDaemon", "--idunn-health-contract", "$IdunnHealthContract" )) { if (`$process.CommandLine -notlike "*`$pattern*") { throw "Muninn Raven serve command line is missing `${pattern}: `$(`$process.CommandLine)" } } "@ $uploadSpecs = @( @{ LocalPath = $LocalLoopbackScript; RemotePath = ($LoopbackScript -replace "\\", "/") }, @{ LocalPath = $localServePs; RemotePath = ($servePsPath -replace "\\", "/") }, @{ LocalPath = $localServeVbs; RemotePath = ($serveVbsPath -replace "\\", "/") }, @{ LocalPath = $localServeCmd; RemotePath = ($serveCmdPath -replace "\\", "/") } ) Invoke-RavenUploadedPowerShell -RavenHost $RavenHost -RemoteScriptContent $remoteScript -UploadSpecs $uploadSpecs -TempPrefix "odin-raven-muninn-restart" } finally { Remove-Item -LiteralPath $uploadRoot -Recurse -Force -ErrorAction SilentlyContinue } exit $LASTEXITCODE