Skip to content

Commit 8f60b4d

Browse files
authored
feat: wire up file sync window (#64)
- Adds `PauseSyncSession` and `ResumeSyncSession` - Adds `SyncSessionViewModel` that wraps `SyncSessionModel` and adds view methods (as you cannot access the parent context if you're in a `ItemsRepeater` apparently) - Wires up Initialize, List, Pause, Resume, Terminate and Create in the file sync UI ## TODO: - Prevent the app from loading until mutagen finishes initializing (either successfully or not) (in a different PR) - Add reinitialization logic to mutagen controller (in a different PR) Closes #26 Closes #28 Closes #29
1 parent e3fd7d9 commit 8f60b4d

17 files changed

+1295
-359
lines changed

App/App.xaml.cs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Coder.Desktop.App.Models;
@@ -73,6 +74,8 @@ public async Task ExitApplication()
7374
{
7475
_handleWindowClosed = false;
7576
Exit();
77+
var syncController = _services.GetRequiredService<ISyncSessionController>();
78+
await syncController.DisposeAsync();
7679
var rpcController = _services.GetRequiredService<IRpcController>();
7780
// TODO: send a StopRequest if we're connected???
7881
await rpcController.DisposeAsync();
@@ -86,20 +89,52 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
8689
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
8790
// Passing in a CT with no cancellation is desired here, because
8891
// the named pipe open will block until the pipe comes up.
89-
_ = rpcController.Reconnect(CancellationToken.None);
92+
// TODO: log
93+
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
94+
{
95+
#if DEBUG
96+
if (t.Exception != null)
97+
{
98+
Debug.WriteLine(t.Exception);
99+
Debugger.Break();
100+
}
101+
#endif
102+
});
90103

91-
// Load the credentials in the background. Even though we pass a CT
92-
// with no cancellation, the method itself will impose a timeout on the
93-
// HTTP portion.
104+
// Load the credentials in the background.
105+
var credentialManagerCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
94106
var credentialManager = _services.GetRequiredService<ICredentialManager>();
95-
_ = credentialManager.LoadCredentials(CancellationToken.None);
107+
_ = credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t =>
108+
{
109+
// TODO: log
110+
#if DEBUG
111+
if (t.Exception != null)
112+
{
113+
Debug.WriteLine(t.Exception);
114+
Debugger.Break();
115+
}
116+
#endif
117+
credentialManagerCts.Dispose();
118+
}, CancellationToken.None);
119+
120+
// Initialize file sync.
121+
var syncSessionCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
122+
var syncSessionController = _services.GetRequiredService<ISyncSessionController>();
123+
_ = syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(t =>
124+
{
125+
// TODO: log
126+
#if DEBUG
127+
if (t.IsCanceled || t.Exception != null) Debugger.Break();
128+
#endif
129+
syncSessionCts.Dispose();
130+
}, CancellationToken.None);
96131

97132
// Prevent the TrayWindow from closing, just hide it.
98133
var trayWindow = _services.GetRequiredService<TrayWindow>();
99-
trayWindow.Closed += (sender, args) =>
134+
trayWindow.Closed += (_, closedArgs) =>
100135
{
101136
if (!_handleWindowClosed) return;
102-
args.Handled = true;
137+
closedArgs.Handled = true;
103138
trayWindow.AppWindow.Hide();
104139
};
105140
}

App/Models/RpcModel.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections.Generic;
2-
using System.Linq;
32
using Coder.Desktop.Vpn.Proto;
43

54
namespace Coder.Desktop.App.Models;
@@ -26,18 +25,18 @@ public class RpcModel
2625

2726
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
2827

29-
public List<Workspace> Workspaces { get; set; } = [];
28+
public IReadOnlyList<Workspace> Workspaces { get; set; } = [];
3029

31-
public List<Agent> Agents { get; set; } = [];
30+
public IReadOnlyList<Agent> Agents { get; set; } = [];
3231

3332
public RpcModel Clone()
3433
{
3534
return new RpcModel
3635
{
3736
RpcLifecycle = RpcLifecycle,
3837
VpnLifecycle = VpnLifecycle,
39-
Workspaces = Workspaces.ToList(),
40-
Agents = Agents.ToList(),
38+
Workspaces = Workspaces,
39+
Agents = Agents,
4140
};
4241
}
4342
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Generic;
2+
3+
namespace Coder.Desktop.App.Models;
4+
5+
public enum SyncSessionControllerLifecycle
6+
{
7+
// Uninitialized means that the daemon has not been started yet. This can
8+
// be resolved by calling RefreshState (or any other RPC method
9+
// successfully).
10+
Uninitialized,
11+
12+
// Stopped means that the daemon is not running. This could be because:
13+
// - It was never started (pre-Initialize)
14+
// - It was stopped due to no sync sessions (post-Initialize, post-operation)
15+
// - The last start attempt failed (DaemonError will be set)
16+
// - The last daemon process crashed (DaemonError will be set)
17+
Stopped,
18+
19+
// Running is the normal state where the daemon is running and managing
20+
// sync sessions. This is only set after a successful start (including
21+
// being able to connect to the daemon).
22+
Running,
23+
}
24+
25+
public class SyncSessionControllerStateModel
26+
{
27+
public SyncSessionControllerLifecycle Lifecycle { get; init; } = SyncSessionControllerLifecycle.Stopped;
28+
29+
/// <summary>
30+
/// May be set when Lifecycle is Stopped to signify that the daemon failed
31+
/// to start or unexpectedly crashed.
32+
/// </summary>
33+
public string? DaemonError { get; init; }
34+
35+
public required string DaemonLogFilePath { get; init; }
36+
37+
/// <summary>
38+
/// This contains the last known state of all sync sessions. Sync sessions
39+
/// are periodically refreshed if the daemon is running. This list is
40+
/// sorted by creation time.
41+
/// </summary>
42+
public IReadOnlyList<SyncSessionModel> SyncSessions { get; init; } = [];
43+
}

App/Models/SyncSessionModel.cs

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using Coder.Desktop.App.Converters;
35
using Coder.Desktop.MutagenSdk.Proto.Synchronization;
6+
using Coder.Desktop.MutagenSdk.Proto.Synchronization.Core;
47
using Coder.Desktop.MutagenSdk.Proto.Url;
58

69
namespace Coder.Desktop.App.Models;
@@ -48,7 +51,7 @@ public string Description(string linePrefix = "")
4851
public class SyncSessionModel
4952
{
5053
public readonly string Identifier;
51-
public readonly string Name;
54+
public readonly DateTime CreatedAt;
5255

5356
public readonly string AlphaName;
5457
public readonly string AlphaPath;
@@ -62,14 +65,24 @@ public class SyncSessionModel
6265
public readonly SyncSessionModelEndpointSize AlphaSize;
6366
public readonly SyncSessionModelEndpointSize BetaSize;
6467

65-
public readonly string[] Errors = [];
68+
public readonly IReadOnlyList<string> Conflicts; // Conflict descriptions
69+
public readonly ulong OmittedConflicts;
70+
public readonly IReadOnlyList<string> Errors;
71+
72+
// If Paused is true, the session can be resumed. If false, the session can
73+
// be paused.
74+
public bool Paused => StatusCategory is SyncSessionStatusCategory.Paused or SyncSessionStatusCategory.Halted;
6675

6776
public string StatusDetails
6877
{
6978
get
7079
{
71-
var str = $"{StatusString} ({StatusCategory})\n\n{StatusDescription}";
72-
foreach (var err in Errors) str += $"\n\n{err}";
80+
var str = StatusString;
81+
if (StatusCategory.ToString() != StatusString) str += $" ({StatusCategory})";
82+
str += $"\n\n{StatusDescription}";
83+
foreach (var err in Errors) str += $"\n\n-----\n\n{err}";
84+
foreach (var conflict in Conflicts) str += $"\n\n-----\n\n{conflict}";
85+
if (OmittedConflicts > 0) str += $"\n\n-----\n\n{OmittedConflicts:N0} conflicts omitted";
7386
return str;
7487
}
7588
}
@@ -84,41 +97,10 @@ public string SizeDetails
8497
}
8598
}
8699

87-
// TODO: remove once we process sessions from the mutagen RPC
88-
public SyncSessionModel(string alphaPath, string betaName, string betaPath,
89-
SyncSessionStatusCategory statusCategory,
90-
string statusString, string statusDescription, string[] errors)
91-
{
92-
Identifier = "TODO";
93-
Name = "TODO";
94-
95-
AlphaName = "Local";
96-
AlphaPath = alphaPath;
97-
BetaName = betaName;
98-
BetaPath = betaPath;
99-
StatusCategory = statusCategory;
100-
StatusString = statusString;
101-
StatusDescription = statusDescription;
102-
AlphaSize = new SyncSessionModelEndpointSize
103-
{
104-
SizeBytes = (ulong)new Random().Next(0, 1000000000),
105-
FileCount = (ulong)new Random().Next(0, 10000),
106-
DirCount = (ulong)new Random().Next(0, 10000),
107-
};
108-
BetaSize = new SyncSessionModelEndpointSize
109-
{
110-
SizeBytes = (ulong)new Random().Next(0, 1000000000),
111-
FileCount = (ulong)new Random().Next(0, 10000),
112-
DirCount = (ulong)new Random().Next(0, 10000),
113-
};
114-
115-
Errors = errors;
116-
}
117-
118100
public SyncSessionModel(State state)
119101
{
120102
Identifier = state.Session.Identifier;
121-
Name = state.Session.Name;
103+
CreatedAt = state.Session.CreationTime.ToDateTime();
122104

123105
(AlphaName, AlphaPath) = NameAndPathFromUrl(state.Session.Alpha);
124106
(BetaName, BetaPath) = NameAndPathFromUrl(state.Session.Beta);
@@ -220,6 +202,9 @@ public SyncSessionModel(State state)
220202
StatusDescription = "The session has conflicts that need to be resolved.";
221203
}
222204

205+
Conflicts = state.Conflicts.Select(ConflictToString).ToList();
206+
OmittedConflicts = state.ExcludedConflicts;
207+
223208
AlphaSize = new SyncSessionModelEndpointSize
224209
{
225210
SizeBytes = state.AlphaState.TotalFileSize,
@@ -235,9 +220,24 @@ public SyncSessionModel(State state)
235220
SymlinkCount = state.BetaState.SymbolicLinks,
236221
};
237222

238-
// TODO: accumulate errors, there seems to be multiple fields they can
239-
// come from
240-
if (!string.IsNullOrWhiteSpace(state.LastError)) Errors = [state.LastError];
223+
List<string> errors = [];
224+
if (!string.IsNullOrWhiteSpace(state.LastError)) errors.Add($"Last error:\n {state.LastError}");
225+
// TODO: scan problems + transition problems + omissions should probably be fields
226+
foreach (var scanProblem in state.AlphaState.ScanProblems) errors.Add($"Alpha scan problem: {scanProblem}");
227+
if (state.AlphaState.ExcludedScanProblems > 0)
228+
errors.Add($"Alpha scan problems omitted: {state.AlphaState.ExcludedScanProblems}");
229+
foreach (var scanProblem in state.AlphaState.ScanProblems) errors.Add($"Beta scan problem: {scanProblem}");
230+
if (state.BetaState.ExcludedScanProblems > 0)
231+
errors.Add($"Beta scan problems omitted: {state.BetaState.ExcludedScanProblems}");
232+
foreach (var transitionProblem in state.AlphaState.TransitionProblems)
233+
errors.Add($"Alpha transition problem: {transitionProblem}");
234+
if (state.AlphaState.ExcludedTransitionProblems > 0)
235+
errors.Add($"Alpha transition problems omitted: {state.AlphaState.ExcludedTransitionProblems}");
236+
foreach (var transitionProblem in state.AlphaState.TransitionProblems)
237+
errors.Add($"Beta transition problem: {transitionProblem}");
238+
if (state.BetaState.ExcludedTransitionProblems > 0)
239+
errors.Add($"Beta transition problems omitted: {state.BetaState.ExcludedTransitionProblems}");
240+
Errors = errors;
241241
}
242242

243243
private static (string, string) NameAndPathFromUrl(URL url)
@@ -251,4 +251,55 @@ private static (string, string) NameAndPathFromUrl(URL url)
251251

252252
return (name, path);
253253
}
254+
255+
private static string ConflictToString(Conflict conflict)
256+
{
257+
string? friendlyProblem = null;
258+
if (conflict.AlphaChanges.Count == 1 && conflict.BetaChanges.Count == 1 &&
259+
conflict.AlphaChanges[0].Old == null &&
260+
conflict.BetaChanges[0].Old == null &&
261+
conflict.AlphaChanges[0].New != null &&
262+
conflict.BetaChanges[0].New != null)
263+
friendlyProblem =
264+
"An entry was created on both endpoints and they do not match. You can resolve this conflict by deleting one of the entries on either side.";
265+
266+
var str = $"Conflict at path '{conflict.Root}':";
267+
foreach (var change in conflict.AlphaChanges)
268+
str += $"\n (alpha) {ChangeToString(change)}";
269+
foreach (var change in conflict.BetaChanges)
270+
str += $"\n (beta) {ChangeToString(change)}";
271+
if (friendlyProblem != null)
272+
str += $"\n\n{friendlyProblem}";
273+
274+
return str;
275+
}
276+
277+
private static string ChangeToString(Change change)
278+
{
279+
return $"{change.Path} ({EntryToString(change.Old)} -> {EntryToString(change.New)})";
280+
}
281+
282+
private static string EntryToString(Entry? entry)
283+
{
284+
if (entry == null) return "<non-existent>";
285+
var str = entry.Kind.ToString();
286+
switch (entry.Kind)
287+
{
288+
case EntryKind.Directory:
289+
str += $" ({entry.Contents.Count} entries)";
290+
break;
291+
case EntryKind.File:
292+
var digest = BitConverter.ToString(entry.Digest.ToByteArray()).Replace("-", "").ToLower();
293+
str += $" ({digest}, executable: {entry.Executable})";
294+
break;
295+
case EntryKind.SymbolicLink:
296+
str += $" (target: {entry.Target})";
297+
break;
298+
case EntryKind.Problematic:
299+
str += $" ({entry.Problem})";
300+
break;
301+
}
302+
303+
return str;
304+
}
254305
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy