Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit8f60b4d

Browse files
authored
feat: wire up file sync window (#64)
- Adds `PauseSyncSession` and `ResumeSyncSession`- Adds `SyncSessionViewModel` that wraps `SyncSessionModel` and addsview 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 thefile 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#26Closes#28Closes#29
1 parente3fd7d9 commit8f60b4d

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
usingSystem;
2+
usingSystem.Diagnostics;
23
usingSystem.Threading;
34
usingSystem.Threading.Tasks;
45
usingCoder.Desktop.App.Models;
@@ -73,6 +74,8 @@ public async Task ExitApplication()
7374
{
7475
_handleWindowClosed=false;
7576
Exit();
77+
varsyncController=_services.GetRequiredService<ISyncSessionController>();
78+
awaitsyncController.DisposeAsync();
7679
varrpcController=_services.GetRequiredService<IRpcController>();
7780
// TODO: send a StopRequest if we're connected???
7881
awaitrpcController.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+
#ifDEBUG
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+
varcredentialManagerCts=newCancellationTokenSource(TimeSpan.FromSeconds(15));
94106
varcredentialManager=_services.GetRequiredService<ICredentialManager>();
95-
_=credentialManager.LoadCredentials(CancellationToken.None);
107+
_=credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t=>
108+
{
109+
// TODO: log
110+
#ifDEBUG
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+
varsyncSessionCts=newCancellationTokenSource(TimeSpan.FromSeconds(10));
122+
varsyncSessionController=_services.GetRequiredService<ISyncSessionController>();
123+
_=syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(t=>
124+
{
125+
// TODO: log
126+
#ifDEBUG
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
vartrayWindow=_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
usingSystem.Collections.Generic;
2-
usingSystem.Linq;
32
usingCoder.Desktop.Vpn.Proto;
43

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

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

29-
publicList<Workspace>Workspaces{get;set;}=[];
28+
publicIReadOnlyList<Workspace>Workspaces{get;set;}=[];
3029

31-
publicList<Agent>Agents{get;set;}=[];
30+
publicIReadOnlyList<Agent>Agents{get;set;}=[];
3231

3332
publicRpcModelClone()
3433
{
3534
returnnewRpcModel
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+
usingSystem.Collections.Generic;
2+
3+
namespaceCoder.Desktop.App.Models;
4+
5+
publicenumSyncSessionControllerLifecycle
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+
publicclassSyncSessionControllerStateModel
26+
{
27+
publicSyncSessionControllerLifecycleLifecycle{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+
publicstring?DaemonError{get;init;}
34+
35+
publicrequiredstringDaemonLogFilePath{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+
publicIReadOnlyList<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
usingSystem;
2+
usingSystem.Collections.Generic;
3+
usingSystem.Linq;
24
usingCoder.Desktop.App.Converters;
35
usingCoder.Desktop.MutagenSdk.Proto.Synchronization;
6+
usingCoder.Desktop.MutagenSdk.Proto.Synchronization.Core;
47
usingCoder.Desktop.MutagenSdk.Proto.Url;
58

69
namespaceCoder.Desktop.App.Models;
@@ -48,7 +51,7 @@ public string Description(string linePrefix = "")
4851
publicclassSyncSessionModel
4952
{
5053
publicreadonlystringIdentifier;
51-
publicreadonlystringName;
54+
publicreadonlyDateTimeCreatedAt;
5255

5356
publicreadonlystringAlphaName;
5457
publicreadonlystringAlphaPath;
@@ -62,14 +65,24 @@ public class SyncSessionModel
6265
publicreadonlySyncSessionModelEndpointSizeAlphaSize;
6366
publicreadonlySyncSessionModelEndpointSizeBetaSize;
6467

65-
publicreadonlystring[]Errors=[];
68+
publicreadonlyIReadOnlyList<string>Conflicts;// Conflict descriptions
69+
publicreadonlyulongOmittedConflicts;
70+
publicreadonlyIReadOnlyList<string>Errors;
71+
72+
// If Paused is true, the session can be resumed. If false, the session can
73+
// be paused.
74+
publicboolPaused=>StatusCategoryisSyncSessionStatusCategory.Paused orSyncSessionStatusCategory.Halted;
6675

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

87-
// TODO: remove once we process sessions from the mutagen RPC
88-
publicSyncSessionModel(stringalphaPath,stringbetaName,stringbetaPath,
89-
SyncSessionStatusCategorystatusCategory,
90-
stringstatusString,stringstatusDescription,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=newSyncSessionModelEndpointSize
103-
{
104-
SizeBytes=(ulong)newRandom().Next(0,1000000000),
105-
FileCount=(ulong)newRandom().Next(0,10000),
106-
DirCount=(ulong)newRandom().Next(0,10000),
107-
};
108-
BetaSize=newSyncSessionModelEndpointSize
109-
{
110-
SizeBytes=(ulong)newRandom().Next(0,1000000000),
111-
FileCount=(ulong)newRandom().Next(0,10000),
112-
DirCount=(ulong)newRandom().Next(0,10000),
113-
};
114-
115-
Errors=errors;
116-
}
117-
118100
publicSyncSessionModel(Statestate)
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=newSyncSessionModelEndpointSize
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(varscanProbleminstate.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(varscanProbleminstate.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(vartransitionProbleminstate.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(vartransitionProbleminstate.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
privatestatic(string,string)NameAndPathFromUrl(URLurl)
@@ -251,4 +251,55 @@ private static (string, string) NameAndPathFromUrl(URL url)
251251

252252
return(name,path);
253253
}
254+
255+
privatestaticstringConflictToString(Conflictconflict)
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+
varstr=$"Conflict at path '{conflict.Root}':";
267+
foreach(varchangeinconflict.AlphaChanges)
268+
str+=$"\n (alpha){ChangeToString(change)}";
269+
foreach(varchangeinconflict.BetaChanges)
270+
str+=$"\n (beta){ChangeToString(change)}";
271+
if(friendlyProblem!=null)
272+
str+=$"\n\n{friendlyProblem}";
273+
274+
returnstr;
275+
}
276+
277+
privatestaticstringChangeToString(Changechange)
278+
{
279+
return$"{change.Path} ({EntryToString(change.Old)} ->{EntryToString(change.New)})";
280+
}
281+
282+
privatestaticstringEntryToString(Entry?entry)
283+
{
284+
if(entry==null)return"<non-existent>";
285+
varstr=entry.Kind.ToString();
286+
switch(entry.Kind)
287+
{
288+
caseEntryKind.Directory:
289+
str+=$" ({entry.Contents.Count} entries)";
290+
break;
291+
caseEntryKind.File:
292+
vardigest=BitConverter.ToString(entry.Digest.ToByteArray()).Replace("-","").ToLower();
293+
str+=$" ({digest}, executable:{entry.Executable})";
294+
break;
295+
caseEntryKind.SymbolicLink:
296+
str+=$" (target:{entry.Target})";
297+
break;
298+
caseEntryKind.Problematic:
299+
str+=$" ({entry.Problem})";
300+
break;
301+
}
302+
303+
returnstr;
304+
}
254305
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp