@echo off setlocal set "SDS_SELF=%~f0" powershell.exe -NoProfile -ExecutionPolicy Bypass -Sta -Command "$path=$env:SDS_SELF; $raw=Get-Content -LiteralPath $path -Raw; $marker='### POWERSHELL ###'; $index=$raw.LastIndexOf($marker); if($index -lt 0){throw 'PowerShell block not found.'}; $code=$raw.Substring($index + $marker.Length); Invoke-Expression $code" exit /b %ERRORLEVEL% ### POWERSHELL ### Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $SunshineDir = if ([string]::IsNullOrWhiteSpace($env:SDS_SUNSHINE_DIR)) { Join-Path $env:ProgramFiles 'Sunshine' } else { $env:SDS_SUNSHINE_DIR } $DeviceId = $env:SDS_DEVICE_ID $ListOnly = $env:SDS_LIST_ONLY -eq '1' $NoRestart = $env:SDS_NO_RESTART -eq '1' $Silent = $env:SDS_SILENT -eq '1' $Action = $env:SDS_ACTION $BatchPath = $env:SDS_SELF function Test-IsAdministrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-CanWriteFile { param([string]$Path) try { $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read) $stream.Close() return $true } catch { return $false } } function Get-JsonArrayAfterMarker { param( [string]$Text, [string]$Marker ) $markerIndex = $Text.LastIndexOf($Marker, [StringComparison]::OrdinalIgnoreCase) if ($markerIndex -lt 0) { return $null } $start = $Text.IndexOf('[', $markerIndex + $Marker.Length) if ($start -lt 0) { return $null } $depth = 0 $inString = $false $escaped = $false for ($i = $start; $i -lt $Text.Length; $i++) { $char = $Text[$i] if ($inString) { if ($escaped) { $escaped = $false continue } if ($char -eq '\') { $escaped = $true continue } if ($char -eq '"') { $inString = $false } continue } if ($char -eq '"') { $inString = $true continue } if ($char -eq '[') { $depth++ continue } if ($char -eq ']') { $depth-- if ($depth -eq 0) { return $Text.Substring($start, $i - $start + 1) } } } return $null } function Format-Rational { param($Rational) $rationalValue = Get-ObjectProperty -Object $Rational -Name 'value' if ($null -eq $rationalValue) { return '' } $denominator = [double](Get-ObjectProperty -Object $rationalValue -Name 'denominator') if ($denominator -eq 0) { return '' } $value = [double](Get-ObjectProperty -Object $rationalValue -Name 'numerator') / $denominator $formatted = ('{0:N2}' -f $value).TrimEnd('0').TrimEnd((Get-Culture).NumberFormat.NumberDecimalSeparator) return "$formatted Hz" } function Get-ObjectProperty { param( $Object, [string]$Name ) if ($null -eq $Object) { return $null } $property = $Object.PSObject.Properties[$Name] if ($null -eq $property) { return $null } return $property.Value } function ConvertTo-DisplayRows { param([object[]]$Devices) if ($Devices.Count -eq 1 -and $Devices[0] -is [Array]) { $Devices = $Devices[0] } $rows = @() foreach ($device in $Devices) { $resolution = '' $refreshRate = '' $primary = $false $active = $false $info = Get-ObjectProperty -Object $device -Name 'info' if ($null -ne $info) { $active = $true $resolutionInfo = Get-ObjectProperty -Object $info -Name 'resolution' if ($null -ne $resolutionInfo) { $resolution = '{0}x{1}' -f (Get-ObjectProperty -Object $resolutionInfo -Name 'width'), (Get-ObjectProperty -Object $resolutionInfo -Name 'height') } $refreshRate = Format-Rational -Rational (Get-ObjectProperty -Object $info -Name 'refresh_rate') $primaryInfo = Get-ObjectProperty -Object $info -Name 'primary' if ($null -ne $primaryInfo) { $primary = [bool]$primaryInfo } } $edid = '' $deviceEdid = Get-ObjectProperty -Object $device -Name 'edid' if ($null -ne $deviceEdid) { $edidParts = @() $manufacturerId = Get-ObjectProperty -Object $deviceEdid -Name 'manufacturer_id' $productCode = Get-ObjectProperty -Object $deviceEdid -Name 'product_code' $serialNumber = Get-ObjectProperty -Object $deviceEdid -Name 'serial_number' if ($manufacturerId) { $edidParts += $manufacturerId } if ($productCode) { $edidParts += $productCode } if ($null -ne $serialNumber) { $edidParts += "SN:$serialNumber" } $edid = $edidParts -join ' ' } $rows += [pscustomobject]@{ Name = [string](Get-ObjectProperty -Object $device -Name 'friendly_name') DeviceId = [string](Get-ObjectProperty -Object $device -Name 'device_id') DisplayName = [string](Get-ObjectProperty -Object $device -Name 'display_name') Active = $active Primary = $primary Resolution = $resolution RefreshRate = $refreshRate Edid = $edid } } return $rows } function Get-SunshineDisplayDevicesDirect { param([string]$InstallDir) $sunshineExe = Join-Path $InstallDir 'sunshine.exe' if (-not (Test-Path -LiteralPath $sunshineExe)) { throw "Sunshine executable not found: $sunshineExe" } $stdoutPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.out' -f ([guid]::NewGuid())) $stderrPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.err' -f ([guid]::NewGuid())) $probeLogPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.log' -f ([guid]::NewGuid())) $process = $null function Stop-ProbeProcess { param($Process) if ($null -eq $Process -or $Process.HasExited) { return } Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue try { $Process.WaitForExit(2000) | Out-Null } catch { } } try { $arguments = @('-1', "log_path=$probeLogPath") $process = Start-Process -FilePath $sunshineExe -ArgumentList $arguments -WorkingDirectory $InstallDir -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru $deadline = (Get-Date).AddSeconds(10) $json = $null while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 200 if (Test-Path -LiteralPath $stdoutPath) { $text = Get-Content -LiteralPath $stdoutPath -Raw $json = Get-JsonArrayAfterMarker -Text $text -Marker 'Currently available display devices:' if (-not [string]::IsNullOrWhiteSpace($json)) { break } } if ($process.HasExited) { break } } Stop-ProbeProcess -Process $process if ([string]::IsNullOrWhiteSpace($json)) { $stdout = if (Test-Path -LiteralPath $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw } else { '' } $stderr = if (Test-Path -LiteralPath $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw } else { '' } throw "Sunshine did not return a display-device list.`n`nstdout:`n$stdout`n`nstderr:`n$stderr" } return ConvertTo-DisplayRows -Devices @($json | ConvertFrom-Json) } finally { Stop-ProbeProcess -Process $process Remove-Item -LiteralPath $stdoutPath, $stderrPath, $probeLogPath -ErrorAction SilentlyContinue } } function Set-ConfigValue { param( [string[]]$Lines, [string]$Key, [string]$Value ) $pattern = '^\s*' + [regex]::Escape($Key) + '\s*=' $replacement = "$Key = $Value" $found = $false for ($i = 0; $i -lt $Lines.Count; $i++) { if ($Lines[$i] -match $pattern) { $Lines[$i] = $replacement $found = $true break } } if (-not $found) { $Lines += $replacement } return ,$Lines } function Remove-ConfigValues { param( [string[]]$Lines, [string[]]$Keys ) $patterns = @($Keys | ForEach-Object { '^\s*' + [regex]::Escape($_) + '\s*=' }) $kept = @() foreach ($line in $Lines) { $remove = $false foreach ($pattern in $patterns) { if ($line -match $pattern) { $remove = $true break } } if (-not $remove) { $kept += $line } } return ,$kept } function Remove-LegacyDisplayPrepCommands { param([string[]]$Lines) $pattern = '^\s*global_prep_cmd\s*=\s*(.+?)\s*$' for ($i = 0; $i -lt $Lines.Count; $i++) { if ($Lines[$i] -notmatch $pattern) { continue } $json = $Matches[1] try { $commands = @($json | ConvertFrom-Json) if ($commands.Count -eq 1 -and $commands[0] -is [Array]) { $commands = $commands[0] } } catch { return ,$Lines } $kept = @() foreach ($command in $commands) { $do = if ($null -ne $command.do) { [string]$command.do } else { '' } $undo = if ($null -ne $command.undo) { [string]$command.undo } else { '' } $combined = "$do $undo" $isLegacyDisplayCommand = $combined -match '(?i)(idd_an\.bat|idd_aus\.bat|DisplaySwitch(\.exe)?\s+(/internal|/external|/extend|/clone|[1234]))' if (-not $isLegacyDisplayCommand) { $kept += $command } } $Lines[$i] = 'global_prep_cmd = ' + (ConvertTo-Json -InputObject $kept -Compress -Depth 10) return ,$Lines } return ,$Lines } function Save-SunshineConfig { param( [string]$ConfigPath, [pscustomobject]$SelectedDisplay ) if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Sunshine config not found: $ConfigPath" } $backupPath = '{0}.bak-{1}' -f $ConfigPath, (Get-Date -Format 'yyyyMMdd-HHmmss') Copy-Item -LiteralPath $ConfigPath -Destination $backupPath $lines = @(Get-Content -LiteralPath $ConfigPath) $lines = Remove-LegacyDisplayPrepCommands -Lines $lines $lines = Set-ConfigValue -Lines $lines -Key 'output_name' -Value $SelectedDisplay.DeviceId $lines = Set-ConfigValue -Lines $lines -Key 'dd_configuration_option' -Value 'ensure_only_display' $lines = Set-ConfigValue -Lines $lines -Key 'dd_resolution_option' -Value 'auto' $lines = Set-ConfigValue -Lines $lines -Key 'dd_refresh_rate_option' -Value 'auto' $lines = Set-ConfigValue -Lines $lines -Key 'dd_config_revert_delay' -Value '3000' Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding UTF8 return $backupPath } function Reset-SunshineDisplayConfig { param([string]$ConfigPath) if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Sunshine config not found: $ConfigPath" } $backupPath = '{0}.bak-{1}' -f $ConfigPath, (Get-Date -Format 'yyyyMMdd-HHmmss') Copy-Item -LiteralPath $ConfigPath -Destination $backupPath $keysToRemove = @( 'output_name' 'dd_configuration_option' 'dd_resolution_option' 'dd_refresh_rate_option' 'dd_config_revert_delay' ) $lines = @(Get-Content -LiteralPath $ConfigPath) $lines = Remove-ConfigValues -Lines $lines -Keys $keysToRemove Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding UTF8 return $backupPath } function Show-MonitorMenu { param([object[]]$Devices) Write-Host '' Write-Host 'Sunshine Streaming-Monitor auswaehlen' Write-Host '====================================' Write-Host '' for ($i = 0; $i -lt $Devices.Count; $i++) { $device = $Devices[$i] $state = if ($device.Active) { 'aktiv' } else { 'inaktiv' } $primary = if ($device.Primary) { ', primaer' } else { '' } $displayName = if ([string]::IsNullOrWhiteSpace($device.DisplayName)) { '-' } else { $device.DisplayName } $resolution = if ([string]::IsNullOrWhiteSpace($device.Resolution)) { '-' } else { $device.Resolution } $refreshRate = if ([string]::IsNullOrWhiteSpace($device.RefreshRate)) { '-' } else { $device.RefreshRate } $edid = if ([string]::IsNullOrWhiteSpace($device.Edid)) { '-' } else { $device.Edid } Write-Host ('[{0}] {1}' -f ($i + 1), $device.Name) Write-Host (' ID : {0}' -f $device.DeviceId) Write-Host (' DisplayName : {0}' -f $displayName) Write-Host (' Status : {0}{1}' -f $state, $primary) Write-Host (' Modus : {0}, {1}' -f $resolution, $refreshRate) Write-Host (' EDID : {0}' -f $edid) Write-Host '' } Write-Host '[R] Sunshine-Monitor-Einstellungen auf Default zuruecksetzen' Write-Host '[Q] Abbrechen' Write-Host '' while ($true) { $choice = (Read-Host 'Auswahl').Trim() if ($choice -match '^(q|Q)$') { return [pscustomobject]@{ Action = 'quit'; Display = $null } } if ($choice -match '^(r|R)$') { return [pscustomobject]@{ Action = 'reset'; Display = $null } } $number = 0 if ([int]::TryParse($choice, [ref]$number) -and $number -ge 1 -and $number -le $Devices.Count) { return [pscustomobject]@{ Action = 'select'; Display = $Devices[$number - 1] } } Write-Host 'Ungueltige Auswahl. Bitte Zahl, R oder Q eingeben.' } } function Pause-IfInteractive { if (-not $Silent -and -not $ListOnly -and $env:SDS_NO_PAUSE -ne '1') { Write-Host '' Read-Host 'Enter zum Schliessen' | Out-Null } } function Restart-Elevated { param( [string]$RequestedAction, [string]$SelectedDeviceId ) $commands = @( ('set "SDS_ACTION={0}"' -f $RequestedAction) ('set "SDS_SUNSHINE_DIR={0}"' -f $SunshineDir) ('set "SDS_NO_RESTART={0}"' -f ($(if ($NoRestart) { '1' } else { '0' }))) ) if (-not [string]::IsNullOrWhiteSpace($SelectedDeviceId)) { $commands += ('set "SDS_DEVICE_ID={0}"' -f $SelectedDeviceId) } if (-not [string]::IsNullOrWhiteSpace($env:SDS_CONFIG_PATH)) { $commands += ('set "SDS_CONFIG_PATH={0}"' -f $env:SDS_CONFIG_PATH) } if ($Silent) { $commands += 'set "SDS_SILENT=1"' } $commands += ('"{0}"' -f $BatchPath) $command = $commands -join ' && ' Start-Process -FilePath $env:ComSpec -ArgumentList ('/c ' + $command) -Verb RunAs } function Restart-SunshineServiceIfRequested { param([switch]$NoRestart) if ($NoRestart) { return } $answer = (Read-Host 'Sunshine jetzt neu starten, damit die Aenderung sofort aktiv wird? [j/N]').Trim() if ($answer -notmatch '^(j|J|y|Y)$') { return } $services = @(Get-Service | Where-Object { $_.Name -like '*Sunshine*' -or $_.DisplayName -like '*Sunshine*' }) if ($services.Count -eq 0) { Write-Host 'Kein Sunshine-Dienst gefunden. Starte Sunshine manuell neu.' return } foreach ($service in $services) { Restart-Service -Name $service.Name -Force } Write-Host 'Sunshine wurde neu gestartet.' } $configPath = if ([string]::IsNullOrWhiteSpace($env:SDS_CONFIG_PATH)) { Join-Path $SunshineDir 'config\sunshine.conf' } else { $env:SDS_CONFIG_PATH } try { if ($Action -eq 'reset') { if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) { Restart-Elevated -RequestedAction 'reset' -SelectedDeviceId '' exit } $backupPath = Reset-SunshineDisplayConfig -ConfigPath $configPath if ($Silent) { Write-Output ("Reset: Sunshine display settings removed") Write-Output ("Config: {0}" -f $configPath) Write-Output ("Backup: {0}" -f $backupPath) exit } Write-Host '' Write-Host 'Sunshine-Monitor-Einstellungen wurden auf Default zurueckgesetzt.' Write-Host ("Config : {0}" -f $configPath) Write-Host ("Backup : {0}" -f $backupPath) Write-Host '' Restart-SunshineServiceIfRequested -NoRestart:$NoRestart Pause-IfInteractive exit } if ([string]::IsNullOrWhiteSpace($DeviceId)) { $devices = @(Get-SunshineDisplayDevicesDirect -InstallDir $SunshineDir) } else { $devices = @(Get-SunshineDisplayDevicesDirect -InstallDir $SunshineDir) } if ($ListOnly) { $devices | Format-Table Name, DeviceId, DisplayName, Active, Primary, Resolution, RefreshRate -AutoSize exit } if ([string]::IsNullOrWhiteSpace($DeviceId)) { $menuResult = Show-MonitorMenu -Devices $devices if ($menuResult.Action -eq 'quit') { Write-Host 'Abgebrochen.' Pause-IfInteractive exit } if ($menuResult.Action -eq 'reset') { if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) { Restart-Elevated -RequestedAction 'reset' -SelectedDeviceId '' exit } $backupPath = Reset-SunshineDisplayConfig -ConfigPath $configPath Write-Host '' Write-Host 'Sunshine-Monitor-Einstellungen wurden auf Default zurueckgesetzt.' Write-Host ("Config : {0}" -f $configPath) Write-Host ("Backup : {0}" -f $backupPath) Write-Host '' Restart-SunshineServiceIfRequested -NoRestart:$NoRestart Pause-IfInteractive exit } $selectedDisplay = $menuResult.Display } else { $selectedDisplay = $devices | Where-Object { $_.DeviceId -ieq $DeviceId } | Select-Object -First 1 if ($null -eq $selectedDisplay) { throw "Display device id not found in Sunshine output: $DeviceId" } } if ($null -eq $selectedDisplay) { exit } if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) { Restart-Elevated -RequestedAction 'select' -SelectedDeviceId $selectedDisplay.DeviceId exit } $backupPath = Save-SunshineConfig -ConfigPath $configPath -SelectedDisplay $selectedDisplay if ($Silent) { Write-Output ("Selected: {0}" -f $selectedDisplay.Name) Write-Output ("DeviceId: {0}" -f $selectedDisplay.DeviceId) Write-Output ("Config: {0}" -f $configPath) Write-Output ("Backup: {0}" -f $backupPath) exit } Write-Host '' Write-Host 'Sunshine-Konfiguration gespeichert.' Write-Host ("Ausgewaehlt : {0}" -f $selectedDisplay.Name) Write-Host ("ID : {0}" -f $selectedDisplay.DeviceId) Write-Host ("Config : {0}" -f $configPath) Write-Host ("Backup : {0}" -f $backupPath) Write-Host '' Restart-SunshineServiceIfRequested -NoRestart:$NoRestart Pause-IfInteractive } catch { Write-Error $_ Pause-IfInteractive exit 1 }