Bump to v3.0.1: move UPnP discovery off the UI thread (fixes hang on Andre's network)
Bug: ticking the "Automatically open my router for incoming connections (UPnP)" box in Preferences could freeze the WinForms message pump until Mono.Nat's NatUtility.StartDiscovery() returned. On Andre's setup it never did — audio kept flowing (audio threads are independent of the UI thread) but the window stopped repainting, the system-tray hotkey stopped responding, and the only way out was Task Manager. Pre-existing latent bug in the v2.1 UPnP code; we just shipped without anyone exercising the path on a problematic network (multiple adapters / VPN / SSDP-swallowing router). Fix: three call sites moved off the UI thread via Task.Run - * MainForm OnShown (startup re-enable from saved AppConfig.UpnpEnabled) * MainForm Preferences applyUpnpEnabled callback (user ticks the box) * MainForm power-resume handler (Refresh() after sleep/wake) RouterPortMapper.Start() returns "immediately" only when StartDiscovery returns quickly; on a slow network it can block synchronously for many seconds. Same is true of Stop()'s socket teardown and Refresh()'s teardown-then-restart sequence. All three are now safely backgrounded. StatusChanged is unaffected - it already fires on the mapper's own thread and the PreferencesDialog handler BeginInvokes back to the UI thread. Live status label updates correctly during the new background discovery. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
af0e7c3fff
commit
5737550453
@@ -20,6 +20,32 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v3.0.1
|
||||
|
||||
Hot-fix for a bug in the "Automatically open my router
|
||||
for incoming connections (UPnP)" tickbox.
|
||||
|
||||
On some network setups (machines with several network
|
||||
adapters, a VPN connected, or a router that doesn't
|
||||
answer the way RemSound's UPnP library expects) ticking
|
||||
that box could freeze RemSound's window — audio kept
|
||||
flowing, but you couldn't open the window again, even
|
||||
from the system tray. The only way out was to end the
|
||||
process from Task Manager.
|
||||
|
||||
The fault was that RemSound was doing the router
|
||||
discovery on the same thread that draws the window, so
|
||||
a slow router (or no router answering at all) would
|
||||
block the window until it finished — which sometimes
|
||||
was never. v3.0.1 moves that work off to a background
|
||||
thread so the window stays responsive while RemSound
|
||||
looks for the router.
|
||||
|
||||
Nothing else has changed from v3.0 — same wire format,
|
||||
same codec list, same everything. If you were already
|
||||
running v3.0 happily, this update fixes a problem you
|
||||
may not have hit; you can install it at your leisure.
|
||||
|
||||
RemSound v3.0
|
||||
|
||||
A big release with two things you'll actually notice:
|
||||
|
||||
@@ -1051,12 +1051,22 @@ public sealed class MainForm : Form
|
||||
|
||||
// Kick off UPnP discovery if the user has the box ticked. Off by default; the
|
||||
// mapper itself coalesces redundant Start() calls so a re-enter via Shown after
|
||||
// a sleep cycle is harmless.
|
||||
// a sleep cycle is harmless. Run on a thread-pool thread because
|
||||
// NatUtility.StartDiscovery() (Mono.Nat 3.0.4) sets up SSDP sockets on every
|
||||
// network interface and CAN BLOCK FOR TENS OF SECONDS, or indefinitely, on
|
||||
// unusual network setups (multiple adapters, VPNs, hostile firewalls, routers
|
||||
// that swallow SSDP). Calling it on the UI thread freezes the WinForms message
|
||||
// pump — Andre's v3.0 hang was this exact pattern. The status label still
|
||||
// updates correctly because StatusChanged fires on the mapper's own thread and
|
||||
// the PreferencesDialog handler BeginInvokes back to the UI thread. 2026-05-23.
|
||||
var startupCfg = AppConfig.Load();
|
||||
if (startupCfg.UpnpEnabled)
|
||||
{
|
||||
try { routerPortMapper.Start(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: start failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
Task.Run(() =>
|
||||
{
|
||||
try { routerPortMapper.Start(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: start failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
});
|
||||
}
|
||||
|
||||
// Startup update check — separate from the periodic timer because users who
|
||||
@@ -1751,17 +1761,28 @@ public sealed class MainForm : Form
|
||||
applyUpnpEnabled: enabled =>
|
||||
{
|
||||
// The persist already happened in the dialog; this callback only flips the
|
||||
// live RouterPortMapper. Start kicks off discovery; Stop politely removes any
|
||||
// existing mapping.
|
||||
// live RouterPortMapper. Start/Stop both run on a thread-pool thread because
|
||||
// NatUtility's discovery + socket teardown CAN BLOCK FOR TENS OF SECONDS, or
|
||||
// indefinitely, on unusual network setups (multiple adapters, VPNs, hostile
|
||||
// firewalls). Doing that on the UI thread here would freeze the
|
||||
// Preferences dialog AND every other UI element until the call returned —
|
||||
// Andre's v3.0 hang was triggered from this exact handler. See the longer
|
||||
// explanation on the startup-time UPnP block in OnShown. 2026-05-23.
|
||||
if (enabled)
|
||||
{
|
||||
try { routerPortMapper.Start(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: start from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
Task.Run(() =>
|
||||
{
|
||||
try { routerPortMapper.Start(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: start from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
try { routerPortMapper.Stop(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: stop from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
Task.Run(() =>
|
||||
{
|
||||
try { routerPortMapper.Stop(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: stop from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
});
|
||||
}
|
||||
},
|
||||
getUpnpSnapshot: () => (routerPortMapper.Status, routerPortMapper.ExternalEndpoint, routerPortMapper.LastError),
|
||||
@@ -3631,11 +3652,18 @@ public sealed class MainForm : Form
|
||||
|
||||
// Re-poke the router. UPnP/NAT-PMP mappings often survive a sleep, but cheap
|
||||
// routers and ISP-supplied combo boxes sometimes drop their NAT table — easier
|
||||
// to just rediscover than to guess. Refresh() is a no-op if UPnP is off.
|
||||
// to just rediscover than to guess. Refresh() is a no-op if UPnP is off. Run on
|
||||
// a thread-pool thread for the same reason as the other UPnP entry points: the
|
||||
// NatUtility teardown + restart inside Refresh() can block for tens of seconds
|
||||
// on unusual networks, and we're on the UI thread during the resume handler.
|
||||
// 2026-05-23.
|
||||
if (AppConfig.Load().UpnpEnabled)
|
||||
{
|
||||
try { routerPortMapper.Refresh(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: refresh-on-resume failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
Task.Run(() =>
|
||||
{
|
||||
try { routerPortMapper.Refresh(); }
|
||||
catch (Exception ex) { logFile.Event($"upnp: refresh-on-resume failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>3.0.0</Version>
|
||||
<Version>3.0.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user