diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake
index 069fd85154b..a3718f7531b 100644
--- a/cmake/packaging/windows.cmake
+++ b/cmake/packaging/windows.cmake
@@ -28,6 +28,9 @@ install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application)
# Mandatory scripts
+install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/sunshine-setup.ps1"
+ DESTINATION "scripts"
+ COMPONENT assets)
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/"
DESTINATION "scripts"
COMPONENT assets)
diff --git a/cmake/packaging/windows_nsis.cmake b/cmake/packaging/windows_nsis.cmake
index 6644a17df67..ed37bbbbb3e 100644
--- a/cmake/packaging/windows_nsis.cmake
+++ b/cmake/packaging/windows_nsis.cmake
@@ -4,35 +4,36 @@
set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\\\${PROJECT_EXE}")
# Extra install commands
-# Restores permissions on the install directory
-# Migrates config files from the root into the new config folder
-# Install service
+# Runs the main setup script which handles all installation tasks
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
- IfSilent +2 0
- ExecShell 'open' 'https://docs.lizardbyte.dev/projects/sunshine'
- nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" add'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"'
- NoController:
+ ; Enable detailed logging
+ LogSet on
+ IfSilent +3 0
+ nsExec::ExecToLog \
+ 'powershell -ExecutionPolicy Bypass \
+ -File \\\"$INSTDIR\\\\scripts\\\\sunshine-setup.ps1\\\" -Action install'
+ Goto +2
+ nsExec::ExecToLog \
+ 'powershell -ExecutionPolicy Bypass \
+ -File \\\"$INSTDIR\\\\scripts\\\\sunshine-setup.ps1\\\" -Action install -Silent'
+ install_done:
")
# Extra uninstall commands
-# Uninstall service
+# Runs the main setup script which handles all uninstallation tasks
set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS
"${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS}
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"'
- nsExec::ExecToLog '\\\"$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe\\\" --restore-nvprefs-undo'
+ ; Enable detailed logging
+ LogSet on
+ nsExec::ExecToLog \
+ 'powershell -ExecutionPolicy Bypass \
+ -File \\\"$INSTDIR\\\\scripts\\\\sunshine-setup.ps1\\\" -Action uninstall'
MessageBox MB_YESNO|MB_ICONQUESTION \
- 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \
- /SD IDNO IDNO NoDelete
- RMDir /r \\\"$INSTDIR\\\"; skipped if no
- nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" remove'
- NoDelete:
+ 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \
+ /SD IDNO IDNO no_delete
+ RMDir /r \\\"$INSTDIR\\\"; skipped if no
+ no_delete:
")
# Adding an option for the start menu
diff --git a/docs/getting_started.md b/docs/getting_started.md
index d2afb2b9354..9872ed4507e 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -334,6 +334,12 @@ brew uninstall sunshine
1. Download and install
[Sunshine-Windows-AMD64-installer.exe](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-Windows-AMD64-installer.exe)
+> [!TIP]
+> Installer logs can be found in the following locations.
+> | File | log paths |
+> | ---- | --------- |
+> | .exe | `%%PROGRAMFILES%/Sunshine/install.log`
`%%TEMP%/Sunshine/logs/install/` |
+
> [!CAUTION]
> You should carefully select or unselect the options you want to install. Do not blindly install or
> enable features.
diff --git a/src_assets/windows/misc/sunshine-setup.ps1 b/src_assets/windows/misc/sunshine-setup.ps1
new file mode 100644
index 00000000000..f0015f0e1c0
--- /dev/null
+++ b/src_assets/windows/misc/sunshine-setup.ps1
@@ -0,0 +1,674 @@
+# Sunshine Setup Script
+# This script orchestrates the installation and uninstallation of Sunshine
+# Usage: sunshine-setup.ps1 -Action [install|uninstall] [-Silent]
+
+param(
+ [Parameter(Mandatory=$false)]
+ [ValidateSet(
+ "install",
+ "uninstall"
+ )]
+ [string]$Action,
+
+ [Parameter(Mandatory=$false)]
+ [switch]$Silent
+)
+
+# Constants
+$DocsUrl = "https://docs.lizardbyte.dev/projects/sunshine"
+
+# Set preference variables for output streams
+$InformationPreference = 'Continue'
+
+# Function to write output to both console (with color/stream) and log file (without color)
+function Write-LogMessage {
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
+ Justification='Write-Host is required for colored output')]
+ param(
+ [Parameter(Mandatory=$true)]
+ [AllowEmptyString()]
+ [string]$Message,
+
+ [Parameter(Mandatory=$false)]
+ [ValidateSet(
+ 'Debug',
+ 'Error',
+ 'Information',
+ 'Step',
+ 'Success',
+ 'Verbose',
+ 'Warning'
+ )]
+ [string]$Level = 'Information',
+
+ [Parameter(Mandatory=$false)]
+ [ValidateSet(
+ 'Black',
+ 'Blue',
+ 'Cyan',
+ 'DarkGray',
+ 'Gray',
+ 'Green',
+ 'Magenta',
+ 'Red',
+ 'White',
+ 'Yellow'
+ )]
+ [string]$Color = $null,
+
+ [Parameter(Mandatory=$false)]
+ [switch]$NoTimestamp,
+
+ [Parameter(Mandatory=$false)]
+ [switch]$NoLogFile
+ )
+
+ # Map levels to colors and output streams
+ $levelConfig = @{
+ 'Debug' = @{ DefaultColor = 'DarkGray'; Stream = 'Debug'; Emoji = ''; LogLevel = 'DEBUG' }
+ 'Error' = @{ DefaultColor = 'Red'; Stream = 'Error'; Emoji = '✗'; LogLevel = 'ERROR' }
+ 'Information' = @{ DefaultColor = $null; Stream = 'Host'; Emoji = ''; LogLevel = 'INFO' }
+ 'Step' = @{ DefaultColor = 'Cyan'; Stream = 'Host'; Emoji = '==>'; LogLevel = 'INFO' }
+ 'Success' = @{ DefaultColor = 'Green'; Stream = 'Host'; Emoji = '✓'; LogLevel = 'INFO' }
+ 'Verbose' = @{ DefaultColor = 'DarkGray'; Stream = 'Verbose'; Emoji = ''; LogLevel = 'VERBOSE' }
+ 'Warning' = @{ DefaultColor = 'Yellow'; Stream = 'Warning'; Emoji = '⚠'; LogLevel = 'WARN' }
+ }
+
+ $config = $levelConfig[$Level]
+
+ # Use custom color if specified, otherwise use default color for the level
+ $displayColor = if ($Color) { $Color } else { $config.DefaultColor }
+
+ # Write to appropriate output stream with color
+ switch ($config.Stream) {
+ 'Debug' {
+ Write-Debug $Message
+ }
+ 'Error' {
+ Write-Error $Message
+ }
+ 'Host' {
+ if ($null -ne $displayColor) {
+ Write-Host "$($config.Emoji) $Message" -ForegroundColor $displayColor
+ } else {
+ Write-Host "$($config.Emoji) $Message"
+ }
+ }
+ 'Information' {
+ Write-Information $Message
+ }
+ 'Verbose' {
+ Write-Verbose $Message
+ }
+ 'Warning' {
+ Write-Warning $Message
+ }
+ default {
+ Write-Information $Message
+ }
+ }
+
+ # Write to log file without color codes (only if LogPath exists and not disabled)
+ if ($script:LogPath -and -not $NoLogFile) {
+ try {
+ # Format log entry with timestamp and level
+ if ($NoTimestamp) {
+ $logEntry = $Message
+ } else {
+ $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+ $logEntry = "[$timestamp] [$($config.LogLevel)] $Message"
+ }
+
+ $logEntry | Out-File `
+ -FilePath $script:LogPath `
+ -Append `
+ -Encoding UTF8
+ } catch {
+ # Avoid infinite recursion - use Write-Verbose directly
+ Write-Verbose "Could not write to log file: $($_.Exception.Message)"
+ }
+ }
+}
+
+# Function to print a separator bar
+function Write-Bar {
+ param(
+ [string]$Level = 'Information',
+ [int]$Length = 63,
+ [string]$Color = $null,
+ [switch]$NoTimestamp
+ )
+ $bar = "=" * $Length
+ if ($Color) {
+ Write-LogMessage -Message $bar -Level $Level -Color $Color -NoTimestamp:$NoTimestamp
+ } else {
+ Write-LogMessage -Message $bar -Level $Level -NoTimestamp:$NoTimestamp
+ }
+}
+
+# Function to print text framed by bars
+function Write-FramedText {
+ param(
+ [string]$Message,
+ [string]$Level = 'Information',
+ [int]$BarLength = 63,
+ [string]$Color = $null,
+ [switch]$NoTimestamp,
+ [switch]$NoCenter
+ )
+
+ # Center the message if NoCenter is not specified
+ $displayMessage = $Message
+ if (-not $NoCenter) {
+ $messageLength = $Message.Trim().Length
+
+ if ($messageLength -lt $BarLength) {
+ $totalPadding = $BarLength - $messageLength
+ $leftPadding = [Math]::Floor($totalPadding / 2)
+ $displayMessage = (' ' * $leftPadding) + $Message.Trim()
+ } else {
+ $displayMessage = $Message.Trim()
+ }
+ }
+
+ if ($Color) {
+ Write-Bar -Level $Level -Length $BarLength -Color $Color -NoTimestamp:$NoTimestamp
+ Write-LogMessage -Message $displayMessage -Level $Level -Color $Color -NoTimestamp:$NoTimestamp
+ Write-Bar -Level $Level -Length $BarLength -Color $Color -NoTimestamp:$NoTimestamp
+ } else {
+ Write-Bar -Level $Level -Length $BarLength -NoTimestamp:$NoTimestamp
+ Write-LogMessage -Message $displayMessage -Level $Level -NoTimestamp:$NoTimestamp
+ Write-Bar -Level $Level -Length $BarLength -NoTimestamp:$NoTimestamp
+ }
+}
+
+# Function to write to log file (helper function)
+function Write-LogFile {
+ param(
+ [string[]]$Lines
+ )
+ if ($script:LogPath) {
+ try {
+ foreach ($line in $Lines) {
+ $line | Out-File `
+ -FilePath $script:LogPath `
+ -Append `
+ -Encoding UTF8
+ }
+ } catch {
+ Write-Warning "Failed to write to log file: $($_.Exception.Message)"
+ }
+ }
+}
+
+# If Action is not provided, prompt the user
+if (-not $Action) {
+ Write-Information ""
+ Write-FramedText -Message "🔅 Sunshine Setup Script" -Level "Information" -Color "Cyan"
+ Write-Information ""
+ Write-LogMessage -Message "Please select an action:" -Level "Information" -Color "Yellow"
+ Write-LogMessage -Message " 1. Install Sunshine" -Level "Information" -Color "Green"
+ Write-LogMessage -Message " 2. Uninstall Sunshine" -Level "Information" -Color "Red"
+ Write-Information ""
+
+ $validChoice = $false
+ while (-not $validChoice) {
+ $choice = Read-Host "Enter your choice (1 or 2)"
+
+ switch ($choice) {
+ "1" {
+ $Action = "install"
+ $validChoice = $true
+ }
+ "2" {
+ $Action = "uninstall"
+ $validChoice = $true
+ }
+ default {
+ Write-Warning "Invalid choice. Please select 1 or 2."
+ Write-Information ""
+ }
+ }
+ }
+ Write-Information ""
+}
+
+# Check if running as administrator, if not, relaunch with elevation
+$currentPrincipal = New-Object `
+ Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
+$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+
+if (-not $isAdmin) {
+ Write-Warning "This script requires administrator privileges. Relaunching with elevation..."
+
+ # Build the argument list for the elevated process
+ $arguments = "-ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Action $Action"
+ if ($Silent) {
+ $arguments += " -Silent"
+ }
+
+ try {
+ # Relaunch the script with elevation
+ Start-Process powershell.exe -Verb RunAs -ArgumentList $arguments -Wait
+ exit $LASTEXITCODE
+ } catch {
+ Write-Error "Failed to elevate privileges: $($_.Exception.Message)"
+ exit 1
+ }
+}
+
+# Get the script directory and root directory
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$RootDir = Split-Path -Parent $ScriptDir
+
+# Set up transcript logging
+$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
+$logDir = Join-Path $env:TEMP "Sunshine\logs\$Action"
+$LogPath = Join-Path $logDir "${timestamp}.log"
+
+# Ensure the log directory exists
+if (-not (Test-Path $logDir)) {
+ New-Item -ItemType Directory -Path $logDir -Force | Out-Null
+}
+
+# Store LogPath in script scope for logging functions
+$script:LogPath = $LogPath
+
+# Function to execute a batch script if it exists
+function Invoke-ScriptIfExist {
+ param(
+ [string]$ScriptPath,
+ [string]$Arguments = "",
+ [string]$Description = "",
+ [string]$Emoji = "🔧"
+ )
+
+ if ($Description) {
+ Write-LogMessage -Message "$Emoji $Description" -Level "Step"
+ }
+
+ if (Test-Path $ScriptPath) {
+ Write-LogMessage -Message "Executing: $ScriptPath $Arguments" -Level "Information"
+
+ # Capture output to suppress it from console but log it
+ $stdoutFile = [System.IO.Path]::GetTempFileName()
+ $stderrFile = [System.IO.Path]::GetTempFileName()
+
+ try {
+ if ($Arguments -ne "") {
+ $process = Start-Process `
+ -FilePath $ScriptPath `
+ -ArgumentList $Arguments `
+ -Wait `
+ -PassThru `
+ -NoNewWindow `
+ -RedirectStandardOutput $stdoutFile `
+ -RedirectStandardError $stderrFile
+ } else {
+ $process = Start-Process `
+ -FilePath $ScriptPath `
+ -Wait `
+ -PassThru `
+ -NoNewWindow `
+ -RedirectStandardOutput $stdoutFile `
+ -RedirectStandardError $stderrFile
+ }
+
+ # Log and display the output
+ if (Test-Path $stdoutFile) {
+ $output = Get-Content $stdoutFile -Raw -ErrorAction SilentlyContinue
+ if ($output) {
+ # Display output with indentation
+ $output -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Information" -Color "DarkGray"
+ }
+ }
+ }
+ }
+ if (Test-Path $stderrFile) {
+ $errors = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
+ if ($errors) {
+ # Display errors with indentation
+ $errors -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Warning"
+ }
+ }
+ }
+ }
+
+ if ($process.ExitCode -ne 0) {
+ Write-LogMessage -Message " ⚠ Script exited with code $($process.ExitCode): $ScriptPath" -Level "Warning"
+ return $process.ExitCode
+ } else {
+ Write-LogMessage -Message " ✓ Done" -Level "Success"
+ return 0
+ }
+ } finally {
+ # Clean up temp files
+ if (Test-Path $stdoutFile) {
+ Remove-Item $stdoutFile -Force -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $stderrFile) {
+ Remove-Item $stderrFile -Force -ErrorAction SilentlyContinue
+ }
+ }
+ } else {
+ Write-LogMessage -Message " ⓘ Skipped (script not found)" -Level "Information" -Color "DarkGray"
+ return 0
+ }
+}
+
+# Function to execute sunshine.exe with arguments if it exists
+function Invoke-SunshineIfExist {
+ param(
+ [string]$Arguments,
+ [string]$Description = "",
+ [string]$Emoji = "🔧"
+ )
+
+ if ($Description) {
+ Write-LogMessage -Message "$Emoji $Description" -Level "Step"
+ }
+
+ $SunshinePath = Join-Path $RootDir "sunshine.exe"
+
+ if (Test-Path $SunshinePath) {
+ Write-LogMessage -Message "Executing: $SunshinePath $Arguments" -Level "Information"
+
+ # Capture output to suppress it from console but log it
+ $stdoutFile = [System.IO.Path]::GetTempFileName()
+ $stderrFile = [System.IO.Path]::GetTempFileName()
+
+ try {
+ $process = Start-Process `
+ -FilePath $SunshinePath `
+ -ArgumentList $Arguments `
+ -Wait `
+ -PassThru `
+ -NoNewWindow `
+ -RedirectStandardOutput $stdoutFile `
+ -RedirectStandardError $stderrFile
+
+ # Log and display the output
+ if (Test-Path $stdoutFile) {
+ $output = Get-Content $stdoutFile -Raw -ErrorAction SilentlyContinue
+ if ($output) {
+ # Display output with indentation
+ $output -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Information" -Color "DarkGray"
+ }
+ }
+ }
+ }
+ if (Test-Path $stderrFile) {
+ $errors = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
+ if ($errors) {
+ # Display errors with indentation
+ $errors -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Warning"
+ }
+ }
+ }
+ }
+
+ if ($process.ExitCode -ne 0) {
+ Write-LogMessage -Message " ⚠ Sunshine exited with code $($process.ExitCode)" -Level "Warning"
+ return $process.ExitCode
+ } else {
+ Write-LogMessage -Message " ✓ Done" -Level "Success"
+ return 0
+ }
+ } finally {
+ # Clean up temp files
+ if (Test-Path $stdoutFile) {
+ Remove-Item $stdoutFile -Force -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $stderrFile) {
+ Remove-Item $stderrFile -Force -ErrorAction SilentlyContinue
+ }
+ }
+ } else {
+ Write-LogMessage -Message " ⓘ Skipped (executable not found)" -Level "Information" -Color "DarkGray"
+ return 0
+ }
+}
+
+# Main script logic
+Write-Information ""
+
+if ($Action -eq "install") {
+ Write-FramedText `
+ -Message "🔅 Sunshine Installation Script" `
+ -Level "Information" `
+ -Color "Yellow"
+ Write-Information ""
+
+ $totalSteps = 6
+ $currentStep = 0
+
+ # Reset permissions on the install directory
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Resetting permissions on installation directory" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ Write-LogMessage -Message "🔐 Resetting permissions on installation directory" -Level "Step"
+ try {
+ Write-LogMessage -Message "Executing: icacls.exe `"$RootDir`" /reset" -Level "Information"
+
+ # Capture output to suppress it from console but log it
+ $stdoutFile = [System.IO.Path]::GetTempFileName()
+ $stderrFile = [System.IO.Path]::GetTempFileName()
+
+ try {
+ $icaclsProcess = Start-Process `
+ -FilePath "icacls.exe" `
+ -ArgumentList "`"$RootDir`" /reset" `
+ -Wait `
+ -PassThru `
+ -NoNewWindow `
+ -RedirectStandardOutput $stdoutFile `
+ -RedirectStandardError $stderrFile
+
+ # Log and display the output
+ if (Test-Path $stdoutFile) {
+ $output = Get-Content $stdoutFile -Raw -ErrorAction SilentlyContinue
+ if ($output) {
+ # Display output with indentation
+ $output -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Information" -Color "DarkGray"
+ }
+ }
+ }
+ }
+ if (Test-Path $stderrFile) {
+ $errors = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
+ if ($errors) {
+ # Display errors with indentation
+ $errors -split "`r?`n" | ForEach-Object {
+ if ($_.Trim()) {
+ Write-LogMessage -Message " $_" -Level "Warning"
+ }
+ }
+ }
+ }
+
+ if ($icaclsProcess.ExitCode -eq 0) {
+ Write-LogMessage -Message " ✓ Done" -Level "Success"
+ } else {
+ Write-LogMessage -Message " ⚠ Exit code $($icaclsProcess.ExitCode)" -Level "Warning"
+ }
+ } finally {
+ # Clean up temp files
+ if (Test-Path $stdoutFile) {
+ Remove-Item $stdoutFile -Force -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $stderrFile) {
+ Remove-Item $stderrFile -Force -ErrorAction SilentlyContinue
+ }
+ }
+ } catch {
+ Write-LogMessage -Message " ⚠ Failed to reset permissions: $($_.Exception.Message)" -Level "Warning"
+ }
+ Write-Information ""
+
+ # 1. Update PATH (add)
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Updating system PATH" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $updatePathScript = Join-Path $RootDir "scripts\update-path.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $updatePathScript `
+ -Arguments "add" `
+ -Description "Adding Sunshine directories to PATH" `
+ -Emoji "📁"
+ Write-Information ""
+
+ # 2. Migrate configuration
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Migrating configuration" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $migrateConfigScript = Join-Path $RootDir "scripts\migrate-config.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $migrateConfigScript `
+ -Description "Migrating configuration files" `
+ -Emoji "⚙️"
+ Write-Information ""
+
+ # 3. Add firewall rules
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Configuring firewall" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $addFirewallScript = Join-Path $RootDir "scripts\add-firewall-rule.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $addFirewallScript `
+ -Description "Adding firewall rules" `
+ -Emoji "🛡️"
+ Write-Information ""
+
+ # 4. Install service
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Installing service" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $installServiceScript = Join-Path $RootDir "scripts\install-service.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $installServiceScript `
+ -Description "Installing Windows Service" `
+ -Emoji "⚡"
+ Write-Information ""
+
+ # 5. Configure autostart
+ $currentStep++
+ Write-Progress `
+ -Activity "Installing Sunshine" `
+ -Status "Configuring autostart" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $autostartScript = Join-Path $RootDir "scripts\autostart-service.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $autostartScript `
+ -Description "Configuring autostart" `
+ -Emoji "🚀"
+ Write-Information ""
+
+ Write-Progress -Activity "Installing Sunshine" -Completed
+ Write-FramedText -Message "✓ Sunshine installation completed successfully!" -Level "Success"
+
+ # Open documentation in browser (only if not running silently)
+ if (-not $Silent) {
+ Write-Information ""
+ Write-LogMessage `
+ -Message "📖 Opening documentation in your browser: $DocsUrl" `
+ -Level "Step"
+ try {
+ Start-Process $DocsUrl
+ Write-LogMessage -Message " ✓ Done" -Level "Success"
+ } catch {
+ Write-LogMessage `
+ -Message " ⓘ Could not open browser automatically: $($_.Exception.Message)" `
+ -Level "Warning"
+ }
+ }
+
+} elseif ($Action -eq "uninstall") {
+ Write-FramedText `
+ -Message "🗑️ Sunshine Uninstallation Script" `
+ -Level "Information" `
+ -Color "Yellow"
+ Write-Information ""
+
+ $totalSteps = 4
+ $currentStep = 0
+
+ # 1. Delete firewall rules
+ $currentStep++
+ Write-Progress `
+ -Activity "Uninstalling Sunshine" `
+ -Status "Removing firewall rules" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $deleteFirewallScript = Join-Path $RootDir "scripts\delete-firewall-rule.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $deleteFirewallScript `
+ -Description "Removing firewall rules" `
+ -Emoji "🛡️"
+ Write-Information ""
+
+ # 2. Uninstall service
+ $currentStep++
+ Write-Progress `
+ -Activity "Uninstalling Sunshine" `
+ -Status "Uninstalling service" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $uninstallServiceScript = Join-Path $RootDir "scripts\uninstall-service.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $uninstallServiceScript `
+ -Description "Removing Windows Service" `
+ -Emoji "⚡"
+ Write-Information ""
+
+ # 3. Restore NVIDIA preferences
+ $currentStep++
+ Write-Progress `
+ -Activity "Uninstalling Sunshine" `
+ -Status "Restoring NVIDIA settings" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ Invoke-SunshineIfExist `
+ -Arguments "--restore-nvprefs-undo" `
+ -Description "Restoring NVIDIA preferences" `
+ -Emoji "🎮"
+ Write-Information ""
+
+ # 4. Update PATH (remove)
+ $currentStep++
+ Write-Progress `
+ -Activity "Uninstalling Sunshine" `
+ -Status "Cleaning up system PATH" `
+ -PercentComplete (($currentStep / $totalSteps) * 100)
+ $updatePathScript = Join-Path $RootDir "scripts\update-path.bat"
+ Invoke-ScriptIfExist `
+ -ScriptPath $updatePathScript `
+ -Arguments "remove" `
+ -Description "Removing from PATH" `
+ -Emoji "📁"
+ Write-Information ""
+
+ Write-Progress -Activity "Uninstalling Sunshine" -Completed
+ Write-FramedText `
+ -Message "✓ Sunshine uninstallation completed successfully!" `
+ -Level "Success"
+}
+
+Write-Information ""
+exit 0