Skip to content

Commit 2335aba

Browse files
committed
Merge remote-tracking branch 'vibe/vibe'
2 parents dd11133 + 8e66abe commit 2335aba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3448
-1568
lines changed

cmake/compile_definitions/windows.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ set(PLATFORM_TARGET_FILES
6767
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_helper_request_helpers.cpp"
6868
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_helper_integration.h"
6969
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_helper_integration.cpp"
70+
"${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_display_cleanup.h"
71+
"${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_display_cleanup.cpp"
7072
"${CMAKE_SOURCE_DIR}/src/platform/windows/hotkey_manager.h"
7173
"${CMAKE_SOURCE_DIR}/src/platform/windows/hotkey_manager.cpp"
7274
"${CMAKE_SOURCE_DIR}/src/platform/windows/playnite_ipc.h"

cmake/packaging/windows_wix.cmake

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ set(CPACK_WIX_EXTRA_SOURCES
4747
"${CMAKE_SOURCE_DIR}/packaging/windows/wix/custom_actions.wxs"
4848
)
4949

50-
# Use a repo-owned WiX template so MajorUpgrade behavior is explicit and stable.
50+
# Override CPack's default WiX template to control MajorUpgrade scheduling.
51+
# We schedule after InstallValidate to avoid RemoveExistingProducts 2613
52+
# failures in transactional upgrade flows.
5153
set(CPACK_WIX_TEMPLATE "${CMAKE_SOURCE_DIR}/packaging/windows/wix/WIX.template.in")
5254

5355

packaging/windows/bootstrapper/VibeshineInstaller.cs

Lines changed: 295 additions & 21 deletions
Large diffs are not rendered by default.

packaging/windows/wix/WIX.template.in

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,14 @@
1919
<Package InstallerVersion="301" Compressed="yes" InstallScope="$(var.CPACK_WIX_INSTALL_SCOPE)"/>
2020
<?endif?>
2121

22+
<?ifndef CPACK_WIX_CAB_PER_COMPONENT?>
2223
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes"/>
24+
<?endif?>
2325

24-
<!--
25-
Keep major upgrades for cross-line ProductCode changes, but:
26-
- allow downgrades
27-
- avoid aggressive early removal
28-
- do not force same-version major upgrades
29-
-->
3026
<MajorUpgrade
31-
Schedule="afterInstallExecute"
32-
AllowDowngrades="yes"/>
27+
Schedule="afterInstallValidate"
28+
AllowSameVersionUpgrades="yes"
29+
DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
3330

3431
<WixVariable Id="WixUILicenseRtf" Value="$(var.CPACK_WIX_LICENSE_RTF)"/>
3532
<Property Id="WIXUI_INSTALLDIR" Value="INSTALL_ROOT"/>

packaging/windows/wix/custom_actions.wxs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<!-- VBScripts for legacy Sunshine handling -->
2929
<Binary Id="AskUninstallLegacySunshineVbs" SourceFile="$(sys.SOURCEFILEDIR)ask_uninstall_legacy_sunshine.vbs"/>
3030
<Binary Id="WaitForLegacyUninstallVbs" SourceFile="$(sys.SOURCEFILEDIR)wait_for_legacy_uninstall.vbs"/>
31+
<Binary Id="RemoveConflictingProductsVbs" SourceFile="$(sys.SOURCEFILEDIR)remove_conflicting_products.vbs"/>
3132

3233
</Fragment>
3334

@@ -130,6 +131,7 @@
130131

131132
<!-- Uninstall-time actions (deferred, elevated) -->
132133
<CustomAction Id="RestoreNvPrefsUndo" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="check" Impersonate="no" />
134+
<CustomAction Id="RemoveConflictingProducts" BinaryKey="RemoveConflictingProductsVbs" VBScriptCall="RemoveConflictingProducts" Execute="deferred" Return="check" />
133135
<!-- Quiet stop for repair/basic UI: proactively stop service when UI is too low for prompts -->
134136
<CustomAction Id="StopSvcQuietImmediate" BinaryKey="WixCA" DllEntry="CAQuietExec" Return="ignore" />
135137
<CustomAction Id="StopSvcQuiet" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="ignore" Impersonate="no" />
@@ -152,7 +154,6 @@
152154
<CustomAction Id="SetMigrateConfig"
153155
Property="MigrateConfig"
154156
Value="&quot;[SystemFolder]cmd.exe&quot; /C &quot;&quot;[INSTALL_ROOT]scripts\migrate-config.bat&quot;&quot;"/>
155-
156157
<CustomAction Id="SetInstallSudovda"
157158
Property="InstallSudovda"
158159
Value="&quot;[SystemFolder]WindowsPowerShell\v1.0\powershell.exe&quot; -NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File &quot;[INSTALL_ROOT]drivers\sudovda\install.ps1&quot;"/>
@@ -202,7 +203,7 @@
202203
<!-- Immediate setter for quiet start service -->
203204
<CustomAction Id="SetStartSvcQuiet"
204205
Property="StartSvcQuiet"
205-
Value="&quot;[SystemFolder]WindowsPowerShell\v1.0\powershell.exe&quot; -ExecutionPolicy Bypass -NoProfile -Command &quot;&amp; { foreach ($name in @('ApolloService','VibeshineService','SunshineService')) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue; if ($svc) { if ($svc.Status -ne 'Running') { Start-Service -Name $name -ErrorAction SilentlyContinue; $svc.WaitForStatus('Running','00:00:20') } break } } }&quot;"/>
206+
Value="&quot;[SystemFolder]WindowsPowerShell\v1.0\powershell.exe&quot; -ExecutionPolicy Bypass -NoProfile -Command &quot;&amp; { foreach ($name in @('ApolloService','SunshineService','VibeshineService')) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue; if ($svc) { if ($svc.Status -ne 'Running') { Start-Service -Name $name -ErrorAction SilentlyContinue; $svc.WaitForStatus('Running','00:00:20') } } } }&quot;"/>
206207
<!-- Immediate setters for quiet process kill and service removal on manual uninstall -->
207208
<CustomAction Id="SetKillProcsQuietImmediate"
208209
Property="KillProcsQuietImmediate"

packaging/windows/wix/patch_custom_actions.wxs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
<ComponentRef Id="ArpCustomUninstallEntry"/>
3030
</Feature>
3131
<InstallExecuteSequence>
32-
<!-- Prune web assets folder before installing files to avoid stale hashed assets on non-reinstall install paths -->
33-
<Custom Action="SetPruneWebDir" Before="InstallFiles">NOT REMOVE AND NOT REINSTALL</Custom>
34-
<Custom Action="PruneWebDir" After="SetPruneWebDir">NOT REMOVE AND NOT REINSTALL</Custom>
32+
<!-- Remove conflicting products before installation starts. -->
33+
<Custom Action="RemoveConflictingProducts" After="InstallInitialize">NOT Installed AND NOT REMOVE</Custom>
34+
35+
<!-- Prune web assets folder before installing files to avoid stale hashed assets on upgrade/repair -->
36+
<Custom Action="SetPruneWebDir" Before="InstallFiles">NOT REMOVE</Custom>
37+
<Custom Action="PruneWebDir" After="SetPruneWebDir">NOT REMOVE</Custom>
3538

3639
<!-- Pre-validate stop/kill when UI can't prompt -->
3740
<Custom Action="SetKillProcsQuietImmediate" Before="InstallValidate">(Installed AND NOT REMOVE AND UILevel &lt; 5) OR (REMOVE = "ALL" AND NOT UPGRADINGPRODUCTCODE)</Custom>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
Const HKEY_CURRENT_USER = &H80000001
2+
Const HKEY_LOCAL_MACHINE = &H80000002
3+
4+
Function RemoveConflictingProducts()
5+
On Error Resume Next
6+
7+
Dim shell, reg, processed
8+
Set shell = CreateObject("WScript.Shell")
9+
Set reg = GetObject("winmgmts:\\.\root\default:StdRegProv")
10+
Set processed = CreateObject("Scripting.Dictionary")
11+
12+
If Err.Number <> 0 Then
13+
LogMessage "RemoveConflictingProducts: failed to initialize objects: " & Err.Description
14+
RemoveConflictingProducts = 3
15+
Exit Function
16+
End If
17+
18+
Dim hives(1), roots(1)
19+
hives(0) = HKEY_LOCAL_MACHINE
20+
hives(1) = HKEY_CURRENT_USER
21+
roots(0) = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
22+
roots(1) = "SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
23+
24+
Dim hive, root, enumResult, subKeys, i
25+
Dim foundAny
26+
foundAny = False
27+
28+
For Each hive In hives
29+
For Each root In roots
30+
enumResult = reg.EnumKey(hive, root, subKeys)
31+
If enumResult = 0 And IsArray(subKeys) Then
32+
For i = 0 To UBound(subKeys)
33+
Dim subKeyName, fullPath, displayName
34+
subKeyName = CStr(subKeys(i))
35+
fullPath = root & "\" & subKeyName
36+
displayName = ReadStringValue(reg, hive, fullPath, "DisplayName")
37+
38+
If IsTargetProduct(displayName) Then
39+
foundAny = True
40+
If Not UninstallConflict(reg, shell, hive, fullPath, subKeyName, displayName, processed) Then
41+
RemoveConflictingProducts = 3
42+
Exit Function
43+
End If
44+
End If
45+
Next
46+
End If
47+
Next
48+
Next
49+
50+
If Not foundAny Then
51+
LogMessage "RemoveConflictingProducts: no conflicting products detected."
52+
End If
53+
54+
RemoveConflictingProducts = 1
55+
End Function
56+
57+
Private Function UninstallConflict(reg, shell, hive, fullPath, subKeyName, displayName, processed)
58+
UninstallConflict = False
59+
60+
Dim windowsInstaller, quietCmd, uninstallCmd, commandToRun, productCode
61+
windowsInstaller = ReadDwordValue(reg, hive, fullPath, "WindowsInstaller")
62+
quietCmd = Trim(ReadStringValue(reg, hive, fullPath, "QuietUninstallString"))
63+
uninstallCmd = Trim(ReadStringValue(reg, hive, fullPath, "UninstallString"))
64+
productCode = Trim(subKeyName)
65+
66+
If windowsInstaller = 1 And LooksLikeProductCode(productCode) Then
67+
commandToRun = BuildMsiUninstallCommand(shell, productCode)
68+
Else
69+
If Len(quietCmd) > 0 Then
70+
commandToRun = quietCmd
71+
Else
72+
commandToRun = uninstallCmd
73+
End If
74+
75+
If Len(commandToRun) = 0 Then
76+
LogMessage "RemoveConflictingProducts: no uninstall command found for " & displayName
77+
Exit Function
78+
End If
79+
80+
If Len(quietCmd) = 0 And (Not CommandHasQuietSwitch(commandToRun)) And (Not CommandTargetsMsiexec(commandToRun)) Then
81+
commandToRun = commandToRun & " /S"
82+
End If
83+
End If
84+
85+
Dim dedupeKey
86+
dedupeKey = UCase(productCode & "|" & commandToRun)
87+
If processed.Exists(dedupeKey) Then
88+
UninstallConflict = True
89+
Exit Function
90+
End If
91+
processed.Add dedupeKey, True
92+
93+
LogMessage "RemoveConflictingProducts: uninstalling " & displayName & " using: " & commandToRun
94+
95+
Dim exitCode
96+
exitCode = shell.Run(commandToRun, 0, True)
97+
LogMessage "RemoveConflictingProducts: uninstall exit code for " & displayName & ": " & CStr(exitCode)
98+
99+
If Not IsAcceptedExitCode(exitCode) Then
100+
LogMessage "RemoveConflictingProducts: uninstall failed for " & displayName
101+
Exit Function
102+
End If
103+
104+
UninstallConflict = True
105+
End Function
106+
107+
Private Function IsTargetProduct(displayName)
108+
Dim nameUpper
109+
nameUpper = UCase(Trim(displayName))
110+
IsTargetProduct = (Left(nameUpper, 8) = "SUNSHINE") _
111+
Or (Left(nameUpper, 6) = "APOLLO") _
112+
Or (Left(nameUpper, 9) = "VIBESHINE")
113+
End Function
114+
115+
Private Function BuildMsiUninstallCommand(shell, productCode)
116+
Dim msiPath
117+
msiPath = shell.ExpandEnvironmentStrings("%WINDIR%\System32\msiexec.exe")
118+
BuildMsiUninstallCommand = Chr(34) & msiPath & Chr(34) _
119+
& " /x " & productCode _
120+
& " /qn /norestart REBOOT=ReallySuppress SUPPRESSMSGBOXES=1"
121+
End Function
122+
123+
Private Function LooksLikeProductCode(value)
124+
Dim trimmed
125+
trimmed = Trim(value)
126+
LooksLikeProductCode = (Len(trimmed) = 38) _
127+
And (Left(trimmed, 1) = "{") _
128+
And (Right(trimmed, 1) = "}")
129+
End Function
130+
131+
Private Function CommandHasQuietSwitch(commandLine)
132+
Dim upper
133+
upper = " " & UCase(commandLine) & " "
134+
CommandHasQuietSwitch = (InStr(upper, " /S ") > 0) _
135+
Or (InStr(upper, " /SILENT ") > 0) _
136+
Or (InStr(upper, " /VERYSILENT ") > 0) _
137+
Or (InStr(upper, " /QUIET ") > 0) _
138+
Or (InStr(upper, " /QN ") > 0)
139+
End Function
140+
141+
Private Function CommandTargetsMsiexec(commandLine)
142+
Dim executable
143+
executable = LCase(GetExecutablePath(commandLine))
144+
If Right(executable, 12) = "\msiexec.exe" Then
145+
CommandTargetsMsiexec = True
146+
Exit Function
147+
End If
148+
If executable = "msiexec.exe" Or executable = "msiexec" Then
149+
CommandTargetsMsiexec = True
150+
Exit Function
151+
End If
152+
CommandTargetsMsiexec = False
153+
End Function
154+
155+
Private Function GetExecutablePath(commandLine)
156+
Dim value, closingQuote, firstSpace
157+
value = Trim(commandLine)
158+
If Len(value) = 0 Then
159+
GetExecutablePath = ""
160+
Exit Function
161+
End If
162+
163+
If Left(value, 1) = Chr(34) Then
164+
closingQuote = InStr(2, value, Chr(34))
165+
If closingQuote > 2 Then
166+
GetExecutablePath = Mid(value, 2, closingQuote - 2)
167+
Exit Function
168+
End If
169+
End If
170+
171+
firstSpace = InStr(value, " ")
172+
If firstSpace <= 0 Then
173+
GetExecutablePath = value
174+
Else
175+
GetExecutablePath = Left(value, firstSpace - 1)
176+
End If
177+
End Function
178+
179+
Private Function IsAcceptedExitCode(code)
180+
IsAcceptedExitCode = (code = 0) Or (code = 3010) Or (code = 1605)
181+
End Function
182+
183+
Private Function ReadStringValue(reg, hive, path, valueName)
184+
On Error Resume Next
185+
Dim value, rc
186+
value = ""
187+
rc = reg.GetStringValue(hive, path, valueName, value)
188+
If rc <> 0 Or IsNull(value) Then
189+
value = ""
190+
rc = reg.GetExpandedStringValue(hive, path, valueName, value)
191+
End If
192+
If IsNull(value) Then
193+
value = ""
194+
End If
195+
ReadStringValue = CStr(value)
196+
End Function
197+
198+
Private Function ReadDwordValue(reg, hive, path, valueName)
199+
On Error Resume Next
200+
Dim value, rc
201+
value = 0
202+
rc = reg.GetDWORDValue(hive, path, valueName, value)
203+
If rc <> 0 Or IsNull(value) Then
204+
value = 0
205+
End If
206+
ReadDwordValue = CLng(value)
207+
End Function
208+
209+
Private Sub LogMessage(message)
210+
On Error Resume Next
211+
Session.Log message
212+
End Sub

src/config.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,7 @@ namespace config {
767767
video_t::dd_t::hdr_request_override_e::automatic, // hdr_request_override
768768
3s, // config_revert_delay
769769
{}, // config_revert_on_disconnect
770+
0, // paused_virtual_display_timeout_secs
770771
false, // always_restore_from_golden
771772
0, // snapshot_restore_hotkey
772773
#ifdef _WIN32
@@ -1557,6 +1558,11 @@ namespace config {
15571558
}
15581559
}
15591560
bool_f(vars, "dd_config_revert_on_disconnect", video.dd.config_revert_on_disconnect);
1561+
{
1562+
int value = 0;
1563+
int_between_f(vars, "dd_paused_virtual_display_timeout_secs", value, {0, std::numeric_limits<int>::max()});
1564+
video.dd.paused_virtual_display_timeout_secs = std::max(0, value);
1565+
}
15601566
bool_f(vars, "dd_always_restore_from_golden", video.dd.always_restore_from_golden);
15611567
bool_f(vars, "dd_activate_virtual_display", video.dd.activate_virtual_display);
15621568
generic_f(vars, "dd_snapshot_exclude_devices", video.dd.snapshot_exclude_devices, dd::snapshot_exclude_devices_from_view);
@@ -2232,10 +2238,11 @@ namespace config {
22322238
const auto prev_dd_manual_refresh_rate = video.dd.manual_refresh_rate;
22332239
const auto prev_dd_revert_delay = video.dd.config_revert_delay;
22342240
const auto prev_dd_revert_on_disconnect = video.dd.config_revert_on_disconnect;
2241+
const auto prev_dd_paused_virtual_display_timeout_secs = video.dd.paused_virtual_display_timeout_secs;
22352242
const auto prev_dd_activate_virtual_display = video.dd.activate_virtual_display;
22362243
const auto prev_dd_snapshot_exclude_devices = video.dd.snapshot_exclude_devices;
22372244
const auto prev_dd_dummy_plug = video.dd.wa.dummy_plug_hdr10;
2238-
const auto prev_double_refreshrate = video.double_refreshrate;
2245+
const auto prev_dd_double_refreshrate = video.double_refreshrate;
22392246

22402247
auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str()));
22412248
for (const auto &[name, value] : command_line_overrides) {
@@ -2276,13 +2283,14 @@ namespace config {
22762283
(prev_dd_hdr_opt != video.dd.hdr_option) ||
22772284
(prev_dd_hdr_req_override != video.dd.hdr_request_override) ||
22782285
(prev_dd_manual_resolution != video.dd.manual_resolution) ||
2279-
(prev_dd_manual_refresh_rate != video.dd.manual_refresh_rate) ||
2280-
(prev_dd_revert_delay != video.dd.config_revert_delay) ||
2281-
(prev_dd_revert_on_disconnect != video.dd.config_revert_on_disconnect) ||
2282-
(prev_dd_activate_virtual_display != video.dd.activate_virtual_display) ||
2283-
(prev_dd_snapshot_exclude_devices != video.dd.snapshot_exclude_devices) ||
2284-
(prev_dd_dummy_plug != video.dd.wa.dummy_plug_hdr10) ||
2285-
(prev_double_refreshrate != video.double_refreshrate);
2286+
(prev_dd_manual_refresh_rate != video.dd.manual_refresh_rate) ||
2287+
(prev_dd_revert_delay != video.dd.config_revert_delay) ||
2288+
(prev_dd_revert_on_disconnect != video.dd.config_revert_on_disconnect) ||
2289+
(prev_dd_paused_virtual_display_timeout_secs != video.dd.paused_virtual_display_timeout_secs) ||
2290+
(prev_dd_activate_virtual_display != video.dd.activate_virtual_display) ||
2291+
(prev_dd_snapshot_exclude_devices != video.dd.snapshot_exclude_devices) ||
2292+
(prev_dd_dummy_plug != video.dd.wa.dummy_plug_hdr10) ||
2293+
(prev_dd_double_refreshrate != video.double_refreshrate);
22862294

22872295
// If any DD settings changed and there are no active sessions, revert to clear cached state
22882296
if (dd_config_changed && rtsp_stream::session_count() == 0 && runtime_overrides.empty()) {

src/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ namespace config {
169169
hdr_request_override_e hdr_request_override;
170170
std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists).
171171
bool config_revert_on_disconnect; ///< Specify whether to revert display configuration on client disconnect.
172+
int paused_virtual_display_timeout_secs; ///< Optional timeout to cleanup virtual display while stream is paused (0 disables).
172173
bool always_restore_from_golden; ///< When true, prefer golden snapshot over session snapshots during restore (reduces stuck virtual screens).
173174
int snapshot_restore_hotkey; ///< Virtual-key code for restore hotkey (0 disables).
174175
std::uint32_t snapshot_restore_hotkey_modifiers; ///< Modifier flags for the restore hotkey.

src/confighttp.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
#include "platform/common.h"
5555
#include "webrtc_stream.h"
5656

57+
#ifdef _WIN32
58+
#include "platform/windows/virtual_display_cleanup.h"
59+
#endif
60+
5761
#include <nlohmann/json.hpp>
5862
#if defined(_WIN32)
5963
#include "platform/windows/misc.h"
@@ -2455,6 +2459,16 @@ namespace confighttp {
24552459

24562460
BOOST_LOG(debug) << "WebRTC: creating session";
24572461
if (auto error = webrtc_stream::ensure_capture_started(options)) {
2462+
#ifdef _WIN32
2463+
// Lifecycle gap: if capture start fails after a virtual display was created/applied but
2464+
// before a session exists, ensure we don't leave the virtual display behind.
2465+
if (rtsp_stream::session_count() == 0 && !webrtc_stream::has_active_sessions()) {
2466+
(void) platf::virtual_display_cleanup::run(
2467+
"webrtc_session_start_failed",
2468+
config::video.dd.config_revert_on_disconnect
2469+
);
2470+
}
2471+
#endif
24582472
bad_request(response, request, error->c_str());
24592473
return;
24602474
}

0 commit comments

Comments
 (0)