diff --git a/cmake/packaging/sunshine.iss.in b/cmake/packaging/sunshine.iss.in index 635bd6d012d..3ec1a3d5b3d 100644 --- a/cmake/packaging/sunshine.iss.in +++ b/cmake/packaging/sunshine.iss.in @@ -208,11 +208,13 @@ Source: "{#MySourceDir}\tools\SetDpi.exe"; DestDir: "{app}\tools"; Flags: ignore Source: "{#MySourceDir}\tools\setreg.exe"; DestDir: "{app}\tools"; Flags: ignoreversion; Components: assets ; Autostart script -Source: "{#MySourceDir}\scripts\autostart-service.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion; Components: autostart +; Always copy so users can run it manually as admin if they unchecked autostart. +Source: "{#MySourceDir}\scripts\autostart-service.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion ; Firewall scripts -Source: "{#MySourceDir}\scripts\add-firewall-rule.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion; Components: firewall -Source: "{#MySourceDir}\scripts\delete-firewall-rule.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion; Components: firewall +; Always copy so users can run them manually as admin if they unchecked firewall. +Source: "{#MySourceDir}\scripts\add-firewall-rule.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "{#MySourceDir}\scripts\delete-firewall-rule.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion ; Virtual Display Driver scripts & files Source: "{#MySourceDir}\scripts\install-vdd.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion @@ -228,6 +230,12 @@ Source: "{#MySourceDir}\scripts\vmouse\install-vmouse.bat"; DestDir: "{app}\scri Source: "{#MySourceDir}\scripts\vmouse\uninstall-vmouse.bat"; DestDir: "{app}\scripts\vmouse"; Flags: ignoreversion Source: "{#MySourceDir}\scripts\vmouse\driver\*"; DestDir: "{app}\scripts\vmouse\driver"; Flags: ignoreversion recursesubdirs +; Virtual audio sink (microphone / VB-Cable) scripts +; Always shipped — there is no install-time component for this; the user can +; right-click + "Run as administrator" on install-vsink.bat to enable it. +Source: "{#MySourceDir}\scripts\install-vsink.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion skipifsourcedoesntexist +Source: "{#MySourceDir}\scripts\uninstall-vsink.bat"; DestDir: "{app}\scripts"; Flags: ignoreversion skipifsourcedoesntexist + ; Optional diagnostic tools Source: "{#MySourceDir}\tools\dxgi-info.exe"; DestDir: "{app}\tools"; Flags: ignoreversion; Components: tools Source: "{#MySourceDir}\tools\audio-info.exe"; DestDir: "{app}\tools"; Flags: ignoreversion; Components: tools @@ -269,9 +277,12 @@ Filename: "https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=DXpTjzl2kZwBjN7jlRMkRJ"; Filename: "{win}\explorer.exe"; Parameters: """{app}\assets\gui\{#MyAppGUIExeName}"""; Description: "{cm:FinishLaunchGUI}"; Flags: postinstall nowait skipifsilent [UninstallRun] -; Stop running processes first +; Stop running processes first. We force-kill the service binary +; (sunshinesvc.exe) here as well, otherwise `sc stop` later may block waiting +; on its stop handler — which is the typical "uninstaller appears frozen" symptom. Filename: "taskkill"; Parameters: "/f /im sunshine-gui.exe"; Flags: runhidden; RunOnceId: "KillGUI" Filename: "taskkill"; Parameters: "/f /im sunshine.exe"; Flags: runhidden; RunOnceId: "KillSunshine" +Filename: "taskkill"; Parameters: "/f /im sunshinesvc.exe"; Flags: runhidden; RunOnceId: "KillSunshineSvc" ; Remove system components Filename: "{app}\scripts\delete-firewall-rule.bat"; Flags: runhidden waituntilterminated; RunOnceId: "DelFirewall" Filename: "{app}\scripts\uninstall-service.bat"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallService" diff --git a/src_assets/windows/misc/gamepad/install-gamepad.bat b/src_assets/windows/misc/gamepad/install-gamepad.bat index 0a047b584b7..abbec0250b0 100644 --- a/src_assets/windows/misc/gamepad/install-gamepad.bat +++ b/src_assets/windows/misc/gamepad/install-gamepad.bat @@ -48,8 +48,10 @@ for /f "tokens=3" %%a in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVer rem get browser_download_url from asset 0 of https://api.github.com/repos/nefarius/vigembus/releases/latest set latest_release_url=https://api.github.com/repos/nefarius/vigembus/releases/latest -rem Use curl to get the api response, and find the browser_download_url -for /F "tokens=* USEBACKQ" %%F in (`curl -s !proxy! -L %latest_release_url% ^| findstr browser_download_url`) do ( +rem Use curl to get the api response, and find the browser_download_url. +rem `--connect-timeout 10 --max-time 20` ensures we don't hang for minutes if +rem GitHub or the local network is unreachable during install. +for /F "tokens=* USEBACKQ" %%F in (`curl -s --connect-timeout 10 --max-time 20 !proxy! -L %latest_release_url% ^| findstr browser_download_url`) do ( set browser_download_url=%%F ) @@ -66,12 +68,12 @@ if "%browser_download_url%"=="" ( echo %browser_download_url% -rem Download the exe +rem Download the exe (with connect/transfer timeout to avoid install hang) set "installer=%temp_dir%\virtual_gamepad.exe" -curl -f -s -L !proxy! -o "%installer%" "%browser_download_url%" +curl -f -s -L --connect-timeout 10 --max-time 120 !proxy! -o "%installer%" "%browser_download_url%" if errorlevel 1 ( echo Direct download failed, trying mirror... - curl -f -s -L !proxy! -o "%installer%" "https://mirror.ghproxy.com/%browser_download_url%" + curl -f -s -L --connect-timeout 10 --max-time 120 !proxy! -o "%installer%" "https://mirror.ghproxy.com/%browser_download_url%" ) if not exist "%installer%" ( diff --git a/src_assets/windows/misc/path/update-path.bat b/src_assets/windows/misc/path/update-path.bat index 275c40e3645..19ecdfb5f3e 100644 --- a/src_assets/windows/misc/path/update-path.bat +++ b/src_assets/windows/misc/path/update-path.bat @@ -1,112 +1,49 @@ @echo off +rem ============================================================================ +rem update-path.bat +rem +rem Adds or removes the Sunshine install directory and its `tools\` subdir to +rem the *machine* PATH (HKLM Session Manager Environment). +rem +rem Implementation note: previous pure-batch versions parsed PATH with delayed +rem expansion + `for` substring tricks. That is O(n^2) on PATH length and +rem brittle on PATHs containing `!`, `&` or unbalanced quotes — large machines +rem hit this and the Inno uninstaller appeared to hang. This version offloads +rem the parsing to PowerShell and writes the registry directly via +rem Set-ItemProperty (REG_EXPAND_SZ, no WM_SETTINGCHANGE broadcast — so no +rem 5-second SendMessageTimeout wait per top-level window). +rem ============================================================================ +setlocal set "PATH=%SystemRoot%\System32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SystemRoot%\System32\WindowsPowerShell\v1.0" -setlocal EnableDelayedExpansion -rem Check if parameter is provided if "%~1"=="" ( - echo Usage: %0 [add^|remove] - echo add - Adds Sunshine directories to system PATH - echo remove - Removes Sunshine directories from system PATH + echo Usage: %~nx0 [add^|remove] exit /b 1 ) -rem Get sunshine root directory -for %%I in ("%~dp0\..") do set "ROOT_DIR=%%~fI" -echo Sunshine root directory: !ROOT_DIR! - -rem Define directories to add to path -set "PATHS_TO_MANAGE[0]=!ROOT_DIR!" -set "PATHS_TO_MANAGE[1]=!ROOT_DIR!\tools" - -rem System path registry location -set "KEY_NAME=HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" -set "VALUE_NAME=Path" - -rem Get the current path -for /f "tokens=2*" %%A in ('reg query "%KEY_NAME%" /v "%VALUE_NAME%"') do set "CURRENT_PATH=%%B" -echo Current path: !CURRENT_PATH! - -rem Check if adding to path -if /i "%~1"=="add" ( - set "NEW_PATH=!CURRENT_PATH!" - - rem Process each directory to add - for /L %%i in (0,1,1) do ( - set "DIR_TO_ADD=!PATHS_TO_MANAGE[%%i]!" - - rem Check if path already contains this directory - echo "!CURRENT_PATH!" | findstr /i /c:"!DIR_TO_ADD!" > nul - if !ERRORLEVEL!==0 ( - echo !DIR_TO_ADD! already in path - ) else ( - echo Adding to path: !DIR_TO_ADD! - set "NEW_PATH=!NEW_PATH!;!DIR_TO_ADD!" - ) - ) - - rem Only update if path was changed - if "!NEW_PATH!" neq "!CURRENT_PATH!" ( - rem Set the new path in the registry - reg add "%KEY_NAME%" /v "%VALUE_NAME%" /t REG_EXPAND_SZ /d "!NEW_PATH!" /f - if !ERRORLEVEL!==0 ( - echo Successfully added Sunshine directories to PATH - ) else ( - echo Failed to add Sunshine directories to PATH - ) - ) else ( - echo No changes needed to PATH - ) - exit /b !ERRORLEVEL! -) - -rem Check if removing from path -if /i "%~1"=="remove" ( - set "CHANGES_MADE=0" - - rem Process each directory to remove - for /L %%i in (0,1,1) do ( - set "DIR_TO_REMOVE=!PATHS_TO_MANAGE[%%i]!" - - rem Check if path contains this directory - echo "!CURRENT_PATH!" | findstr /i /c:"!DIR_TO_REMOVE!" > nul - if !ERRORLEVEL!==0 ( - echo Removing from path: !DIR_TO_REMOVE! - - rem Build a new path by parsing and filtering the current path - set "NEW_PATH=" - for %%p in ("!CURRENT_PATH:;=" "!") do ( - set "PART=%%~p" - if /i "!PART!" NEQ "!DIR_TO_REMOVE!" ( - if defined NEW_PATH ( - set "NEW_PATH=!NEW_PATH!;!PART!" - ) else ( - set "NEW_PATH=!PART!" - ) - ) - ) - - set "CURRENT_PATH=!NEW_PATH!" - set "CHANGES_MADE=1" - ) else ( - echo !DIR_TO_REMOVE! not found in path - ) - ) - - rem Only update if path was changed - if "!CHANGES_MADE!"=="1" ( - rem Set the new path in the registry - reg add "%KEY_NAME%" /v "%VALUE_NAME%" /t REG_EXPAND_SZ /d "!CURRENT_PATH!" /f - if !ERRORLEVEL!==0 ( - echo Successfully removed Sunshine directories from PATH - ) else ( - echo Failed to remove Sunshine directories from PATH - ) - ) else ( - echo No changes needed to PATH - ) - exit /b !ERRORLEVEL! -) - -echo Unknown parameter: %~1 -echo Usage: %0 [add^|remove] -exit /b 1 \ No newline at end of file +set "SUNSHINE_PATH_ACTION=%~1" +for %%I in ("%~dp0\..") do set "SUNSHINE_PATH_ROOT=%%~fI" + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference = 'Stop';" ^ + "$action = $env:SUNSHINE_PATH_ACTION;" ^ + "$root = $env:SUNSHINE_PATH_ROOT;" ^ + "$tools = Join-Path $root 'tools';" ^ + "$key = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment';" ^ + "$cur = (Get-ItemProperty -Path $key -Name Path -ErrorAction Stop).Path;" ^ + "$parts = @($cur -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' });" ^ + "$targets = @($root, $tools);" ^ + "switch ($action.ToLower()) {" ^ + " 'add' { foreach ($t in $targets) { if (-not ($parts -icontains $t)) { $parts += $t } } }" ^ + " 'remove' { $parts = @($parts | Where-Object { -not ($targets -icontains $_) }) }" ^ + " default { Write-Host (\"Unknown action: \" + $action); exit 2 }" ^ + "}" ^ + "$new = ($parts -join ';');" ^ + "if ($new -ne $cur) {" ^ + " Set-ItemProperty -Path $key -Name Path -Value $new -Type ExpandString;" ^ + " Write-Host 'PATH updated.';" ^ + "} else {" ^ + " Write-Host 'PATH unchanged.';" ^ + "}" + +exit /b %ERRORLEVEL% diff --git a/src_assets/windows/misc/service/uninstall-service.bat b/src_assets/windows/misc/service/uninstall-service.bat index 43c021ff33e..15a8c0dd60c 100644 --- a/src_assets/windows/misc/service/uninstall-service.bat +++ b/src_assets/windows/misc/service/uninstall-service.bat @@ -33,10 +33,19 @@ if %ERRORLEVEL%==0 ( echo !CONTENT!> "%SERVICE_CONFIG_FILE%" ) -rem Stop and delete the legacy SunshineSvc service -net stop sunshinesvc -sc delete sunshinesvc +rem Stop and delete the legacy SunshineSvc service (non-blocking) +sc stop sunshinesvc >nul 2>&1 +sc delete sunshinesvc >nul 2>&1 -rem Stop and delete the new SunshineService service -net stop SunshineService -sc delete SunshineService +rem Force-kill the service binary FIRST so SCM can transition the service to +rem STOPPED quickly. We deliberately avoid `net stop`, which blocks for up to +rem 30 seconds waiting on the service's stop handler — that is the typical +rem cause of "Inno uninstaller appears frozen". +taskkill /f /im sunshinesvc.exe >nul 2>&1 + +rem Issue a stop control as a courtesy (idempotent, returns quickly), then +rem delete. If still in stop-pending, SCM marks the service for deletion and +rem removes it once stopped — which is immediate after the taskkill above. +sc stop SunshineService >nul 2>&1 +sc delete SunshineService >nul 2>&1 +exit /b 0 diff --git a/src_assets/windows/misc/vdd/uninstall-vdd.bat b/src_assets/windows/misc/vdd/uninstall-vdd.bat index d351a5819ab..81401605917 100644 --- a/src_assets/windows/misc/vdd/uninstall-vdd.bat +++ b/src_assets/windows/misc/vdd/uninstall-vdd.bat @@ -1,5 +1,6 @@ @echo off set "PATH=%SystemRoot%\System32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SystemRoot%\System32\WindowsPowerShell\v1.0" +chcp 65001 >nul rem Get sunshine root directory for %%I in ("%~dp0\..") do set "ROOT_DIR=%%~fI" @@ -9,12 +10,33 @@ set "DIST_DIR=%ROOT_DIR%\tools\vdd" set "NEFCON=%ROOT_DIR%\tools\nefconw.exe" if not exist "%NEFCON%" set "NEFCON=%DIST_DIR%\nefconw.exe" if not exist "%NEFCON%" ( - echo WARNING: nefconw.exe not found, skipping device node removal. + echo WARNING: nefconw.exe not found, skipping driver/device removal. goto :cleanup ) -if exist "%DIST_DIR%" ( - "%NEFCON%" --remove-device-node --hardware-id ROOT\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318 + +rem 1) Remove device node(s) first so the driver is no longer in use +echo Removing VDD device node... +"%NEFCON%" --remove-device-node --hardware-id Root\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318 + +rem Brief wait so the kernel finishes releasing the device handle before we +rem attempt to remove the driver package from the DriverStore. +timeout /t 1 /nobreak >nul 2>&1 + +rem 2) Uninstall the driver package from the DriverStore (requires INF path) +if exist "%DIST_DIR%\ZakoVDD.inf" ( + echo Uninstalling VDD driver package... + "%NEFCON%" --uninstall-driver --inf-path "%DIST_DIR%\ZakoVDD.inf" +) else ( + echo WARNING: ZakoVDD.inf not found in "%DIST_DIR%", skipping driver package uninstall. ) + +rem 3) Best-effort second pass in case multiple device instances remain +"%NEFCON%" --remove-device-node --hardware-id Root\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318 >nul 2>&1 + :cleanup -reg delete "HKLM\SOFTWARE\ZakoTech" /f -rmdir /S /Q "%DIST_DIR%" +echo Cleaning registry... +reg delete "HKLM\SOFTWARE\ZakoTech" /f 2>nul +if exist "%DIST_DIR%" ( + rmdir /S /Q "%DIST_DIR%" +) +echo VDD uninstall completed. diff --git a/src_assets/windows/misc/vmouse/install-vmouse.bat b/src_assets/windows/misc/vmouse/install-vmouse.bat index eb12e8c0a83..b31bb70f99f 100644 --- a/src_assets/windows/misc/vmouse/install-vmouse.bat +++ b/src_assets/windows/misc/vmouse/install-vmouse.bat @@ -53,15 +53,22 @@ rem ============================================================================ rem Stop Sunshine service to release HID device handle rem ============================================================================ +rem Use sc + taskkill (non-blocking) instead of `net stop`, which can block +rem for up to 30 seconds on a stuck stop handler. echo Stopping Sunshine service... set "SERVICE_WAS_RUNNING=0" -net stop SunshineService >nul 2>&1 +sc query SunshineService >nul 2>&1 if not errorlevel 1 ( - set "SERVICE_WAS_RUNNING=1" + sc query SunshineService | find /I "RUNNING" >nul 2>&1 + if not errorlevel 1 ( + set "SERVICE_WAS_RUNNING=1" + sc stop SunshineService >nul 2>&1 + ) + taskkill /f /im sunshinesvc.exe >nul 2>&1 + timeout /t 1 /nobreak >nul 2>&1 echo Sunshine service stopped. - timeout /t 2 /nobreak 1>nul ) else ( - echo Sunshine service not running, OK. + echo Sunshine service not installed, OK. ) rem ============================================================================ @@ -70,16 +77,24 @@ rem ============================================================================ echo Cleaning up existing Virtual Mouse driver... -rem Remove ALL existing device nodes (loop until none remain) +rem Remove ALL existing device nodes (loop until none remain). +rem Hard cap at 20 iterations to prevent an infinite loop if nefcon reports +rem success without actually removing anything (observed on some builds). set "CLEANUP_COUNT=0" +set "CLEANUP_MAX=20" :remove_loop "%NEFCON%" --remove-device-node --hardware-id Root\ZakoVirtualMouse --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da if not errorlevel 1 ( set /a CLEANUP_COUNT+=1 + if !CLEANUP_COUNT! GEQ !CLEANUP_MAX! ( + echo Reached max remove iterations (!CLEANUP_MAX!), stopping. + goto after_remove_loop + ) echo Removed a device node, checking for more... timeout /t 1 /nobreak >nul goto remove_loop ) +:after_remove_loop echo Removed !CLEANUP_COUNT! device node(s) via nefcon. rem Fallback: use pnputil to remove any remaining device instances @@ -144,12 +159,10 @@ rem Restart Sunshine service if it was running before rem ============================================================================ if "!SERVICE_WAS_RUNNING!"=="1" ( - echo Restarting Sunshine service... - net start SunshineService >nul 2>&1 + sc query SunshineService >nul 2>&1 if not errorlevel 1 ( - echo Sunshine service restarted. - ) else ( - echo WARNING: Could not restart Sunshine service. + echo Restarting Sunshine service... + sc start SunshineService >nul 2>&1 ) ) diff --git a/src_assets/windows/misc/vmouse/uninstall-vmouse.bat b/src_assets/windows/misc/vmouse/uninstall-vmouse.bat index 56b74eae306..bcdaf272cbd 100644 --- a/src_assets/windows/misc/vmouse/uninstall-vmouse.bat +++ b/src_assets/windows/misc/vmouse/uninstall-vmouse.bat @@ -18,29 +18,45 @@ if not exist "%NEFCON%" ( set "NEFCON=%ROOT_DIR%\tools\vdd\nefconw.exe" ) -rem Stop Sunshine service to release HID device handle +rem Stop Sunshine service to release HID device handle. +rem Use sc + taskkill instead of `net stop` to avoid up to 30s SCM blocking. echo Stopping Sunshine service... set "SERVICE_WAS_RUNNING=0" -net stop SunshineService >nul 2>&1 +sc query SunshineService >nul 2>&1 if not errorlevel 1 ( - set "SERVICE_WAS_RUNNING=1" + rem Service exists; check if it's running + sc query SunshineService | find /I "RUNNING" >nul 2>&1 + if not errorlevel 1 ( + set "SERVICE_WAS_RUNNING=1" + sc stop SunshineService >nul 2>&1 + ) + rem Force-kill the service binary so it releases the HID handle quickly. + taskkill /f /im sunshinesvc.exe >nul 2>&1 + timeout /t 1 /nobreak >nul 2>&1 echo Sunshine service stopped. - timeout /t 2 /nobreak 1>nul ) else ( - echo Sunshine service not running, OK. + echo Sunshine service not installed, OK. ) if not exist "%NEFCON%" goto skip_nefcon_uninstall echo Removing all Virtual Mouse devices via nefcon... set "NEFCON_REMOVED=0" +set "NEFCON_MAX_ITERS=20" :uninstall_remove_loop "%NEFCON%" --remove-device-node --hardware-id Root\ZakoVirtualMouse --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da if not errorlevel 1 ( set /a NEFCON_REMOVED+=1 + rem Hard cap to prevent an infinite loop if nefcon reports success without + rem actually removing anything (observed on some nefcon builds). + if !NEFCON_REMOVED! GEQ !NEFCON_MAX_ITERS! ( + echo Reached max remove iterations (!NEFCON_MAX_ITERS!), stopping. + goto after_remove_loop + ) timeout /t 1 /nobreak >nul goto uninstall_remove_loop ) +:after_remove_loop echo Removed !NEFCON_REMOVED! device node(s) via nefcon. echo Uninstalling Virtual Mouse driver... @@ -73,8 +89,16 @@ if exist "%DIST_DIR%" ( echo Virtual Mouse driver uninstalled. -rem Restart Sunshine service if it was running before +rem Restart Sunshine service if it was running before and still exists. +rem In the full uninstaller flow the service has already been deleted by +rem uninstall-service.bat, so this only matters when the script is run +rem standalone (e.g. user-initiated driver reset). if "!SERVICE_WAS_RUNNING!"=="1" ( - echo Restarting Sunshine service... - net start SunshineService >nul 2>&1 + sc query SunshineService >nul 2>&1 + if not errorlevel 1 ( + echo Restarting Sunshine service... + sc start SunshineService >nul 2>&1 + ) ) + +exit /b 0