Release v4.8: fix rare crash from endpoint thrash on a dual-address peer (#16)
The same singer hit a rare crash: their transmitter (COMP3) was reachable at two addresses at once — the VPN address 10.8.0.1 they chose and that machine's wireless 192.168.3.245 — and the discovery-driven endpoint-follow ping-ponged the connection between the two (the log shows four moves in 58 ms) right where the process died with no shutdown line, no managed exception, no dialog: a hard crash from the receiver audio-session teardown/rebuild churn the thrash caused. Fix: the follow loop now never moves off an endpoint that's still answering heartbeats, only follows once the current one has been unreachable for a sustained grace period (6 s), only to an address that is itself answering, and never more than once per cooldown (15 s) — so it can't thrash, and a peer reached on a working address stays put (honours the singer's "just stay on 10.8.0.1"). New endpointUnreachableSinceUtc + lastEndpointMoveUtc state; genuine DHCP/network moves are still followed a few seconds later. Also: a global crash handler (Program.WriteCrashReport on AppDomain.UnhandledException + TaskScheduler.UnobservedTaskException) writes a crash-*.txt into the logs folder, so a future "RemSound just vanished" report leaves a stack behind. Removed a stale doc comment left over from the v4.7 adoption removal. Manual gains a "RemSound closed unexpectedly" troubleshooting entry. Version 4.8; About + RELEASE_NOTES updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
64d5cb5a86
commit
0df2a576fc
@@ -1245,6 +1245,10 @@ Most likely your ASIO latency target is below the network's real-world jitter le
|
|||||||
|
|
||||||
RemSound keeps trying the address you connected to, so when a peer comes back at the same address — the usual case after a reboot — the sound returns on its own within a few seconds, with nothing for you to do. If the peer comes back at a _different_ address (a new DHCP lease, say), reconnect to it: pick it again from **Discovered peers** , or enter its new address with **Add peer by IP** (Connectivity tab, Alt+A). If the sound still doesn't return, the peer is genuinely unreachable — off, asleep, or a firewall is blocking the path.
|
RemSound keeps trying the address you connected to, so when a peer comes back at the same address — the usual case after a reboot — the sound returns on its own within a few seconds, with nothing for you to do. If the peer comes back at a _different_ address (a new DHCP lease, say), reconnect to it: pick it again from **Discovered peers** , or enter its new address with **Add peer by IP** (Connectivity tab, Alt+A). If the sound still doesn't return, the peer is genuinely unreachable — off, asleep, or a firewall is blocking the path.
|
||||||
|
|
||||||
|
### RemSound closed unexpectedly
|
||||||
|
|
||||||
|
RemSound is normally very stable, but if it ever closes on its own, it writes a small crash file into your logs folder (the RemSound folder → **user settings and logs** → **logs** , named `crash-` followed by the date and time). There's nothing you need to do with it — but if it happens, sending that file along with your report records what went wrong, so it can be tracked down and fixed.
|
||||||
|
|
||||||
### One side says “unreachable” even though sound is flowing
|
### One side says “unreachable” even though sound is flowing
|
||||||
|
|
||||||
The health check-ins use the same channel as the audio, so if the sound gets through, the check-ins should too. If one side shows “unreachable” while the sound plays fine, make sure both computers are running the same version of RemSound — an older version on either end can speak a slightly different check-in language.
|
The health check-ins use the same channel as the audio, so if the sound gets through, the check-ins should too. If one side shows “unreachable” while the sound plays fine, make sure both computers are running the same version of RemSound — an older version on either end can speak a slightly different check-in language.
|
||||||
|
|||||||
+11
-23
@@ -1,45 +1,33 @@
|
|||||||
# RemSound v4.7
|
# RemSound v4.8
|
||||||
|
|
||||||
Sound comes back on its own after a reboot — plus a few screen-reader and status improvements.
|
A rare crash fixed, and crash reports for next time.
|
||||||
|
|
||||||
## Sound returns by itself after a reboot
|
## Fixed: a rare unexpected close
|
||||||
|
|
||||||
If RemSound started up before your network or VPN was ready, it could lock onto the wrong machine on your network and sit silent until you closed and reopened it. RemSound now keeps trying the peer you actually chose and connects the moment it answers — no manual reconnect needed.
|
If a peer was reachable at two addresses at once — for example a transmitter on a VPN *and* the local network at the same time — RemSound could rapidly flip the connection back and forth between the two addresses. Usually harmless, but a fast enough flip could tear the audio down and rebuild it quickly enough to make RemSound close unexpectedly: you'd find the sound had stopped and the program gone, and relaunching brought it straight back.
|
||||||
|
|
||||||
(With thanks to the singer who reported this and captured the log that pinned it down.)
|
RemSound now settles on whichever address is actually working and stays there, only following a genuine move once the current address has properly stopped responding — so the flipping, and the crash it could cause, are gone.
|
||||||
|
|
||||||
## Hearing the status, improved
|
(With thanks again to the singer who reported it and sent the log.)
|
||||||
|
|
||||||
The **Speak the RemSound status information** hotkey (for screen-reader users) now:
|
## New: a crash file if anything ever does go wrong
|
||||||
|
|
||||||
- reads the status **a line at a time** — peers, ping, uptime, rates, totals — so each part lands as its own short phrase;
|
If RemSound ever closes unexpectedly, it now writes a small `crash-*.txt` file into your logs folder (RemSound folder → **user settings and logs** → **logs**). There's nothing you need to do with it — but if you ever hit a problem, sending that file in turns a "closed for no reason" into something that can actually be pinned down.
|
||||||
- shows big data totals in **gigabytes** once they pass a gigabyte;
|
|
||||||
- copies the whole status to the **clipboard** on a quick **double press**, so you can paste it to someone.
|
|
||||||
|
|
||||||
The status line now also shows **how much CPU and memory RemSound itself is using**.
|
|
||||||
|
|
||||||
## If RemSound won't start: Install Scripts
|
|
||||||
|
|
||||||
RemSound needs the .NET 10 desktop runtime. On the rare machine that doesn't have it, RemSound can't start to install it itself — so there's now an **Install Scripts** folder next to the program containing a one-click installer (a `.cmd` and a `.ps1`) that fetches and installs the runtime for you.
|
|
||||||
|
|
||||||
## Other fixes
|
|
||||||
|
|
||||||
- The **"what's new"** notes no longer appear a second time after an update that didn't finish.
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
**v4.7 talks to v3.3 through v4.6 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
**v4.8 talks to v3.3 through v4.7 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
1. Download `RemSound-v4.7.zip` from this release.
|
1. Download `RemSound-v4.8.zip` from this release.
|
||||||
2. Close RemSound.
|
2. Close RemSound.
|
||||||
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your settings, profiles, logs or recordings.
|
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your settings, profiles, logs or recordings.
|
||||||
4. Run `RemSound.exe`.
|
4. Run `RemSound.exe`.
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
**From v3.6 or newer:** Help → Check for updates installs v4.7 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
**From v3.6 or newer:** Help → Check for updates installs v4.8 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
||||||
|
|
||||||
**From v1.9–v3.5:** Check for updates works, but uses your current version's older updater for this one hop. If auto-update has been failing on your machine, install by hand using the steps above.
|
**From v1.9–v3.5:** Check for updates works, but uses your current version's older updater for this one hop. If auto-update has been failing on your machine, install by hand using the steps above.
|
||||||
|
|
||||||
|
|||||||
@@ -1294,6 +1294,9 @@ RemSound.exe --connect 192.168.1.42
|
|||||||
<h3>A peer rebooted or changed address and the sound didn't come back</h3>
|
<h3>A peer rebooted or changed address and the sound didn't come back</h3>
|
||||||
<p>RemSound keeps trying the address you connected to, so when a peer comes back at the same address — the usual case after a reboot — the sound returns on its own within a few seconds, with nothing for you to do. If the peer comes back at a <em>different</em> address (a new DHCP lease, say), reconnect to it: pick it again from <strong>Discovered peers</strong>, or enter its new address with <strong>Add peer by IP</strong> (Connectivity tab, Alt+A). If the sound still doesn't return, the peer is genuinely unreachable — off, asleep, or a firewall is blocking the path.</p>
|
<p>RemSound keeps trying the address you connected to, so when a peer comes back at the same address — the usual case after a reboot — the sound returns on its own within a few seconds, with nothing for you to do. If the peer comes back at a <em>different</em> address (a new DHCP lease, say), reconnect to it: pick it again from <strong>Discovered peers</strong>, or enter its new address with <strong>Add peer by IP</strong> (Connectivity tab, Alt+A). If the sound still doesn't return, the peer is genuinely unreachable — off, asleep, or a firewall is blocking the path.</p>
|
||||||
|
|
||||||
|
<h3>RemSound closed unexpectedly</h3>
|
||||||
|
<p>RemSound is normally very stable, but if it ever closes on its own, it writes a small crash file into your logs folder (the RemSound folder → <strong>user settings and logs</strong> → <strong>logs</strong>, named <code>crash-</code> followed by the date and time). There's nothing you need to do with it — but if it happens, sending that file along with your report records what went wrong, so it can be tracked down and fixed.</p>
|
||||||
|
|
||||||
<h3>One side says “unreachable” even though sound is flowing</h3>
|
<h3>One side says “unreachable” even though sound is flowing</h3>
|
||||||
<p>The health check-ins use the same channel as the audio, so if the sound gets through, the check-ins should too. If one side shows “unreachable” while the sound plays fine, make sure both computers are running the same version of RemSound — an older version on either end can speak a slightly different check-in language.</p>
|
<p>The health check-ins use the same channel as the audio, so if the sound gets through, the check-ins should too. If one side shows “unreachable” while the sound plays fine, make sure both computers are running the same version of RemSound — an older version on either end can speak a slightly different check-in language.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,26 @@ internal sealed class AboutDialog : Form
|
|||||||
/// updates" path.</summary>
|
/// updates" path.</summary>
|
||||||
private const string ReleaseNotes =
|
private const string ReleaseNotes =
|
||||||
"""
|
"""
|
||||||
|
RemSound v4.8
|
||||||
|
|
||||||
|
A rare crash fixed, and crash reports for next
|
||||||
|
time.
|
||||||
|
|
||||||
|
If a peer was reachable at two addresses at once
|
||||||
|
— for example over a VPN and the local network at
|
||||||
|
the same time — RemSound could rapidly flip the
|
||||||
|
connection between the two, and in a fast enough
|
||||||
|
flip it could close unexpectedly. It now settles
|
||||||
|
on whichever address is working and stays there,
|
||||||
|
so that flipping (and the crash it could cause)
|
||||||
|
is gone.
|
||||||
|
|
||||||
|
And if RemSound ever does close unexpectedly, it
|
||||||
|
now writes a small crash file into your logs
|
||||||
|
folder. There's nothing for you to do with it,
|
||||||
|
but it means a problem that used to leave no
|
||||||
|
trace can now be sent in and pinned down.
|
||||||
|
|
||||||
RemSound v4.7
|
RemSound v4.7
|
||||||
|
|
||||||
Sound now comes back on its own after a reboot.
|
Sound now comes back on its own after a reboot.
|
||||||
|
|||||||
@@ -382,6 +382,17 @@ public sealed class MainForm : Form
|
|||||||
// discovery has temporarily lost sight of them ("Foo (192.168.1.5) — offline").
|
// discovery has temporarily lost sight of them ("Foo (192.168.1.5) — offline").
|
||||||
private readonly Dictionary<Guid, string> selectedPeerLabels = [];
|
private readonly Dictionary<Guid, string> selectedPeerLabels = [];
|
||||||
|
|
||||||
|
// Anti-thrash state for the discovery-driven endpoint follow (see the peer-rebuild loop). A peer
|
||||||
|
// reachable at two addresses at once (a VPN address AND a LAN address, say) announces from both,
|
||||||
|
// and discovery reports whichever it heard last; following that blindly made the tracked endpoint
|
||||||
|
// ping-pong between the two, and a fast ping-pong tore the receiver's audio session down and back
|
||||||
|
// up quickly enough to crash the app (#16). A follow now needs the current endpoint to have been
|
||||||
|
// unreachable for a sustained spell and can't fire more than once per cooldown. Keyed by peer id.
|
||||||
|
private readonly Dictionary<Guid, DateTime> endpointUnreachableSinceUtc = [];
|
||||||
|
private readonly Dictionary<Guid, DateTime> lastEndpointMoveUtc = [];
|
||||||
|
private static readonly TimeSpan EndpointMoveUnreachableGrace = TimeSpan.FromSeconds(6);
|
||||||
|
private static readonly TimeSpan EndpointMoveCooldown = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
private readonly Dictionary<CheckedListBox, int> lastFocusedListIndices = [];
|
private readonly Dictionary<CheckedListBox, int> lastFocusedListIndices = [];
|
||||||
|
|
||||||
private readonly System.Windows.Forms.Timer statusTimer = new() { Interval = 1000 };
|
private readonly System.Windows.Forms.Timer statusTimer = new() { Interval = 1000 };
|
||||||
@@ -5260,30 +5271,48 @@ public sealed class MainForm : Form
|
|||||||
knownPeers.Clear();
|
knownPeers.Clear();
|
||||||
foreach (var peer in byEndpoint.Values) knownPeers[peer.InstanceId] = peer;
|
foreach (var peer in byEndpoint.Values) knownPeers[peer.InstanceId] = peer;
|
||||||
|
|
||||||
// If a selected peer's announced address changed (DHCP renewal, network switch),
|
// If a selected peer's announced address changed (DHCP renewal, network switch), follow it to
|
||||||
// update the cached endpoint so the sender follows the new IP — BUT only when the
|
// the new address — but conservatively. A peer reachable at two addresses at once (e.g. a VPN
|
||||||
// endpoint we're currently using has actually stopped working.
|
// address AND a LAN address) announces from both, and discovery reports whichever it heard
|
||||||
//
|
// last. Following that blindly made the tracked endpoint ping-pong between the two; because
|
||||||
// Why the guard: a peer reachable at two addresses at once — e.g. a VPN address AND a
|
// this one endpoint feeds the audio sender, the heartbeat AND the receiver's allow-list, the
|
||||||
// LAN address — announces itself from both, and discovery reports whichever it heard
|
// churn was heard as crackle (Tech Singer's Win7-over-VPN report, 2026-05-31) and, when it
|
||||||
// last. Blindly following that made the tracked endpoint ping-pong between the two
|
// thrashed fast enough, tore the receiver's audio session down and back up quickly enough to
|
||||||
// every couple of seconds. Because this one endpoint feeds the audio sender, the
|
// crash the app (#16, same singer). So: never move off an address that's still answering
|
||||||
// heartbeat, AND the receiver's allow-list, the ping-pong meant a chunk of audio was
|
// heartbeats; only follow once the current one has been unreachable for a sustained spell,
|
||||||
// aimed at — or accepted only from — an address that doesn't actually reach the peer,
|
// only TO an address that is itself answering, and never more than once per cooldown. Net
|
||||||
// heard as heavy crackle (Tech Singer's Win7-over-VPN report, 2026-05-31). Keeping the
|
// effect — a peer you reach on a working address stays put; a genuine move (the old address
|
||||||
// endpoint pinned while it's still passing heartbeats stops the thrash. A genuine move
|
// really went away) is still followed a few seconds later.
|
||||||
// (DHCP renewal, Wi-Fi switch) makes the old endpoint go unreachable first, at which
|
var nowUtc = DateTime.UtcNow;
|
||||||
// point the guard lets the move through.
|
|
||||||
foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList())
|
foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList())
|
||||||
{
|
{
|
||||||
if (!knownPeers.TryGetValue(id, out var peer)) continue;
|
if (!knownPeers.TryGetValue(id, out var peer)) continue;
|
||||||
var newEndpoint = new IPEndPoint(peer.Address, peer.AudioPort);
|
|
||||||
if (!newEndpoint.Equals(oldEndpoint) && !IsEndpointHeartbeatHealthy(oldEndpoint))
|
|
||||||
{
|
|
||||||
selectedPeerEndpoints[id] = newEndpoint;
|
|
||||||
logFile.Event($"peer {peer.Name} endpoint moved {oldEndpoint} -> {newEndpoint} (old endpoint not healthy)");
|
|
||||||
}
|
|
||||||
selectedPeerLabels[id] = peer.Name;
|
selectedPeerLabels[id] = peer.Name;
|
||||||
|
|
||||||
|
var newEndpoint = new IPEndPoint(peer.Address, peer.AudioPort);
|
||||||
|
// Same address, or the one we're on is still healthy: nothing to do — and reset the
|
||||||
|
// unreachable-since clock so a brief future blip starts counting from zero.
|
||||||
|
if (newEndpoint.Equals(oldEndpoint) || IsEndpointHeartbeatHealthy(oldEndpoint))
|
||||||
|
{
|
||||||
|
endpointUnreachableSinceUtc.Remove(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The current endpoint isn't answering. Start (or read) its unreachable-since clock, and
|
||||||
|
// don't act on the very first unhealthy tick — wait out the grace period.
|
||||||
|
if (!endpointUnreachableSinceUtc.TryGetValue(id, out var downSince))
|
||||||
|
{
|
||||||
|
endpointUnreachableSinceUtc[id] = nowUtc;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nowUtc - downSince < EndpointMoveUnreachableGrace) continue; // not down long enough yet
|
||||||
|
if (!IsEndpointHeartbeatHealthy(newEndpoint)) continue; // don't chase a dead address
|
||||||
|
if (lastEndpointMoveUtc.TryGetValue(id, out var lastMove)
|
||||||
|
&& nowUtc - lastMove < EndpointMoveCooldown) continue; // anti-thrash cooldown
|
||||||
|
|
||||||
|
selectedPeerEndpoints[id] = newEndpoint;
|
||||||
|
lastEndpointMoveUtc[id] = nowUtc;
|
||||||
|
endpointUnreachableSinceUtc.Remove(id);
|
||||||
|
logFile.Event($"peer {peer.Name} endpoint moved {oldEndpoint} -> {newEndpoint} (old endpoint unreachable {(int)(nowUtc - downSince).TotalSeconds}s)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoints may have moved (DHCP/announcement-update path above) or selections may have
|
// Endpoints may have moved (DHCP/announcement-update path above) or selections may have
|
||||||
@@ -5349,19 +5378,6 @@ public sealed class MainForm : Form
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stale-address recovery. When exactly one tracked peer has gone Unreachable (its
|
|
||||||
/// resolved address — often a stale DNS answer — has no host behind it) and exactly one
|
|
||||||
/// OTHER address is actively heartbeat-pinging us, that address is almost certainly the
|
|
||||||
/// same peer at its real location. Re-point the audio sender, heartbeat tracking and the
|
|
||||||
/// receiver allow-list at the live address.
|
|
||||||
///
|
|
||||||
/// Deliberately conservative — it fires only on the unambiguous one-unreachable-and-one-
|
|
||||||
/// live case, only for private-range (RFC1918) live addresses (so a relay's public source
|
|
||||||
/// address can never hijack the sender), and with a 10 s cooldown so it can't thrash. The
|
|
||||||
/// messier multi-peer case is left for the user to sort out by hand. Runs once per second
|
|
||||||
/// from the status ticker. 2026-05-15.
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wipes the rolling max-gap window and pushes <see cref="lastSourceChangeUtc"/> forward,
|
/// Wipes the rolling max-gap window and pushes <see cref="lastSourceChangeUtc"/> forward,
|
||||||
/// so the next continuous auto-tune tick has nothing to react to. Called whenever a user
|
/// so the next continuous auto-tune tick has nothing to react to. Called whenever a user
|
||||||
|
|||||||
@@ -12,6 +12,27 @@ internal static class Program
|
|||||||
// window. volatile for cross-thread visibility; RestoreFromTray marshals to the UI thread.
|
// window. volatile for cross-thread visibility; RestoreFromTray marshals to the UI thread.
|
||||||
private static volatile MainForm? activeMainForm;
|
private static volatile MainForm? activeMainForm;
|
||||||
|
|
||||||
|
// Writes an otherwise-fatal exception to a timestamped crash file in the logs folder, so a
|
||||||
|
// "RemSound just disappeared, no dialog" report (#16) leaves a stack behind to diagnose instead
|
||||||
|
// of nothing. Best-effort and self-contained — a crash handler must never throw.
|
||||||
|
private static void WriteCrashReport(string source, Exception? ex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = AppConfig.LogsDirectory;
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var path = Path.Combine(dir, $"crash-{DateTime.Now:yyyyMMdd-HHmmss-fff}.txt");
|
||||||
|
var report =
|
||||||
|
$"RemSound crash report{Environment.NewLine}" +
|
||||||
|
$"Version: {typeof(Program).Assembly.GetName().Version}{Environment.NewLine}" +
|
||||||
|
$"Time: {DateTime.Now:O}{Environment.NewLine}" +
|
||||||
|
$"Source: {source}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
|
(ex?.ToString() ?? "(no exception object)");
|
||||||
|
File.WriteAllText(path, report);
|
||||||
|
}
|
||||||
|
catch { /* a crash handler must never throw */ }
|
||||||
|
}
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
@@ -25,6 +46,16 @@ internal static class Program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture otherwise-fatal background-thread exceptions to a crash file, so a "RemSound just
|
||||||
|
// vanished with no dialog" report (#16) leaves a stack behind instead of nothing. Best-effort.
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||||
|
WriteCrashReport("AppDomain.UnhandledException", e.ExceptionObject as Exception);
|
||||||
|
TaskScheduler.UnobservedTaskException += (_, e) =>
|
||||||
|
{
|
||||||
|
WriteCrashReport("TaskScheduler.UnobservedTaskException", e.Exception);
|
||||||
|
e.SetObserved();
|
||||||
|
};
|
||||||
|
|
||||||
// --config-dir <folder> (test / portable isolation): redirect ALL user state - config,
|
// --config-dir <folder> (test / portable isolation): redirect ALL user state - config,
|
||||||
// profiles, logs, cue sounds - to an explicit folder for THIS process only. Applied first,
|
// profiles, logs, cue sounds - to an explicit folder for THIS process only. Applied first,
|
||||||
// before the layout migration and sound consolidation below read or write the default
|
// before the layout migration and sound consolidation below read or write the default
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
tag_name on the latest GitHub release; bump it on every public release. The
|
tag_name on the latest GitHub release; bump it on every public release. The
|
||||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||||
is what the About dialog and the updater both read. -->
|
is what the About dialog and the updater both read. -->
|
||||||
<Version>4.7</Version>
|
<Version>4.8</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user