diff --git a/App/Services/MutagenController.cs b/App/Services/MutagenController.cs index dd489df..3a68962 100644 --- a/App/Services/MutagenController.cs +++ b/App/Services/MutagenController.cs @@ -85,7 +85,7 @@ public interface ISyncSessionController : IAsyncDisposable /// </summary> Task<SyncSessionControllerStateModel> RefreshState(CancellationToken ct = default); - Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, CancellationToken ct = default); + Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string> progressCallback, CancellationToken ct = default); Task<SyncSessionModel> PauseSyncSession(string identifier, CancellationToken ct = default); Task<SyncSessionModel> ResumeSyncSession(string identifier, CancellationToken ct = default); Task TerminateSyncSession(string identifier, CancellationToken ct = default); @@ -200,12 +200,15 @@ public async Task<SyncSessionControllerStateModel> RefreshState(CancellationToke return state; } - public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, CancellationToken ct = default) + public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string>? progressCallback = null, CancellationToken ct = default) { using var _ = await _lock.LockAsync(ct); var client = await EnsureDaemon(ct); await using var prompter = await Prompter.Create(client, true, ct); + if (progressCallback != null) + prompter.OnProgress += (_, progress) => progressCallback(progress); + var createRes = await client.Synchronization.CreateAsync(new CreateRequest { Prompter = prompter.Identifier, @@ -603,6 +606,8 @@ private async Task StopDaemon(CancellationToken ct) private class Prompter : IAsyncDisposable { + public event EventHandler<string>? OnProgress; + private readonly AsyncDuplexStreamingCall<HostRequest, HostResponse> _dup; private readonly CancellationTokenSource _cts; private readonly Task _handleRequestsTask; @@ -684,6 +689,9 @@ private async Task HandleRequests(CancellationToken ct) if (response.Message == null) throw new InvalidOperationException("Prompting.Host response stream returned a null message"); + if (!response.IsPrompt) + OnProgress?.Invoke(this, response.Message); + // Currently we only reply to SSH fingerprint messages with // "yes" and send an empty reply for everything else. var reply = ""; diff --git a/App/ViewModels/FileSyncListViewModel.cs b/App/ViewModels/FileSyncListViewModel.cs index 7fdd881..d01338c 100644 --- a/App/ViewModels/FileSyncListViewModel.cs +++ b/App/ViewModels/FileSyncListViewModel.cs @@ -67,6 +67,9 @@ public partial class FileSyncListViewModel : ObservableObject public partial string NewSessionRemotePath { get; set; } = ""; // TODO: NewSessionRemotePathDialogOpen for remote path + [ObservableProperty] + public partial string NewSessionStatus { get; set; } = ""; + public bool NewSessionCreateEnabled { get @@ -187,6 +190,7 @@ private void ClearNewForm() NewSessionLocalPath = ""; NewSessionRemoteHost = ""; NewSessionRemotePath = ""; + NewSessionStatus = ""; } [RelayCommand] @@ -263,13 +267,26 @@ private void CancelNewSession() ClearNewForm(); } + private void OnCreateSessionProgress(string message) + { + // Ensure we're on the UI thread. + if (_dispatcherQueue == null) return; + if (!_dispatcherQueue.HasThreadAccess) + { + _dispatcherQueue.TryEnqueue(() => OnCreateSessionProgress(message)); + return; + } + + NewSessionStatus = message; + } + [RelayCommand] private async Task ConfirmNewSession() { if (OperationInProgress || !NewSessionCreateEnabled) return; OperationInProgress = true; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); try { // The controller will send us a state changed event. @@ -286,7 +303,7 @@ await _syncSessionController.CreateSyncSession(new CreateSyncSessionRequest Host = NewSessionRemoteHost, Path = NewSessionRemotePath, }, - }, cts.Token); + }, OnCreateSessionProgress, cts.Token); ClearNewForm(); } @@ -304,6 +321,7 @@ await _syncSessionController.CreateSyncSession(new CreateSyncSessionRequest finally { OperationInProgress = false; + NewSessionStatus = ""; } } diff --git a/App/Views/Pages/FileSyncListMainPage.xaml b/App/Views/Pages/FileSyncListMainPage.xaml index d38bc29..5a96898 100644 --- a/App/Views/Pages/FileSyncListMainPage.xaml +++ b/App/Views/Pages/FileSyncListMainPage.xaml @@ -274,8 +274,11 @@ <ColumnDefinition Width="2*" MinWidth="200" /> <ColumnDefinition Width="1*" MinWidth="120" /> <ColumnDefinition Width="2*" MinWidth="200" /> - <ColumnDefinition Width="1*" MinWidth="100" MaxWidth="200" /> - <ColumnDefinition Width="1*" MinWidth="100" MaxWidth="200" /> + <!-- + To fit the status better, the last two columns + are merged for the new sync row. + --> + <ColumnDefinition Width="2*" MinWidth="200" MaxWidth="400" /> </Grid.ColumnDefinitions> <Border Grid.Column="0" Padding="0"> @@ -340,6 +343,13 @@ HorizontalAlignment="Stretch" Text="{x:Bind ViewModel.NewSessionRemotePath, Mode=TwoWay}" /> </Border> + <Border Grid.Column="4"> + <TextBlock + Text="{x:Bind ViewModel.NewSessionStatus, Mode=OneWay}" + VerticalAlignment="Center" + TextTrimming="CharacterEllipsis" + IsTextTrimmedChanged="TooltipText_IsTextTrimmedChanged" /> + </Border> </Grid> </StackPanel> </ScrollView> diff --git a/Tests.App/Services/MutagenControllerTest.cs b/Tests.App/Services/MutagenControllerTest.cs index 1605f1c..c834009 100644 --- a/Tests.App/Services/MutagenControllerTest.cs +++ b/Tests.App/Services/MutagenControllerTest.cs @@ -112,6 +112,13 @@ public async Task Ok(CancellationToken ct) // Ensure the daemon is stopped because all sessions are terminated. await AssertDaemonStopped(dataDirectory, ct); + var progressMessages = new List<string>(); + void OnProgress(string message) + { + TestContext.Out.WriteLine("Create session progress: " + message); + progressMessages.Add(message); + } + var session1 = await controller.CreateSyncSession(new CreateSyncSessionRequest { Alpha = new CreateSyncSessionRequest.Endpoint @@ -124,7 +131,10 @@ public async Task Ok(CancellationToken ct) Protocol = CreateSyncSessionRequest.Endpoint.ProtocolKind.Local, Path = betaDirectory.FullName, }, - }, ct); + }, OnProgress, ct); + + // There should've been at least one progress message. + Assert.That(progressMessages, Is.Not.Empty); state = controller.GetState(); Assert.That(state.SyncSessions, Has.Count.EqualTo(1)); @@ -142,7 +152,7 @@ public async Task Ok(CancellationToken ct) Protocol = CreateSyncSessionRequest.Endpoint.ProtocolKind.Local, Path = betaDirectory.FullName, }, - }, ct); + }, null, ct); state = controller.GetState(); Assert.That(state.SyncSessions, Has.Count.EqualTo(2)); @@ -225,7 +235,7 @@ await controller.CreateSyncSession(new CreateSyncSessionRequest Protocol = CreateSyncSessionRequest.Endpoint.ProtocolKind.Local, Path = betaDirectory.FullName, }, - }, ct); + }, null, ct); } await AssertDaemonStopped(dataDirectory, ct); @@ -265,7 +275,7 @@ await controller1.CreateSyncSession(new CreateSyncSessionRequest Protocol = CreateSyncSessionRequest.Endpoint.ProtocolKind.Local, Path = betaDirectory.FullName, }, - }, ct); + }, null, ct); controller2 = new MutagenController(_mutagenBinaryPath, dataDirectory); await controller2.RefreshState(ct);