Skip to content

Commit 0ef9bfa

Browse files
Fix concurrent update checks and show download-in-progress toast
- Add SemaphoreSlim to serialize Velopack operations across instances - Show "already downloading" toast when manual update clicked during download - Fix beta/alpha channel not fetching pre-releases (prerelease: true)
1 parent ec7438e commit 0ef9bfa

4 files changed

Lines changed: 204 additions & 167 deletions

File tree

src/TEdit/Properties/Language.Designer.cs

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/TEdit/Properties/Language.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2951,6 +2951,9 @@ Would you like to enable error reporting?</value>
29512951
<data name="update_checking" xml:space="preserve">
29522952
<value>Checking for updates...</value>
29532953
</data>
2954+
<data name="update_downloading" xml:space="preserve">
2955+
<value>An update is already downloading, please wait...</value>
2956+
</data>
29542957
<data name="update_check_failed" xml:space="preserve">
29552958
<value>Update check failed.</value>
29562959
</data>
Lines changed: 183 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,183 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Threading.Tasks;
4-
using TEdit.ViewModel;
5-
using Velopack;
6-
using Velopack.Sources;
7-
8-
namespace TEdit.Services;
9-
10-
public class UpdateService
11-
{
12-
private const string GithubRepo = "https://github.com/TEdit/Terraria-Map-Editor";
13-
14-
private readonly List<UpdateManager> _managers = new();
15-
private UpdateManager _lastDownloadManager;
16-
private UpdateInfo _lastDownloadUpdate;
17-
18-
/// <summary>
19-
/// Creates update managers for all channels the user's tier includes.
20-
/// Beta receives beta + stable. Alpha receives alpha + beta + stable.
21-
/// </summary>
22-
public UpdateService(UpdateChannel channel = UpdateChannel.Stable)
23-
{
24-
// Stable is always included — every tier gets stable releases.
25-
// CI packs with --channel stable/beta/alpha so ExplicitChannel must match.
26-
_managers.Add(CreateManager("stable"));
27-
28-
if (channel >= UpdateChannel.Beta)
29-
_managers.Add(CreateManager("beta", prerelease: true));
30-
31-
if (channel >= UpdateChannel.Alpha)
32-
_managers.Add(CreateManager("alpha", prerelease: true));
33-
}
34-
35-
private static UpdateManager CreateManager(string channelName, bool prerelease = false)
36-
{
37-
var source = new GithubSource(GithubRepo, null, prerelease);
38-
var options = new UpdateOptions
39-
{
40-
ExplicitChannel = channelName,
41-
};
42-
43-
return new UpdateManager(source, options);
44-
}
45-
46-
public bool IsInstalled => _managers[0].IsInstalled;
47-
48-
/// <summary>
49-
/// Checks all eligible channels and returns the manager + update info for the newest available version.
50-
/// </summary>
51-
private async Task<(UpdateManager manager, UpdateInfo update)?> FindBestUpdateAsync()
52-
{
53-
UpdateManager bestManager = null;
54-
UpdateInfo bestUpdate = null;
55-
56-
foreach (var mgr in _managers)
57-
{
58-
try
59-
{
60-
var update = await mgr.CheckForUpdatesAsync();
61-
if (update == null) continue;
62-
63-
if (bestUpdate == null || update.TargetFullRelease.Version > bestUpdate.TargetFullRelease.Version)
64-
{
65-
bestManager = mgr;
66-
bestUpdate = update;
67-
}
68-
}
69-
catch (Exception ex)
70-
{
71-
ErrorLogging.LogDebug($"[Update] Channel check failed: {ex.Message}");
72-
}
73-
}
74-
75-
if (bestManager != null && bestUpdate != null)
76-
return (bestManager, bestUpdate);
77-
78-
return null;
79-
}
80-
81-
public async Task<bool> CheckOnlyAsync()
82-
{
83-
if (!IsInstalled)
84-
{
85-
ErrorLogging.LogInfo("[Update] Not a Velopack install — skipping update check.");
86-
return false;
87-
}
88-
89-
try
90-
{
91-
var result = await FindBestUpdateAsync();
92-
if (result == null)
93-
{
94-
ErrorLogging.LogInfo("[Update] No updates available.");
95-
return false;
96-
}
97-
98-
ErrorLogging.LogInfo($"[Update] Update available: {result.Value.update.TargetFullRelease.Version}");
99-
return true;
100-
}
101-
catch (Exception ex)
102-
{
103-
ErrorLogging.LogWarn($"[Update] Check failed: {ex.Message}");
104-
ErrorLogging.LogException(ex);
105-
return false;
106-
}
107-
}
108-
109-
public async Task<bool> CheckAndDownloadAsync()
110-
{
111-
if (!IsInstalled)
112-
{
113-
ErrorLogging.LogInfo("[Update] Not a Velopack install — skipping update check.");
114-
return false;
115-
}
116-
117-
try
118-
{
119-
var result = await FindBestUpdateAsync();
120-
if (result == null)
121-
{
122-
ErrorLogging.LogInfo("[Update] No updates available.");
123-
return false;
124-
}
125-
126-
var (mgr, update) = result.Value;
127-
ErrorLogging.LogInfo($"[Update] Update available: {update.TargetFullRelease.Version}");
128-
await mgr.DownloadUpdatesAsync(update);
129-
ErrorLogging.LogInfo("[Update] Update downloaded. Waiting for user to restart.");
130-
131-
// Cache for ApplyAndRestart so it doesn't need to re-fetch
132-
_lastDownloadManager = mgr;
133-
_lastDownloadUpdate = update;
134-
135-
return true;
136-
}
137-
catch (Exception ex)
138-
{
139-
ErrorLogging.LogWarn($"[Update] Check failed: {ex.Message}");
140-
ErrorLogging.LogException(ex);
141-
return false;
142-
}
143-
}
144-
145-
public void ApplyAndRestart()
146-
{
147-
if (!IsInstalled) return;
148-
149-
try
150-
{
151-
if (_lastDownloadManager != null && _lastDownloadUpdate != null)
152-
{
153-
_lastDownloadManager.ApplyUpdatesAndRestart(_lastDownloadUpdate);
154-
return;
155-
}
156-
157-
// Fallback: no cached result (shouldn't happen in normal flow)
158-
ErrorLogging.LogWarn("[Update] No cached download result — cannot apply update.");
159-
}
160-
catch (Exception ex)
161-
{
162-
ErrorLogging.LogWarn($"[Update] Apply failed: {ex.Message}");
163-
ErrorLogging.LogException(ex);
164-
}
165-
}
166-
}
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using TEdit.ViewModel;
6+
using Velopack;
7+
using Velopack.Sources;
8+
9+
namespace TEdit.Services;
10+
11+
public class UpdateService
12+
{
13+
private const string GithubRepo = "https://github.com/TEdit/Terraria-Map-Editor";
14+
15+
// Serialize all Velopack operations to avoid lock file contention
16+
private static readonly SemaphoreSlim _updateLock = new(1, 1);
17+
18+
/// <summary>True while any instance is downloading an update.</summary>
19+
public static bool IsUpdateInProgress => _updateLock.CurrentCount == 0;
20+
21+
private readonly List<UpdateManager> _managers = new();
22+
private UpdateManager _lastDownloadManager;
23+
private UpdateInfo _lastDownloadUpdate;
24+
25+
/// <summary>
26+
/// Creates update managers for all channels the user's tier includes.
27+
/// Beta receives beta + stable. Alpha receives alpha + beta + stable.
28+
/// </summary>
29+
public UpdateService(UpdateChannel channel = UpdateChannel.Stable)
30+
{
31+
// Stable is always included — every tier gets stable releases.
32+
// CI packs with --channel stable/beta/alpha so ExplicitChannel must match.
33+
_managers.Add(CreateManager("stable"));
34+
35+
if (channel >= UpdateChannel.Beta)
36+
_managers.Add(CreateManager("beta", prerelease: true));
37+
38+
if (channel >= UpdateChannel.Alpha)
39+
_managers.Add(CreateManager("alpha", prerelease: true));
40+
}
41+
42+
private static UpdateManager CreateManager(string channelName, bool prerelease = false)
43+
{
44+
var source = new GithubSource(GithubRepo, null, prerelease);
45+
var options = new UpdateOptions
46+
{
47+
ExplicitChannel = channelName,
48+
};
49+
50+
return new UpdateManager(source, options);
51+
}
52+
53+
public bool IsInstalled => _managers[0].IsInstalled;
54+
55+
/// <summary>
56+
/// Checks all eligible channels and returns the manager + update info for the newest available version.
57+
/// </summary>
58+
private async Task<(UpdateManager manager, UpdateInfo update)?> FindBestUpdateAsync()
59+
{
60+
UpdateManager bestManager = null;
61+
UpdateInfo bestUpdate = null;
62+
63+
foreach (var mgr in _managers)
64+
{
65+
try
66+
{
67+
var update = await mgr.CheckForUpdatesAsync();
68+
if (update == null) continue;
69+
70+
if (bestUpdate == null || update.TargetFullRelease.Version > bestUpdate.TargetFullRelease.Version)
71+
{
72+
bestManager = mgr;
73+
bestUpdate = update;
74+
}
75+
}
76+
catch (Exception ex)
77+
{
78+
ErrorLogging.LogDebug($"[Update] Channel check failed: {ex.Message}");
79+
}
80+
}
81+
82+
if (bestManager != null && bestUpdate != null)
83+
return (bestManager, bestUpdate);
84+
85+
return null;
86+
}
87+
88+
public async Task<bool> CheckOnlyAsync()
89+
{
90+
if (!IsInstalled)
91+
{
92+
ErrorLogging.LogInfo("[Update] Not a Velopack install — skipping update check.");
93+
return false;
94+
}
95+
96+
await _updateLock.WaitAsync();
97+
try
98+
{
99+
var result = await FindBestUpdateAsync();
100+
if (result == null)
101+
{
102+
ErrorLogging.LogInfo("[Update] No updates available.");
103+
return false;
104+
}
105+
106+
ErrorLogging.LogInfo($"[Update] Update available: {result.Value.update.TargetFullRelease.Version}");
107+
return true;
108+
}
109+
catch (Exception ex)
110+
{
111+
ErrorLogging.LogWarn($"[Update] Check failed: {ex.Message}");
112+
ErrorLogging.LogException(ex);
113+
return false;
114+
}
115+
finally
116+
{
117+
_updateLock.Release();
118+
}
119+
}
120+
121+
public async Task<bool> CheckAndDownloadAsync()
122+
{
123+
if (!IsInstalled)
124+
{
125+
ErrorLogging.LogInfo("[Update] Not a Velopack install — skipping update check.");
126+
return false;
127+
}
128+
129+
await _updateLock.WaitAsync();
130+
try
131+
{
132+
var result = await FindBestUpdateAsync();
133+
if (result == null)
134+
{
135+
ErrorLogging.LogInfo("[Update] No updates available.");
136+
return false;
137+
}
138+
139+
var (mgr, update) = result.Value;
140+
ErrorLogging.LogInfo($"[Update] Update available: {update.TargetFullRelease.Version}");
141+
await mgr.DownloadUpdatesAsync(update);
142+
ErrorLogging.LogInfo("[Update] Update downloaded. Waiting for user to restart.");
143+
144+
// Cache for ApplyAndRestart so it doesn't need to re-fetch
145+
_lastDownloadManager = mgr;
146+
_lastDownloadUpdate = update;
147+
148+
return true;
149+
}
150+
catch (Exception ex)
151+
{
152+
ErrorLogging.LogWarn($"[Update] Check failed: {ex.Message}");
153+
ErrorLogging.LogException(ex);
154+
return false;
155+
}
156+
finally
157+
{
158+
_updateLock.Release();
159+
}
160+
}
161+
162+
public void ApplyAndRestart()
163+
{
164+
if (!IsInstalled) return;
165+
166+
try
167+
{
168+
if (_lastDownloadManager != null && _lastDownloadUpdate != null)
169+
{
170+
_lastDownloadManager.ApplyUpdatesAndRestart(_lastDownloadUpdate);
171+
return;
172+
}
173+
174+
// Fallback: no cached result (shouldn't happen in normal flow)
175+
ErrorLogging.LogWarn("[Update] No cached download result — cannot apply update.");
176+
}
177+
catch (Exception ex)
178+
{
179+
ErrorLogging.LogWarn($"[Update] Apply failed: {ex.Message}");
180+
ErrorLogging.LogException(ex);
181+
}
182+
}
183+
}

src/TEdit/ViewModel/WorldViewModel.Commands.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ private async Task Update()
262262
return;
263263
}
264264

265+
if (Services.UpdateService.IsUpdateInProgress)
266+
{
267+
App.SnackbarService.ShowInfo(
268+
Properties.Language.update_downloading,
269+
Properties.Language.update_title);
270+
return;
271+
}
272+
265273
App.SnackbarService.ShowInfo(
266274
Properties.Language.update_checking,
267275
Properties.Language.update_title);

0 commit comments

Comments
 (0)