From 24446dcb17d14bf40d093ea4028ff5ed9dc45c45 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Sat, 22 Aug 2026 22:03:36 -0700 Subject: [PATCH 01/20] feat: remove display name from RemoteServer # UniScan.Client.App - You can no longer add a Display Name from `AddRemoteDialogView` # UniScan.Client.Core - `RemoteServer` now uses the Connection Method's display string ## Breaking Changes - `RemoteDto`, `RemoteServer` no longer accept custom display names - `IRemoteFactory.Create` no longer accepts a `displayName` parameter # UniScan.Network.Client - Added `IRemoteConnectionMethod.ToDisplayString()` - Implemented `TCPRemoteConnectionMethod.ToDisplayString()` --- .../UniScan.Client.App/UniScan.Client.App.csproj | 4 ++-- .../ViewModels/Dialogs/AddRemoteDialogViewModel.cs | 5 +---- .../ViewModels/RemoteViewModel.cs | 2 +- .../Views/Dialogs/AddRemoteDialogView.axaml | 4 ---- .../UniScan.Client.Core/Config/Remote/RemoteDto.cs | 4 ++-- .../Config/Remote/RemotesListFile.cs | 9 +-------- .../Config/Types/RemoteServer.cs | 14 +++++--------- .../DI/Factory/RemoteFactory.cs | 4 ++-- .../UniScan.Client.Core/UniScan.Client.Core.csproj | 4 ++-- .../Remote/Connection/IRemoteConnectionMethod.cs | 2 ++ .../Methods/TCPRemoteConnectionMethod.cs | 2 ++ .../UniScan.Network.Client.csproj | 4 ++-- 12 files changed, 22 insertions(+), 36 deletions(-) diff --git a/Client/UniScan.Client.App/UniScan.Client.App.csproj b/Client/UniScan.Client.App/UniScan.Client.App.csproj index bc05cec..e99dc9e 100755 --- a/Client/UniScan.Client.App/UniScan.Client.App.csproj +++ b/Client/UniScan.Client.App/UniScan.Client.App.csproj @@ -5,8 +5,8 @@ 14 true UniScanProject - 0.0.9 - 0.0.9 + 0.0.10 + 0.0.10 diff --git a/Client/UniScan.Client.App/ViewModels/Dialogs/AddRemoteDialogViewModel.cs b/Client/UniScan.Client.App/ViewModels/Dialogs/AddRemoteDialogViewModel.cs index 7906f0d..f4cee32 100644 --- a/Client/UniScan.Client.App/ViewModels/Dialogs/AddRemoteDialogViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/Dialogs/AddRemoteDialogViewModel.cs @@ -15,9 +15,6 @@ public partial class AddRemoteDialogViewModel : ViewModelBase { private readonly IRemoteFactory _remoteFactory; - [ObservableProperty] - public partial string? DisplayName { get; set; } - [ObservableProperty] public partial ConnectionMethodFactoryViewModelSource.ConnectionMethodViewModelFactoryPair? SelectedMethodFactory { get; set; } @@ -47,7 +44,7 @@ public void Confirm() return; } - CreatedRemote = _remoteFactory.Create(DisplayName ?? "New Remote", method); + CreatedRemote = _remoteFactory.Create(method); DialogHost.Close("MainDialogHost", CreatedRemote); } diff --git a/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs b/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs index 4a3755e..c5d9db3 100644 --- a/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs @@ -21,7 +21,7 @@ public class RemoteViewModel : SubPagedViewModelBase, IDisposable public DeviceListViewModel DeviceList { get; } - public RemoteViewModel(RemoteServer remote) : base(new NotConnectedRemotePageViewModel(remote), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(remote.DisplayName))) + public RemoteViewModel(RemoteServer remote) : base(new NotConnectedRemotePageViewModel(remote), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(Guid.NewGuid().ToString()))) { this._notConnectedPage = (NotConnectedRemotePageViewModel)CurrentSubpage; this.Remote = remote; diff --git a/Client/UniScan.Client.App/Views/Dialogs/AddRemoteDialogView.axaml b/Client/UniScan.Client.App/Views/Dialogs/AddRemoteDialogView.axaml index 4c95f01..401d51e 100644 --- a/Client/UniScan.Client.App/Views/Dialogs/AddRemoteDialogView.axaml +++ b/Client/UniScan.Client.App/Views/Dialogs/AddRemoteDialogView.axaml @@ -14,10 +14,6 @@ Padding="6"> - - - diff --git a/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs b/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs index 34145ba..cf01020 100644 --- a/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs +++ b/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs @@ -5,7 +5,7 @@ namespace UniScan.Client.Core.Config.Remote; [method: JsonConstructor] -public record RemoteDto([property: JsonPropertyName("displayName")] string DisplayName, [property: JsonPropertyName("connectionMethod")] [property: JsonRequired] IRemoteConnectionMethod ConnectionMethod) +public record RemoteDto([property: JsonPropertyName("connectionMethod")] [property: JsonRequired] IRemoteConnectionMethod ConnectionMethod) { - public static RemoteDto FromRemoteServer(RemoteServer remoteServer) => new(remoteServer.DisplayName, remoteServer.ConnectionMethod); + public static RemoteDto FromRemoteServer(RemoteServer remoteServer) => new(remoteServer.ConnectionMethod); } \ No newline at end of file diff --git a/Client/UniScan.Client.Core/Config/Remote/RemotesListFile.cs b/Client/UniScan.Client.Core/Config/Remote/RemotesListFile.cs index dff0487..c352a7c 100644 --- a/Client/UniScan.Client.Core/Config/Remote/RemotesListFile.cs +++ b/Client/UniScan.Client.Core/Config/Remote/RemotesListFile.cs @@ -47,14 +47,7 @@ public async Task LoadAsync() return await SaveNewAsync(); } - #if DEBUG - foreach (RemoteDto remoteDto in r) - { - _logger.Debug("Loaded remote {Name} ({ConnectionMethod})", remoteDto.DisplayName, remoteDto.ConnectionMethod); - } - #endif - - return new RemoteManager(r.Select(d => _remoteFactory.Create(d.DisplayName, d.ConnectionMethod))); + return new RemoteManager(r.Select(d => _remoteFactory.Create(d.ConnectionMethod))); } public async Task SaveAsync(IRemoteManager stored) diff --git a/Client/UniScan.Client.Core/Config/Types/RemoteServer.cs b/Client/UniScan.Client.Core/Config/Types/RemoteServer.cs index e8a881e..7182315 100644 --- a/Client/UniScan.Client.Core/Config/Types/RemoteServer.cs +++ b/Client/UniScan.Client.Core/Config/Types/RemoteServer.cs @@ -40,10 +40,7 @@ public class ServerAttributes public class RemoteServer { - /// - /// The remote's display name - /// - public string DisplayName { get; } + public string DisplayName => ConnectionMethod.ToDisplayString();//todo cache /// /// connection method @@ -65,9 +62,8 @@ public class RemoteServer private readonly IServiceProvider _serviceProvider; - public RemoteServer(string displayName, IRemoteConnectionMethod connectionMethod, IEnumerable configurators, PacketRegistry packetRegistry, IServiceProvider serviceProvider) + public RemoteServer(IRemoteConnectionMethod connectionMethod, IEnumerable configurators, PacketRegistry packetRegistry, IServiceProvider serviceProvider) { - DisplayName = displayName; ConnectionMethod = connectionMethod; _serviceProvider = serviceProvider; @@ -75,17 +71,17 @@ public RemoteServer(string displayName, IRemoteConnectionMethod connectionMethod Socket.ConnectionState.Connected += (sender, args) => { _connected.Value = true; - Log.Information("Connected to {Remote} ({RemoteAddress}) over {ConnectionMethod}", DisplayName, args.Channel.RemoteAddress, ConnectionMethod); + Log.Information("Connected to {RemoteAddress} over {ConnectionMethod}", args.Channel.RemoteAddress, ConnectionMethod); }; Socket.ConnectionState.Disconnected += (sender, args) => { _connected.Value = false; - Log.Information("Disconnected from {Remote} ({RemoteAddress})", DisplayName, args.Channel.RemoteAddress); + Log.Information("Disconnected from {RemoteAddress}", args.Channel.RemoteAddress); }; } - public RemoteServer(RemoteDto dto, IEnumerable configurators, PacketRegistry packetRegistry, IServiceProvider serviceProvider) : this(dto.DisplayName, dto.ConnectionMethod, configurators, packetRegistry, serviceProvider) {} + public RemoteServer(RemoteDto dto, IEnumerable configurators, PacketRegistry packetRegistry, IServiceProvider serviceProvider) : this(dto.ConnectionMethod, configurators, packetRegistry, serviceProvider) {} public async Task RunConnectionAsync() { diff --git a/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs b/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs index 0c8ce6f..b2c63e7 100644 --- a/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs +++ b/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs @@ -11,10 +11,10 @@ namespace UniScan.Client.Core.DI.Factory; public interface IRemoteFactory { - public RemoteServer Create(string displayName, IRemoteConnectionMethod connectionMethod); + public RemoteServer Create(IRemoteConnectionMethod connectionMethod); } public class RemoteFactory(IServiceProvider provider) : IRemoteFactory { - public RemoteServer Create(string displayName, IRemoteConnectionMethod connectionMethod) => new(displayName, connectionMethod, provider.GetServices(), provider.GetRequiredService(), provider); + public RemoteServer Create(IRemoteConnectionMethod connectionMethod) => new(connectionMethod, provider.GetServices(), provider.GetRequiredService(), provider); } \ No newline at end of file diff --git a/Client/UniScan.Client.Core/UniScan.Client.Core.csproj b/Client/UniScan.Client.Core/UniScan.Client.Core.csproj index 0a00d1d..5696317 100644 --- a/Client/UniScan.Client.Core/UniScan.Client.Core.csproj +++ b/Client/UniScan.Client.Core/UniScan.Client.Core.csproj @@ -6,8 +6,8 @@ enable 14 UniScan - 0.2.0 - 0.2.0 + 0.3.0 + 0.3.0 diff --git a/Network/UniScan.Network.Client/Remote/Connection/IRemoteConnectionMethod.cs b/Network/UniScan.Network.Client/Remote/Connection/IRemoteConnectionMethod.cs index 8540eb2..7366bb5 100644 --- a/Network/UniScan.Network.Client/Remote/Connection/IRemoteConnectionMethod.cs +++ b/Network/UniScan.Network.Client/Remote/Connection/IRemoteConnectionMethod.cs @@ -7,4 +7,6 @@ public interface IRemoteConnectionMethod { void Apply(Bootstrap bootstrap); Task ConnectAsync(Bootstrap bootstrap); + + string ToDisplayString(); } \ No newline at end of file diff --git a/Network/UniScan.Network.Client/Remote/Connection/Methods/TCPRemoteConnectionMethod.cs b/Network/UniScan.Network.Client/Remote/Connection/Methods/TCPRemoteConnectionMethod.cs index e2b0016..2eb084d 100644 --- a/Network/UniScan.Network.Client/Remote/Connection/Methods/TCPRemoteConnectionMethod.cs +++ b/Network/UniScan.Network.Client/Remote/Connection/Methods/TCPRemoteConnectionMethod.cs @@ -24,5 +24,7 @@ public void Apply(Bootstrap bootstrap) } public Task ConnectAsync(Bootstrap bootstrap) => bootstrap.ConnectAsync(EndPoint); + + public string ToDisplayString() => EndPoint.ToString(); } #endif \ No newline at end of file diff --git a/Network/UniScan.Network.Client/UniScan.Network.Client.csproj b/Network/UniScan.Network.Client/UniScan.Network.Client.csproj index d859edc..e2077e3 100755 --- a/Network/UniScan.Network.Client/UniScan.Network.Client.csproj +++ b/Network/UniScan.Network.Client/UniScan.Network.Client.csproj @@ -6,8 +6,8 @@ enable true UniScan - 0.0.1 - 0.0.1 + 0.0.2 + 0.0.2 From b4318df07b68476dc70c0b0c642c374372ee18bb Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Sun, 23 Aug 2026 22:41:56 -0700 Subject: [PATCH 02/20] feat!: move remote connection flow to TaskPipeline, add ct to TaskPipeline # UniScan.Client.App - Created `RemoteConnectionPipeline`, which handles the flow for connecting to a remote, negotiation, and finalization - `TaskPipeline` now accepts a `CancellationToken` and will handle when an exception is thrown. - `LoadingViewModel` now accepts loading text ## Breaking Changes - `ITaskStage` tasks now require a `CancellationToken` # UniScan.Client.Core - Connecting is no longer done through `RemoteServer` - Instead, it has been transformed into a `TaskPipeline` in `UniScan.Client.App` - I intend on bringing `TaskPipeline` into either `Shiki.Common` or `UniScan.Client.Core` in the future, and moving the pipeline creation back into RemoteServer ## Breaking Changes - Removed `RemoteServer.RunConnectionAsync` # UniScan.Network - `ISocket.SendRequestAsync` now accepts a `CancellationToken` # UniScan.Network.Client - `TCPRemoteConnectionMethod.ConnectAsync` now awaits `bootstrap.ConnectAsync()` - `ClientSocket.StartAsync` now stops the socket on exception - `ClientSocket.SendRequestAsync` now accepts a `CancellationToken` # UniScan.Server.Core - Now has extra logging on exit --- .../RemoteConnectionPipeline.TaskContexts.cs | 45 +++++++++++++ .../RemoteConnectionPipeline.Tasks.cs | 67 +++++++++++++++++++ .../Connection/RemoteConnectionPipeline.cs | 32 +++++++++ .../Core/Pipeline/ITaskStage.cs | 3 +- ...nAppInitializationPipeline.Initializers.cs | 9 +-- ...nAppInitializationPipeline.TaskContexts.cs | 3 +- .../UniScanAppInitializationPipeline.cs | 7 +- .../Core/Pipeline/TaskPipeline.cs | 17 ++++- .../Core/Pipeline/TaskPipelineBuilder.cs | 4 +- .../Core/Pipeline/TaskStage.cs | 9 +-- .../UniScanApp.Initializers.cs | 13 ++-- Client/UniScan.Client.App/UniScanApp.axaml.cs | 4 +- .../Controls/RemoteControlViewModel.cs | 5 +- .../ViewModels/LoadingViewModel.cs | 4 +- .../ViewModels/MainViewModel.cs | 4 +- .../ViewModels/Pages/MainPageViewModel.cs | 4 +- .../Pages/NotConnectedRemotePageViewModel.cs | 29 ++++---- .../ViewModels/RemoteViewModel.cs | 18 ++++- .../Views/LoadingView.axaml | 2 +- .../Pages/NotConnectedRemotePageView.axaml | 2 +- .../Config/Types/RemoteServer.cs | 41 +++--------- .../UniScan.Network.Client/ClientSocket.cs | 44 +++++++----- .../Methods/TCPRemoteConnectionMethod.cs | 2 +- Network/UniScan.Network/Socket/ISocket.cs | 2 +- Server/UniScan.Server.Core/UniScanServer.cs | 3 + Server/UniScan.Server.Host/UniScanHost.cs | 2 + 26 files changed, 268 insertions(+), 107 deletions(-) create mode 100644 Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.TaskContexts.cs create mode 100644 Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.Tasks.cs create mode 100644 Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.cs rename Client/UniScan.Client.App/Core/{ => Pipeline}/Initialization/UniScanAppInitializationPipeline.Initializers.cs (79%) rename Client/UniScan.Client.App/Core/{ => Pipeline}/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs (97%) rename Client/UniScan.Client.App/Core/{ => Pipeline}/Initialization/UniScanAppInitializationPipeline.cs (91%) diff --git a/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.TaskContexts.cs b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.TaskContexts.cs new file mode 100644 index 0000000..f5dee5a --- /dev/null +++ b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.TaskContexts.cs @@ -0,0 +1,45 @@ +using System; +using R3; +using UniScan.Client.Core.Config.Types; + +namespace UniScan.Client.App.Core.Pipeline.Connection; + +public partial class RemoteConnectionPipeline +{ + public static partial class TaskContexts + { + public class ConnectionContext(IServiceProvider provider, RemoteServer server) : ITaskContext + { + public BindableReactiveProperty Status { get; } = new("Connecting..."); + + public RemoteServer RemoteServer { get; } = server; + public IRemoteServerMutationProxy RemoteServerMutationProxy => RemoteServer; + + public IServiceProvider ServiceProvider { get; } = provider; + } + + public class NegotiationContext(string status, IServiceProvider provider, RemoteServer server) : ITaskContext + { + public BindableReactiveProperty Status { get; } = new(status); + + public RemoteServer RemoteServer { get; } = server; + public IRemoteServerMutationProxy RemoteServerMutationProxy => RemoteServer; + + public IServiceProvider ServiceProvider { get; } = provider; + + public static NegotiationContext TransitionFrom(ConnectionContext oldContext) => new(oldContext.Status.Value, oldContext.ServiceProvider, oldContext.RemoteServer); + } + + public class RemoteContext(string status, IServiceProvider provider, RemoteServer server) : ITaskContext + { + public BindableReactiveProperty Status { get; } = new(status); + + public RemoteServer RemoteServer { get; } = server; + public IRemoteServerMutationProxy RemoteServerMutationProxy => RemoteServer; + + public IServiceProvider ServiceProvider { get; } = provider; + + public static RemoteContext TransitionFrom(NegotiationContext oldContext) => new(oldContext.Status.Value, oldContext.ServiceProvider, oldContext.RemoteServer); + } + } +} \ No newline at end of file diff --git a/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.Tasks.cs b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.Tasks.cs new file mode 100644 index 0000000..62eca6b --- /dev/null +++ b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.Tasks.cs @@ -0,0 +1,67 @@ +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using DotNetty.Transport.Channels; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using UniScan.Network.Data.Info.Software; +using UniScan.Network.Packet.Packets.Serverbound; +using UniScan.Network.Packet.Packets.Serverbound.Client; + +namespace UniScan.Client.App.Core.Pipeline.Connection; + +public partial class RemoteConnectionPipeline +{ + public async Task StartConnection(TaskContexts.ConnectionContext ctx, CancellationToken ct = default) + { + ctx.Status.Value = $"Connecting to {ctx.RemoteServer.ConnectionMethod.ToDisplayString()}"; + + await ctx.RemoteServer.Socket.StartAsync(); + if (!ctx.RemoteServer.Socket.Connected) + { + throw new InvalidOperationException("Connection failed!"); + } + } + + public async Task Handshake(TaskContexts.NegotiationContext ctx, CancellationToken ct = default) + { + ctx.Status.Value = $"Handshaking"; + + ClientSoftwareInfo? clientSoftware = ctx.ServiceProvider.GetService(); + if (clientSoftware == null) + { + throw new NullReferenceException(nameof(clientSoftware)); + } + + var serverSoftware = await ctx.RemoteServer.Socket.SendRequestAsync(ClientSoftwareInfoPacket.CreateRequest(clientSoftware), ct); + + if (serverSoftware.HasValue) + { + ctx.RemoteServerMutationProxy.SetSoftwareInfo(serverSoftware.Value.Info); + _logger.Information("Received server software info: {Info}", serverSoftware.Value.Info); + } + } + + public async Task GetDeviceList(TaskContexts.RemoteContext ctx, CancellationToken ct = default) + { + ctx.Status.Value = "Receiving devices"; + + + var devices = await ctx.RemoteServer.Socket.SendRequestAsync(GetDeviceListPacket.CreateRequest(), ct); + + if (devices.HasValue) + { + _logger.Debug("Received devices list: [{Devices}]", string.Join(", ", devices.Value.Devices)); + + foreach (var deviceInfoDto in devices.Value.Devices) + { + ctx.RemoteServer.Devices.Add(deviceInfoDto); + } + } + else + { + Log.Error(devices.Error, "Exception returned when trying to get devices list"); + } + } +} \ No newline at end of file diff --git a/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.cs b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.cs new file mode 100644 index 0000000..729ff06 --- /dev/null +++ b/Client/UniScan.Client.App/Core/Pipeline/Connection/RemoteConnectionPipeline.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using R3; +using Serilog; + +namespace UniScan.Client.App.Core.Pipeline.Connection; + +public partial class RemoteConnectionPipeline +{ + public TaskPipeline Pipeline { get; } + private readonly ILogger _logger = Log.ForContext(); + private readonly IDisposable _subscription; + + public RemoteConnectionPipeline() + { + Pipeline = new TaskPipelineBuilder() + .ThenRun(StartConnection) + .ThenTransitionTo() + .ThenRun(Handshake) + .ThenTransitionTo() + .ThenRun(GetDeviceList) + .ThenRun((_, _) => + { + _subscription?.Dispose(); + + return Task.CompletedTask; + }) + .Build(); + + _subscription = Pipeline.Status.Subscribe(s => { _logger.Information("{Status}", s); }); + } +} \ No newline at end of file diff --git a/Client/UniScan.Client.App/Core/Pipeline/ITaskStage.cs b/Client/UniScan.Client.App/Core/Pipeline/ITaskStage.cs index 0b19f62..8c3174a 100644 --- a/Client/UniScan.Client.App/Core/Pipeline/ITaskStage.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/ITaskStage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace UniScan.Client.App.Core.Pipeline; @@ -7,7 +8,7 @@ namespace UniScan.Client.App.Core.Pipeline; public interface ITaskStage { int Count { get; } - IEnumerable> Tasks { get; } + IEnumerable> Tasks { get; } ITaskContext Transition(ITaskContext old); } \ No newline at end of file diff --git a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.Initializers.cs b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.Initializers.cs similarity index 79% rename from Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.Initializers.cs rename to Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.Initializers.cs index 0ed109f..bf070a3 100644 --- a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.Initializers.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.Initializers.cs @@ -1,13 +1,14 @@ +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using UniScan.Client.App.ViewModels; using UniScan.Client.App.Views; -namespace UniScan.Client.App.Core.Initialization; +namespace UniScan.Client.App.Core.Pipeline.Initialization; public partial class UniScanAppInitializationPipeline { - internal static async Task InitializeViews(TaskContexts.PostClient ctx) + internal static async Task InitializeViews(TaskContexts.PostClient ctx, CancellationToken ct = default) { ctx.Status.Value = "Initializing UI"; @@ -17,14 +18,14 @@ internal static async Task InitializeViews(TaskContexts.PostClient ctx) ctx.ServiceCollection.AddSingleton(); } - internal static async Task InitializeServiceProvider(TaskContexts.PreServiceProvider ctx) + internal static async Task InitializeServiceProvider(TaskContexts.PreServiceProvider ctx, CancellationToken ct = default) { ctx.Status.Value = "Building ServiceProvider"; ctx.Services = ctx.ServiceCollection.BuildServiceProvider(); } - internal static async Task InitializeRemotes(TaskContexts.PostServiceProvider ctx) + internal static async Task InitializeRemotes(TaskContexts.PostServiceProvider ctx, CancellationToken ct = default) { ctx.Status.Value = "Saving remotes"; diff --git a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs similarity index 97% rename from Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs rename to Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs index a0e8d60..a1e160f 100644 --- a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.TaskContexts.cs @@ -1,10 +1,9 @@ using System; using Microsoft.Extensions.DependencyInjection; using R3; -using UniScan.Client.App.Core.Pipeline; using UniScan.Client.Core; -namespace UniScan.Client.App.Core.Initialization; +namespace UniScan.Client.App.Core.Pipeline.Initialization; public partial class UniScanAppInitializationPipeline { diff --git a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.cs b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.cs similarity index 91% rename from Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.cs rename to Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.cs index d4c3d52..f368927 100644 --- a/Client/UniScan.Client.App/Core/Initialization/UniScanAppInitializationPipeline.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/Initialization/UniScanAppInitializationPipeline.cs @@ -2,9 +2,8 @@ using System.Threading.Tasks; using R3; using Serilog; -using UniScan.Client.App.Core.Pipeline; -namespace UniScan.Client.App.Core.Initialization; +namespace UniScan.Client.App.Core.Pipeline.Initialization; public partial class UniScanAppInitializationPipeline { @@ -17,7 +16,7 @@ public UniScanAppInitializationPipeline(UniScanApp app) Pipeline = new TaskPipelineBuilder() //early .ThenRun(app.InitializeEnvironment) - .ThenRun(_ => + .ThenRun((_, _) => { _logger = Log.ForContext(); @@ -34,7 +33,7 @@ public UniScanAppInitializationPipeline(UniScanApp app) .ThenTransitionTo() .ThenRun(InitializeRemotes) .ThenRun(app.FinishInitialization) - .ThenRun(_ => + .ThenRun((_, _) => { _subscription?.Dispose(); diff --git a/Client/UniScan.Client.App/Core/Pipeline/TaskPipeline.cs b/Client/UniScan.Client.App/Core/Pipeline/TaskPipeline.cs index 6af65b6..220906a 100644 --- a/Client/UniScan.Client.App/Core/Pipeline/TaskPipeline.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/TaskPipeline.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using R3; +using Serilog; namespace UniScan.Client.App.Core.Pipeline; @@ -17,7 +19,7 @@ internal TaskPipeline() {}//only allow builder to call ctor internal void Add(ITaskStage stage) => _stages.Add(stage); - public async Task RunAsync(ITaskContext initial) + public async Task RunAsync(ITaskContext initial, CancellationToken ct = default) { ITaskContext currentContext = initial; IDisposable? subscription = null; @@ -36,13 +38,22 @@ public async Task RunAsync(ITaskContext initial) foreach (var task in stage.Tasks) { - await task(currentContext); + ct.ThrowIfCancellationRequested(); + + await task(currentContext, ct); completed++; Progress.Value = (completed * 100) / tasks; } } - }//todo catch exception? idk how to get this fucking exception to bubble + } + catch (Exception ex) + { + Status.Value = "Failed! Check the logs for more info."; + Log.Error(ex, "Task failed."); + + throw; + } finally { subscription?.Dispose(); diff --git a/Client/UniScan.Client.App/Core/Pipeline/TaskPipelineBuilder.cs b/Client/UniScan.Client.App/Core/Pipeline/TaskPipelineBuilder.cs index 5d1c6df..8756e1a 100644 --- a/Client/UniScan.Client.App/Core/Pipeline/TaskPipelineBuilder.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/TaskPipelineBuilder.cs @@ -1,5 +1,7 @@ using System; +using System.Threading; using System.Threading.Tasks; +using Serilog; namespace UniScan.Client.App.Core.Pipeline; @@ -25,7 +27,7 @@ public TaskPipelineBuilder(TaskPipeline pipeline, TaskStage ThenRun(Func task) + public TaskPipelineBuilder ThenRun(Func task) { _stage.Add(task); return this; diff --git a/Client/UniScan.Client.App/Core/Pipeline/TaskStage.cs b/Client/UniScan.Client.App/Core/Pipeline/TaskStage.cs index d7bbd21..4709233 100644 --- a/Client/UniScan.Client.App/Core/Pipeline/TaskStage.cs +++ b/Client/UniScan.Client.App/Core/Pipeline/TaskStage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace UniScan.Client.App.Core.Pipeline; @@ -9,16 +10,16 @@ public class TaskStage : ITaskStage where TContext : class, ITaskContext { private readonly Func _transition; - private readonly List> _tasks = []; + private readonly List> _tasks = []; public int Count => _tasks.Count; - public IEnumerable> Tasks + public IEnumerable> Tasks { get { foreach (var task in _tasks) - yield return ctx => task((TContext)ctx); + yield return (ctx, ct) => task((TContext)ctx, ct); } } @@ -27,7 +28,7 @@ public TaskStage(Func transition) _transition = transition; } - public void Add(Func task) => _tasks.Add(task); + public void Add(Func task) => _tasks.Add(task); public ITaskContext Transition(ITaskContext old) => old is not TOldContext ctx ? throw new InvalidCastException() : _transition(ctx); diff --git a/Client/UniScan.Client.App/UniScanApp.Initializers.cs b/Client/UniScan.Client.App/UniScanApp.Initializers.cs index f2dda92..9f6f997 100644 --- a/Client/UniScan.Client.App/UniScanApp.Initializers.cs +++ b/Client/UniScan.Client.App/UniScanApp.Initializers.cs @@ -1,13 +1,14 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Serilog; using Shiki.ModuleManagement; using Shiki.ModuleManagement.Implementations.Sources; -using UniScan.Client.App.Core.Initialization; using UniScan.Client.App.Core.Module; using UniScan.Client.App.Core.Module.Modules.Internal; +using UniScan.Client.App.Core.Pipeline.Initialization; using UniScan.Client.Core; using UniScan.Client.Core.DI.Factory; using UniScan.Platform.DependencyInjection; @@ -16,7 +17,7 @@ namespace UniScan.Client.App; public partial class UniScanApp { - internal async Task InitializeEnvironment(UniScanAppInitializationPipeline.TaskContexts.Early context) + internal async Task InitializeEnvironment(UniScanAppInitializationPipeline.TaskContexts.Early context, CancellationToken ct = default) { context.Status.Value = "Initializing environment"; @@ -36,7 +37,7 @@ internal async Task InitializeEnvironment(UniScanAppInitializationPipeline.TaskC Log.Logger.Debug("Initialized Environment {Env}", _hostEnvironment); } - internal async Task InitializeSoftwareInfo(UniScanAppInitializationPipeline.TaskContexts.Early ctx) + internal async Task InitializeSoftwareInfo(UniScanAppInitializationPipeline.TaskContexts.Early ctx, CancellationToken ct = default) { ctx.Status.Value = "Initializing SoftwareInfo"; @@ -44,7 +45,7 @@ internal async Task InitializeSoftwareInfo(UniScanAppInitializationPipeline.Task Log.Information("{Info}", SoftwareInfo); } - internal async Task InitializeModules(UniScanAppInitializationPipeline.TaskContexts.Early ctx) + internal async Task InitializeModules(UniScanAppInitializationPipeline.TaskContexts.Early ctx, CancellationToken ct = default) { ctx.Status.Value = "Initializing modules"; @@ -75,7 +76,7 @@ internal async Task InitializeModules(UniScanAppInitializationPipeline.TaskConte } } - internal async Task InitializeClient(UniScanAppInitializationPipeline.TaskContexts.PreClient ctx) + internal async Task InitializeClient(UniScanAppInitializationPipeline.TaskContexts.PreClient ctx, CancellationToken ct = default) { ctx.Status.Value = "Initializing client and loading remotes"; @@ -84,7 +85,7 @@ internal async Task InitializeClient(UniScanAppInitializationPipeline.TaskContex ctx.ServiceCollection.AddSingleton(_ => ctx.Client.ServiceProvider.GetRequiredService()); } - internal Task FinishInitialization(UniScanAppInitializationPipeline.TaskContexts.PostServiceProvider ctx) + internal Task FinishInitialization(UniScanAppInitializationPipeline.TaskContexts.PostServiceProvider ctx, CancellationToken ct = default) { try { diff --git a/Client/UniScan.Client.App/UniScanApp.axaml.cs b/Client/UniScan.Client.App/UniScanApp.axaml.cs index ca522f5..7914dde 100755 --- a/Client/UniScan.Client.App/UniScanApp.axaml.cs +++ b/Client/UniScan.Client.App/UniScanApp.axaml.cs @@ -14,8 +14,8 @@ using Shiki.Common.Identity; using Shiki.ModuleManagement; using Shiki.ModuleManagement.Implementations.Sources; -using UniScan.Client.App.Core.Initialization; using UniScan.Client.App.Core.Module; +using UniScan.Client.App.Core.Pipeline.Initialization; using UniScan.Client.App.ViewModels; using UniScan.Client.Core; using UniScan.Client.Core.DI.Factory; @@ -75,7 +75,7 @@ public override void OnFrameworkInitializationCompleted() { Log.Information("Loading root view"); - RootViewModel = new RootViewModel(new LoadingViewModel(InitializationPipeline.Pipeline)); + RootViewModel = new RootViewModel(new LoadingViewModel("Loading...", InitializationPipeline.Pipeline)); switch (ApplicationLifetime) { case IClassicDesktopStyleApplicationLifetime desktop: diff --git a/Client/UniScan.Client.App/ViewModels/Controls/RemoteControlViewModel.cs b/Client/UniScan.Client.App/ViewModels/Controls/RemoteControlViewModel.cs index 47dadd4..8fe123a 100644 --- a/Client/UniScan.Client.App/ViewModels/Controls/RemoteControlViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/Controls/RemoteControlViewModel.cs @@ -12,11 +12,11 @@ public partial class RemoteControlViewModel : ViewModelBase, IDisposable private readonly RemoteViewModel _viewModel; - public RemoteControlViewModel(RemoteServer remote) + public RemoteControlViewModel(IServiceProvider provider, RemoteServer remote) { this.Remote = remote; - _viewModel = new RemoteViewModel(Remote); + _viewModel = new RemoteViewModel(provider, Remote); } [RelayCommand] @@ -28,7 +28,6 @@ public async Task OnClicked() [RelayCommand] public async Task OnConnectClicked() { - } public void Dispose() diff --git a/Client/UniScan.Client.App/ViewModels/LoadingViewModel.cs b/Client/UniScan.Client.App/ViewModels/LoadingViewModel.cs index af4cc85..3c02b99 100644 --- a/Client/UniScan.Client.App/ViewModels/LoadingViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/LoadingViewModel.cs @@ -7,9 +7,11 @@ public class LoadingViewModel : ViewModelBase public static string VersionString => $"UniScan Client v{UniScanApp.SoftwareInfo.Version} (Platform v{UniScanApp.PlatformVersion})"; public TaskPipeline TaskPipeline { get; } + public string LoadingText { get; } - public LoadingViewModel(TaskPipeline taskPipeline) + public LoadingViewModel(string loadingText, TaskPipeline taskPipeline) { + LoadingText = loadingText; TaskPipeline = taskPipeline; } } \ No newline at end of file diff --git a/Client/UniScan.Client.App/ViewModels/MainViewModel.cs b/Client/UniScan.Client.App/ViewModels/MainViewModel.cs index 7b78d4b..87deffc 100755 --- a/Client/UniScan.Client.App/ViewModels/MainViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/MainViewModel.cs @@ -33,7 +33,7 @@ public MainViewModel(UniScanClient client, ClientSettingsViewModel clientSetting RemoteFactory = remoteFactory; Settings = clientSettingsViewModel; - RemotesView = Client.RemoteManager.Remotes.CreateView(remote => new RemoteControlViewModel(remote)) + RemotesView = Client.RemoteManager.Remotes.CreateView(remote => new RemoteControlViewModel(Client.ServiceProvider, remote)) .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current); RemotesView.CollectionChanged += (sender, args) => @@ -52,7 +52,7 @@ public MainViewModel(UniScanClient client, ClientSettingsViewModel clientSetting } }; - _mainPage = new MainPageViewModel(Client.RemoteManager); + _mainPage = new MainPageViewModel(Client.ServiceProvider, Client.RemoteManager); CurrentSubpage = _mainPage; } diff --git a/Client/UniScan.Client.App/ViewModels/Pages/MainPageViewModel.cs b/Client/UniScan.Client.App/ViewModels/Pages/MainPageViewModel.cs index b288070..ec131cc 100644 --- a/Client/UniScan.Client.App/ViewModels/Pages/MainPageViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/Pages/MainPageViewModel.cs @@ -24,11 +24,11 @@ public partial class MainPageViewModel : ViewModelBase, IDisposable private readonly IRemoteManager _remoteManager; - public MainPageViewModel(IRemoteManager remoteManager) + public MainPageViewModel(IServiceProvider provider, IRemoteManager remoteManager) { _remoteManager = remoteManager; - _remotesView = remoteManager.Remotes.CreateView(remote => new RemoteViewModel(remote)); + _remotesView = remoteManager.Remotes.CreateView(remote => new RemoteViewModel(provider, remote)); _remotesView.AttachFilter(new RemoteConnectionStateFilter()); ConnectedRemotesView = _remotesView.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current); diff --git a/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs b/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs index 5fb6ee7..388415d 100644 --- a/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs @@ -1,39 +1,34 @@ using System; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; -using Serilog; +using UniScan.Client.App.Core.Pipeline.Connection; using UniScan.Client.Core.Config.Types; namespace UniScan.Client.App.ViewModels.Pages; -public partial class NotConnectedRemotePageViewModel(RemoteServer remote) : ViewModelBase +public partial class NotConnectedRemotePageViewModel(IServiceProvider provider, RemoteServer remote) : ViewModelBase { public RemoteServer Remote { get; set; } = remote; public bool HasConnectionMethod { get; set; } = remote.ConnectionMethod != null; + + public event Action? OnConnecting; + public event Action? OnConnectFailed; [RelayCommand] - public async Task OnAnonymousConnectClicked() + public async Task OnConnectClicked() { - if (Remote.Socket.Connected) - return; + RemoteConnectionPipeline pipeline = new(); + OnConnecting?.Invoke(pipeline); try { - await Remote.Socket.StartAsync(); - - if (Remote.Socket.Connected) - { - _ = Remote.RunConnectionAsync(); - } - else - { - Log.Error("Failed to connect!"); - } + await pipeline.Pipeline.RunAsync(new RemoteConnectionPipeline.TaskContexts.ConnectionContext(provider, + remote)); } - catch (Exception ex) + catch (Exception e) { - Log.Information(ex, "FUCK!"); + OnConnectFailed?.Invoke(e); } } } \ No newline at end of file diff --git a/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs b/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs index 220d369..e8889ea 100644 --- a/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/RemoteViewModel.cs @@ -21,9 +21,25 @@ public class RemoteViewModel : SubPagedViewModelBase, IDisposable public DeviceListViewModel DeviceList { get; } - public RemoteViewModel(RemoteServer remote) : base(new NotConnectedRemotePageViewModel(remote), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(Guid.NewGuid().ToString()))) + public RemoteViewModel(IServiceProvider provider, RemoteServer remote) : base(new NotConnectedRemotePageViewModel(provider, remote), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(Guid.NewGuid().ToString()))) { this._notConnectedPage = (NotConnectedRemotePageViewModel)CurrentSubpage; + this._notConnectedPage.OnConnecting += (pipeline) => + { + this.CurrentSubpage = new LoadingViewModel("Connecting...", pipeline.Pipeline); + }; + + this._notConnectedPage.OnConnectFailed += (ex) => + { + this.CurrentSubpage = new DisconnectedRemotePageViewModel("Failed to connect to server! " + ex.Message) + { + OkClicked = new RelayCommand(() => + { + this.CurrentSubpage = _notConnectedPage; + }) + }; + }; + this.Remote = remote; DeviceList = new DeviceListViewModel(remote); diff --git a/Client/UniScan.Client.App/Views/LoadingView.axaml b/Client/UniScan.Client.App/Views/LoadingView.axaml index 1d1af18..8bdd006 100644 --- a/Client/UniScan.Client.App/Views/LoadingView.axaml +++ b/Client/UniScan.Client.App/Views/LoadingView.axaml @@ -12,7 +12,7 @@ - Loading... + diff --git a/Client/UniScan.Client.App/Views/Pages/NotConnectedRemotePageView.axaml b/Client/UniScan.Client.App/Views/Pages/NotConnectedRemotePageView.axaml index 4d76553..510da61 100644 --- a/Client/UniScan.Client.App/Views/Pages/NotConnectedRemotePageView.axaml +++ b/Client/UniScan.Client.App/Views/Pages/NotConnectedRemotePageView.axaml @@ -10,7 +10,7 @@ - + - + - - - - diff --git a/Client/UniScan.Client.App/Views/Controls/DeviceListView.axaml b/Client/UniScan.Client.App/Views/Controls/DeviceListView.axaml index d860bd6..454136a 100644 --- a/Client/UniScan.Client.App/Views/Controls/DeviceListView.axaml +++ b/Client/UniScan.Client.App/Views/Controls/DeviceListView.axaml @@ -7,7 +7,7 @@ xmlns:pages="clr-namespace:UniScan.Client.App.ViewModels.Pages" xmlns:controls2="clr-namespace:UniScan.Client.App.ViewModels.Controls" xmlns:controls3="clr-namespace:UniScan.Client.App.Views.Controls" - xmlns:util="clr-namespace:UniScan.Client.App.UI.Util" + xmlns:conversion="clr-namespace:UniScan.Client.App.UI.Conversion" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Controls.DeviceListView" x:DataType="controls2:DeviceListViewModel"> @@ -32,11 +32,11 @@ - + - + diff --git a/Client/UniScan.Client.App/Views/Controls/RemoteControlView.axaml b/Client/UniScan.Client.App/Views/Controls/RemoteControlView.axaml index 5a3ced0..96f5e72 100644 --- a/Client/UniScan.Client.App/Views/Controls/RemoteControlView.axaml +++ b/Client/UniScan.Client.App/Views/Controls/RemoteControlView.axaml @@ -5,13 +5,15 @@ xmlns:controls="clr-namespace:UniScan.Client.App.ViewModels.Controls" xmlns:controls1="clr-namespace:UniScan.Client.App.Views.Controls" xmlns:controls2="clr-namespace:UniScan.Client.App.ViewModels.Controls" + xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Controls.RemoteControlView" x:DataType="controls2:RemoteControlViewModel"> public IScanChannel? CurrentChannel { get; set; } - //dict of special attributes, allows for enough flexibility when a scanner may have extra state to store - public Dictionary Attributes { get; set; } = [];//TODO send to deepest point of hell and instead subclass DeviceState - /// public override string ToString() => JsonSerializer.Serialize(this); } \ No newline at end of file diff --git a/Core/UniScan.Device/Device/Scanner.cs b/Core/UniScan.Device/Device/Scanner.cs index cfdc770..433e084 100755 --- a/Core/UniScan.Device/Device/Scanner.cs +++ b/Core/UniScan.Device/Device/Scanner.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using R3; using Semver; using Serilog; using UniScan.Core.State; @@ -13,7 +14,6 @@ namespace UniScan.Device.Device; public interface IScannerEvents { - public event Action? OnStateUpdated; } public interface IScannerAPI @@ -50,13 +50,12 @@ public abstract class Scanner : IScanner, IScannerEvents public abstract bool Active { get; } [JsonIgnore] - public abstract DeviceState? State { get; protected set; } + public abstract IReadOnlyBindableReactiveProperty State { get; } [JsonIgnore] public abstract ScannerInfo? ScannerInfo { get; protected set; } protected readonly ILogger Logger; - public abstract event Action? OnStateUpdated; public Scanner(IScannerConnection connection) { diff --git a/Network/UniScan.Network.Client/ClientSocket.cs b/Network/UniScan.Network.Client/ClientSocket.cs index 1b92c40..87fe029 100755 --- a/Network/UniScan.Network.Client/ClientSocket.cs +++ b/Network/UniScan.Network.Client/ClientSocket.cs @@ -12,10 +12,25 @@ using UniScan.Network.Packet.PayloadPart; using UniScan.Network.Request; using UniScan.Network.Socket; +using UniScan.Network.Socket.Configuration; using UniScan.Network.Util; namespace UniScan.Network.Client; +public interface IClientSocketFactory +{ + ClientSocket CreateInstance(IRemoteConnectionMethod connectionMethod); +} + +public class ClientSocketFactory( + PacketRegistry registry, + IEnumerable configurators, + IServiceProvider provider +) : IClientSocketFactory +{ + public ClientSocket CreateInstance(IRemoteConnectionMethod connectionMethod) => new(new UniScanClientChannelInitializer(registry, configurators, provider), connectionMethod); +} + public class ClientSocket : ISocket { public ILogger Logger => Log.ForContext(); @@ -27,7 +42,7 @@ public class ClientSocket : ISocket private readonly IRemoteConnectionMethod _connectionMethod; public IChannel? Channel { get; private set; } - + public bool Connected => Channel?.Active == true; private readonly RequestManager _requestManager = new(); @@ -36,7 +51,7 @@ public ClientSocket(UniScanClientChannelInitializer channelInitializer, IRemoteC { ArgumentNullException.ThrowIfNull(channelInitializer); ArgumentNullException.ThrowIfNull(connectionMethod); - + _connectionMethod = connectionMethod; _channelInitializer = channelInitializer; } @@ -72,7 +87,8 @@ public async Task StartAsync() public async Task StopAsync() { - if (_requestManager != null) await _requestManager.RejectAllAsync(new OperationCanceledException("Socket is shutting down")); + if (_requestManager != null) + await _requestManager.RejectAllAsync(new OperationCanceledException("Socket is shutting down")); if (Channel != null) { await Channel.CloseAsync(); @@ -87,23 +103,26 @@ public async Task StopAsync() } public async Task SendPacketAsync(IPacket packet) => await SendPacketAsync(Channel, packet); - + public async Task SendPacketAsync(IChannel? channel, IPacket packet) { if (channel is not { Active: true }) return false; - + await channel.WriteAndFlushAsync(packet); return true; } - public async Task> SendRequestAsync(IRequestPayloadPart request, CancellationToken ct = default) + public async Task> SendRequestAsync( + IRequestPayloadPart request, CancellationToken ct = default) where TResponse : IPacket, IResponsePayloadPart => await _requestManager.MakeRequestAsync(Channel, request, ct); - - public async Task> SendRequestAsync(IChannel? channel, IRequestPayloadPart request, CancellationToken ct = default) + + public async Task> SendRequestAsync( + IChannel? channel, IRequestPayloadPart request, CancellationToken ct = default) where TResponse : IPacket, IResponsePayloadPart { - if (channel is not { Active: true }) return new Result(new ArgumentNullException(nameof(channel))); - + if (channel is not { Active: true }) + return new Result(new ArgumentNullException(nameof(channel))); + return await _requestManager.MakeRequestAsync(channel, request, ct); } } \ No newline at end of file diff --git a/Network/UniScan.Network.Server/Handler/ClientsManager.cs b/Network/UniScan.Network.Server/Handler/ClientsManager.cs new file mode 100644 index 0000000..5eb2099 --- /dev/null +++ b/Network/UniScan.Network.Server/Handler/ClientsManager.cs @@ -0,0 +1,28 @@ +using DotNetty.Common.Concurrency; +using DotNetty.Transport.Channels; +using DotNetty.Transport.Channels.Groups; + +namespace UniScan.Network.Server.Handler; + +public class ClientsManager : ChannelHandlerAdapter +{ + private readonly SubscribableGroup _channels = new(); + public override bool IsSharable => true; + + public override void ChannelActive(IChannelHandlerContext context) + { + _channels.Add(context.Channel); + base.ChannelActive(context); + } + + public override void ChannelInactive(IChannelHandlerContext context) + { + _channels.Remove(context.Channel); + base.ChannelInactive(context); + } + + public async Task BroadcastAsync(IPacket packet) + { + await _channels.BroadcastAsync(packet); + } +} \ No newline at end of file diff --git a/Network/UniScan.Network.Server/ServerSocket.cs b/Network/UniScan.Network.Server/ServerSocket.cs index a1bd8b4..4437d36 100755 --- a/Network/UniScan.Network.Server/ServerSocket.cs +++ b/Network/UniScan.Network.Server/ServerSocket.cs @@ -12,6 +12,7 @@ using UniScan.Network.Packet.Packets.Serverbound.Client; using UniScan.Network.Packet.PayloadPart; using UniScan.Network.Request; +using UniScan.Network.Server.Handler; using UniScan.Network.Socket; using UniScan.Network.Util; @@ -40,6 +41,8 @@ public class ServerSocket : ISocket private readonly RequestManager _requestManager = new(); + public ClientsManager ClientManager { get; } = new(); + // TODO IHostMethod public ServerSocket(UniScanServerChannelInitializer channelInitializer, int port) { @@ -70,6 +73,7 @@ public async Task StartAsync() pipeline.AddLast(ChannelInitializer); pipeline.AddFirst(new ConnectionStateTracker()); + pipeline.AddFirst(ClientManager); pipeline.AddLast(new ResponseHandler(_requestManager)); })); @@ -82,7 +86,7 @@ public async Task StartAsync() await StopAsync(); } } - + public async Task StopAsync() { if (_requestManager != null) await _requestManager.RejectAllAsync(new OperationCanceledException("Socket is shutting down")).ContinueWith(_ => _requestManager.DisposeAsync()); diff --git a/Network/UniScan.Network/Constants.cs b/Network/UniScan.Network/Constants.cs index 3c52c21..c35d031 100755 --- a/Network/UniScan.Network/Constants.cs +++ b/Network/UniScan.Network/Constants.cs @@ -2,5 +2,5 @@ namespace UniScan.Network; public class Constants { - public const int ProtocolVersion = 2; + public const int ProtocolVersion = 3; } \ No newline at end of file diff --git a/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs b/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs index baf0544..6952dd3 100644 --- a/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs +++ b/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs @@ -6,5 +6,5 @@ namespace UniScan.Network.Data.Info.Remote; public readonly record struct RemoteLink( [property: Key(0)] Uri? IconUrl, [property: Key(1)] string Name, - [property: Key(2)] string Url + [property: Key(2)] Uri Url ); \ No newline at end of file diff --git a/Network/UniScan.Network/Packet/Packets/Clientbound/Device/ScannerRegistrationPacket.cs b/Network/UniScan.Network/Packet/Packets/Clientbound/Device/ScannerRegistrationPacket.cs deleted file mode 100644 index cb1e1bf..0000000 --- a/Network/UniScan.Network/Packet/Packets/Clientbound/Device/ScannerRegistrationPacket.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Shiki.Common.Identity; -using Shiki.Common.Identity.Slug; -using Shiki.Common.Identity.Slug.Formatting.Formatters; -using UniScan.Network.Packet.PayloadPart; - -namespace UniScan.Network.Packet.Packets.Clientbound.Device; - -[RegistryPacket("UniScan", "packet", "clientbound", "device", "registration")] -public record ScannerRegistrationPacket( - string? DisplayName, - Guid? RequestId, - Slug ScannerIdentifier -) : IClientboundPacket, IResponsePayloadPart, ISelectedScannerPayloadPart; \ No newline at end of file diff --git a/Network/UniScan.Network/Packet/Packets/Serverbound/Subscription/SubscribePacket.cs b/Network/UniScan.Network/Packet/Packets/Serverbound/Subscription/SubscribePacket.cs index 37316b3..028d634 100644 --- a/Network/UniScan.Network/Packet/Packets/Serverbound/Subscription/SubscribePacket.cs +++ b/Network/UniScan.Network/Packet/Packets/Serverbound/Subscription/SubscribePacket.cs @@ -1,3 +1,4 @@ +using MessagePack; using Shiki.Common.Identity; using Shiki.Common.Identity.Slug; using Shiki.Common.Identity.Slug.Formatting.Formatters; @@ -15,5 +16,9 @@ namespace UniScan.Network.Packet.Packets.Serverbound.Subscription; /// The request ID (must be present, how do we enforce?) [RegistryPacket("UniScan", "packet", "serverbound", "subscription", "subscribe")] [RequiredHandlerPermission("UniScan", "permission", "subscription", "subscribe")] +[MessagePackObject] [method: RequestConstructor] -public partial record SubscribePacket(Slug ScannerIdentifier, Guid? RequestId) : IServerboundPacket, IRequiresAcceptedClientPayloadPart, ISelectedScannerPayloadPart, IRequiresAuthenticationPayloadPart, IRequestPayloadPart; \ No newline at end of file +public partial record SubscribePacket( + [property: Key(0)] Slug ScannerIdentifier, + [property: Key(1)] Guid? RequestId +) : IServerboundPacket, IRequiresAcceptedClientPayloadPart, ISelectedScannerPayloadPart, IRequiresAuthenticationPayloadPart, IRequestPayloadPart; \ No newline at end of file diff --git a/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs b/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs index 5ab4348..a954328 100644 --- a/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs +++ b/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs @@ -1,6 +1,7 @@ using DotNetty.Common.Concurrency; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Groups; +using R3; using Shiki.Common.Identity; using Shiki.Common.Identity.Slug; using Shiki.Common.Identity.Slug.Formatting.Formatters; @@ -19,16 +20,21 @@ public sealed class HostClientsHandler : IDisposable, IAsyncDisposable private readonly SubscribableGroup _subscribers = new(); + private IDisposable _disposable; + public HostClientsHandler(Slug scannerId, Scanner scanner) { _scannerId = scannerId; _scanner = scanner; - _scanner.OnStateUpdated += HandleState; + _disposable = _scanner.State.AsObservable().Subscribe(HandleState); } - private void HandleState(DeviceState state) + private void HandleState(DeviceState? state) { + if (state == null) + return; + // TODO it will be better to get difference of entire state and ship that out but atm this is best I can do //every 10 state updates lets send full state too to avoid desync StatePacket packet = new(state, null, _scannerId); @@ -40,13 +46,13 @@ private void HandleState(DeviceState state) public void Dispose() { - _scanner.OnStateUpdated -= HandleState; + _disposable.Dispose(); _ = _subscribers.CloseAllAsync(); } public async ValueTask DisposeAsync() { - _scanner.OnStateUpdated -= HandleState; + _disposable.Dispose(); await _subscribers.CloseAllAsync(); } } \ No newline at end of file diff --git a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs index efe38e4..fb0d1d5 100644 --- a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs +++ b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs @@ -26,8 +26,12 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, SubscribePacket { ctx.WriteAsync(new AcknowledgePacket(new TransportableBooleanResult(null), msg.RequestId)); - ctx.WriteAsync(new ScannerRegistrationPacket(host.DisplayName, msg.RequestId, msg.ScannerIdentifier)); - ctx.WriteAsync(new StatePacket(host.Scanner.State, msg.RequestId, host.Identifier)); + // ctx.WriteAsync(new ScannerRegistrationPacket(host.DisplayName, msg.RequestId, msg.ScannerIdentifier)); + if (host.Scanner.State.Value != null) + { + ctx.WriteAsync(new StatePacket(host.Scanner.State.Value, msg.RequestId, host.Identifier)); + } + ctx.Flush(); host.NetworkClients.AddClient(ctx.Channel); diff --git a/Server/UniScan.Server.Core/UniScanServer.cs b/Server/UniScan.Server.Core/UniScanServer.cs index 903633d..ee83ffd 100755 --- a/Server/UniScan.Server.Core/UniScanServer.cs +++ b/Server/UniScan.Server.Core/UniScanServer.cs @@ -15,6 +15,7 @@ using UniScan.Network; using UniScan.Network.Data.Info.Remote; using UniScan.Network.Data.Info.Software; +using UniScan.Network.Packet.Packets.Bidirectional.Status; using UniScan.Network.Packet.Packets.Clientbound.Remote; using UniScan.Network.Registry.Source.Sources; using UniScan.Network.Server; @@ -58,10 +59,14 @@ public class UniScanServer ); public static readonly RemoteInfo RemoteInfo = new( - "Hello, world!", - "Example description", + "UniScan Test Server", + "Soon I will make all of this configurable", new RemoteSettings(true), - new RemoteBranding(null, []), + new RemoteBranding(new Uri("https://github.com/UniScanProject.png?size=64"), [ + new RemoteLink(new Uri("https://github.com/UniScanProject.png?size=32"), "UniScan on GitHub", new Uri("https://github.com/UniScanProject/UniScan")), + new RemoteLink(new Uri("https://uniscan.dexrn.me/assets/logo_512x512.png"), "UniScan Web", new Uri("https://uniscan.dexrn.me")), + new RemoteLink(new Uri("https://dexrn.me/favicon.png"), "Developer's Website", new Uri("https://dexrn.me")) + ]), new RemoteSocial("Hello, world!", []) ); @@ -111,6 +116,11 @@ public async Task RunAsync(CancellationToken ct = default) public async Task ExitAsync() { Log.Information("Closing socket..."); + if (Socket is ServerSocket serverSocket) + { + await serverSocket.ClientManager.BroadcastAsync(new DisconnectPacket("Server shutting down...")); + } + await Socket.StopAsync(); Log.Information("Disconnecting scanners..."); From eb43378815559014c5c2e2287942b45dd20b5100 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Thu, 27 Aug 2026 00:07:28 -0700 Subject: [PATCH 09/20] feat!: announcements page, some bug fixes # UniScan.Client.App - `NotConnectedRemotePageViewModel` now handles disconnects better - `MainRemotePageVIew` is now tabbed, with an added Announcements page. # UniScan.Network - Added `RemoteAnnouncementAuthor` - I am almost certain that this will be replaced once we have a proper user system in place. ## Breaking Changes - The protocol version is now `4` - `RemoteAnnouncement` now stores a list of `RemoteAnnouncementAuthors` # UniScan.Network.Server - Added `SubscribableGroup.Contains(IChannel)` # UniScan.Server.Core - Added `HostClientsHandler.Contains(IChannel)` - Clients are now sent a falsy acknowledgement when they attempt to subscribe to the same scanner more than once - `UniScanServer._networkGroupManager` now temporarily uses `MultithreadedGroupManager` until settings and a provider is made. - Fixed opening and never closing the file handle in `ScannerMeta.SaveAsync` --- .../Pages/NotConnectedRemotePageViewModel.cs | 24 +++++- .../Views/Pages/MainRemotePageView.axaml | 74 ++++++++++++++++--- .../SubscribableGroup.cs | 2 + Network/UniScan.Network/Constants.cs | 2 +- .../Data/Info/Remote/RemoteAnnouncement.cs | 11 ++- .../Data/Info/Remote/RemoteSocial.cs | 2 +- .../UniScan.Network/Packet/PacketEncoder.cs | 8 +- .../Host/Network/HostClientsHandler.cs | 2 + .../Filter/AcceptedClientPacketFilter.cs | 2 + .../ClientSoftwareInfoPacketHandler.cs | 2 +- .../Handler/SubscribePacketHandler.cs | 36 +++++++-- Server/UniScan.Server.Core/UniScanServer.cs | 18 ++++- Server/UniScan.Server.Host/ScannerMeta.cs | 8 +- 13 files changed, 159 insertions(+), 32 deletions(-) diff --git a/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs b/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs index 615f408..f6335f7 100644 --- a/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs +++ b/Client/UniScan.Client.App/ViewModels/Pages/NotConnectedRemotePageViewModel.cs @@ -1,6 +1,8 @@ using System; +using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; +using R3; using UniScan.Client.Core.Remote; using UniScan.Client.Core.Remote.Pipeline; @@ -13,22 +15,40 @@ public partial class NotConnectedRemotePageViewModel(IServiceProvider provider, public bool HasConnectionMethod { get; set; } = remote.ConnectionMethod != null; public event Action? OnConnecting; - public event Action? OnConnectFailed; + public event Action? OnConnectFailed; + + private CancellationTokenSource? _cts; + private IDisposable? _disposable; [RelayCommand] public async Task OnConnectClicked() { RemoteConnectionPipeline pipeline = new(); + + _cts = new CancellationTokenSource(); + _disposable = Remote.Connected.Skip(1).Subscribe(OnConnectionStatusChanged); OnConnecting?.Invoke(pipeline); try { await pipeline.Pipeline.RunAsync(new RemoteConnectionPipeline.TaskContexts.ConnectionContext(provider, - remote)); + remote), _cts.Token); } catch (Exception e) { OnConnectFailed?.Invoke(e); } } + + private void OnConnectionStatusChanged(bool connected) + { + if (!connected) + { + _cts?.Cancel(); + _cts = null; + + _disposable?.Dispose(); + _disposable = null; + } + } } \ No newline at end of file diff --git a/Client/UniScan.Client.App/Views/Pages/MainRemotePageView.axaml b/Client/UniScan.Client.App/Views/Pages/MainRemotePageView.axaml index 4585b87..ab0c14a 100644 --- a/Client/UniScan.Client.App/Views/Pages/MainRemotePageView.axaml +++ b/Client/UniScan.Client.App/Views/Pages/MainRemotePageView.axaml @@ -8,6 +8,7 @@ xmlns:pages1="clr-namespace:UniScan.Client.App.ViewModels.Pages" xmlns:controls2="clr-namespace:UniScan.Client.App.Views.Controls" xmlns:conversion="clr-namespace:UniScan.Client.App.UI.Conversion" + xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Pages.MainRemotePageView" x:DataType="pages1:MainRemotePageViewModel"> @@ -38,18 +39,67 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Network/UniScan.Network.Server/SubscribableGroup.cs b/Network/UniScan.Network.Server/SubscribableGroup.cs index c21bb51..bc545b4 100644 --- a/Network/UniScan.Network.Server/SubscribableGroup.cs +++ b/Network/UniScan.Network.Server/SubscribableGroup.cs @@ -18,6 +18,8 @@ public void Add(IChannel channel) } public void Remove(IChannel channel) => _channels.TryRemove(channel.Id, out _); + + public bool Contains(IChannel channel) => _channels.ContainsKey(channel.Id); public Task BroadcastAsync(TPacket packet) where TPacket : IPacket => _channels.IsEmpty ? Task.CompletedTask : Task.WhenAll(_channels.Values.Select(c => c.WriteAndFlushAsync(packet))); diff --git a/Network/UniScan.Network/Constants.cs b/Network/UniScan.Network/Constants.cs index c35d031..078e451 100755 --- a/Network/UniScan.Network/Constants.cs +++ b/Network/UniScan.Network/Constants.cs @@ -2,5 +2,5 @@ namespace UniScan.Network; public class Constants { - public const int ProtocolVersion = 3; + public const int ProtocolVersion = 4; } \ No newline at end of file diff --git a/Network/UniScan.Network/Data/Info/Remote/RemoteAnnouncement.cs b/Network/UniScan.Network/Data/Info/Remote/RemoteAnnouncement.cs index c7c95bb..0c3a2e2 100644 --- a/Network/UniScan.Network/Data/Info/Remote/RemoteAnnouncement.cs +++ b/Network/UniScan.Network/Data/Info/Remote/RemoteAnnouncement.cs @@ -2,10 +2,19 @@ namespace UniScan.Network.Data.Info.Remote; +//TODO in future, refer to a User instead +//and then let the remote provide a good enough API to return info on the user +[MessagePackObject] +public readonly record struct RemoteAnnouncementAuthor( + [property: Key(0)] string DisplayName, + [property: Key(1)] Uri? Avatar +); + [MessagePackObject] public readonly record struct RemoteAnnouncement( [property: Key(0)] string Title, [property: Key(1)] string Body, [property: Key(2)] DateTimeOffset Published, - [property: Key(3)] List Edits + [property: Key(3)] List Edits, + [property: Key(4)] List Authors ); \ No newline at end of file diff --git a/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs b/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs index f1879ad..7a325be 100644 --- a/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs +++ b/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs @@ -11,5 +11,5 @@ namespace UniScan.Network.Data.Info.Remote; [MessagePackObject] public readonly record struct RemoteSocial( [property: Key(0)] string? MessageOfTheDay, - [property: Key(1)] Dictionary, RemoteAnnouncement> Announcements + [property: Key(1)] Dictionary, RemoteAnnouncement> Announcements//TODO make this list of IDs, so that we aren't sending such a large packet if there are many announcements ); \ No newline at end of file diff --git a/Network/UniScan.Network/Packet/PacketEncoder.cs b/Network/UniScan.Network/Packet/PacketEncoder.cs index 96dfcad..194d332 100755 --- a/Network/UniScan.Network/Packet/PacketEncoder.cs +++ b/Network/UniScan.Network/Packet/PacketEncoder.cs @@ -40,11 +40,11 @@ protected override void Encode(IChannelHandlerContext context, IPacket message, byte[] msg = MessagePackSerializer.Serialize(type, message, options, ct); #if DEBUG - var lines = msg.Chunk(16) - .Select(chunk => string.Join(" ", Convert.ToHexString(chunk).Chunk(2) - .Select(c => new string(c)))); + var lines = msg.Chunk(16) + .Select(chunk => string.Join(" ", Convert.ToHexString(chunk).Chunk(2) + .Select(c => new string(c)))); - Log.Debug("{lines}", string.Join(Environment.NewLine, lines)); + Log.Debug("sent: {lines}", string.Join(Environment.NewLine, lines)); #endif output.WriteBytes(msg); diff --git a/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs b/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs index a954328..c3c0328 100644 --- a/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs +++ b/Server/UniScan.Server.Core/Host/Network/HostClientsHandler.cs @@ -44,6 +44,8 @@ private void HandleState(DeviceState? state) public void AddClient(IChannel channel) => _subscribers.Add(channel); + public bool Contains(IChannel channel) => _subscribers.Contains(channel); + public void Dispose() { _disposable.Dispose(); diff --git a/Server/UniScan.Server.Core/Module/Modules/Internal/Filter/AcceptedClientPacketFilter.cs b/Server/UniScan.Server.Core/Module/Modules/Internal/Filter/AcceptedClientPacketFilter.cs index 5b43d8d..61e84a3 100644 --- a/Server/UniScan.Server.Core/Module/Modules/Internal/Filter/AcceptedClientPacketFilter.cs +++ b/Server/UniScan.Server.Core/Module/Modules/Internal/Filter/AcceptedClientPacketFilter.cs @@ -1,3 +1,4 @@ +using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using Serilog; using Serilog.Core; @@ -20,6 +21,7 @@ public override void ChannelRead(IChannelHandlerContext context, object message) if (!context.Channel.HasAttribute(ClientAttributes.SoftwareInfoAttribute)) { _logger.Error("Client '{RemoteAddress}' sent packet of type '{Type}' without completing handshake.", context.Channel.RemoteAddress, message.GetType().FullName); + ReferenceCountUtil.Release(message); context.WriteAndFlushAsync(new DisconnectPacket("Client has not completed handshake.")).ContinueWith(_ => context.CloseAsync()); return; diff --git a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/ClientSoftwareInfoPacketHandler.cs b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/ClientSoftwareInfoPacketHandler.cs index 1e824eb..b1da680 100644 --- a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/ClientSoftwareInfoPacketHandler.cs +++ b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/ClientSoftwareInfoPacketHandler.cs @@ -32,7 +32,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, ClientSoftwareI { _logger.Information("Disconnecting client {ChannelIp} due to protocol version mismatch. (expected: {ServerProtocolVer}, got: {ProtocolVer})", ctx.Channel.RemoteAddress, Constants.ProtocolVersion, msg.Info.ProtocolVersion); - ctx.WriteAndFlushAsync(new DisconnectPacket($"Client protocol version does not match expected '{Constants.ProtocolVersion}'")).ContinueWith(_ => ctx.CloseAsync()); + ctx.WriteAndFlushAsync(new DisconnectPacket($"Client protocol version '{msg.Info.ProtocolVersion}' does not match expected '{Constants.ProtocolVersion}'")).ContinueWith(_ => ctx.CloseAsync()); return; } diff --git a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs index fb0d1d5..94453cd 100644 --- a/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs +++ b/Server/UniScan.Server.Core/Module/Modules/Internal/Handler/SubscribePacketHandler.cs @@ -1,6 +1,8 @@ using System.ComponentModel.Design; using DotNetty.Transport.Channels; +using Serilog; using Shiki.Common.Result; +using Shiki.Common.Result.Serialization.Types; using Shiki.Common.Util; using UniScan.Network.Packet.Packets.Bidirectional.Status; using UniScan.Network.Packet.Packets.Clientbound.Device; @@ -13,32 +15,54 @@ namespace UniScan.Server.Core.Module.Modules.Internal.Handler; /// Handles incoming SubscribePackets, used for clients to subscribe to and receive updates on a scanner /// /// The host manager -public class SubscribePacketHandler(ScannerHostManager scannerHostManager) : SimpleChannelInboundHandler +public class SubscribePacketHandler(ScannerHostManager scannerHostManager) + : SimpleChannelInboundHandler { private readonly ScannerHostManager _scannerHostManager = scannerHostManager; + private readonly ILogger _logger = Log.ForContext(); protected override void ChannelRead0(IChannelHandlerContext ctx, SubscribePacket msg) { if (msg.RequestId == null) return; - + if (_scannerHostManager.Scanners.TryGetValue(msg.ScannerIdentifier, out ScannerHost? host)) { + if (host.NetworkClients.Contains(ctx.Channel)) + { + _logger.Warning("Client attempted to subscribe to already subscribed device. Discarding."); + + ctx.WriteAndFlushAsync( + new AcknowledgePacket( + new BooleanResult( + new InvalidOperationException("You are already subscribed to this device.") + ).GetTransportableResult(), + msg.RequestId) + ); + return; + } + + ctx.WriteAsync(new AcknowledgePacket(new TransportableBooleanResult(null), msg.RequestId)); - - // ctx.WriteAsync(new ScannerRegistrationPacket(host.DisplayName, msg.RequestId, msg.ScannerIdentifier)); if (host.Scanner.State.Value != null) { ctx.WriteAsync(new StatePacket(host.Scanner.State.Value, msg.RequestId, host.Identifier)); } ctx.Flush(); - + host.NetworkClients.AddClient(ctx.Channel); } else { - ctx.WriteAndFlushAsync(new AcknowledgePacket(new BooleanResult(new KeyNotFoundException("Scanner not found")).GetTransportableResult(), msg.RequestId)); + _logger.Warning("Client attempted to subscribe to nonexistent device '{Value}'. Discarding.", msg.ScannerIdentifier); + + ctx.WriteAndFlushAsync(new + AcknowledgePacket(new + BooleanResult< + Exception>(new + KeyNotFoundException("Scanner not found")) + .GetTransportableResult(), msg.RequestId)); } } } \ No newline at end of file diff --git a/Server/UniScan.Server.Core/UniScanServer.cs b/Server/UniScan.Server.Core/UniScanServer.cs index ee83ffd..d6cf1b9 100755 --- a/Server/UniScan.Server.Core/UniScanServer.cs +++ b/Server/UniScan.Server.Core/UniScanServer.cs @@ -9,6 +9,8 @@ using Serilog.Core; using Shiki.Common.Extensions; using Shiki.Common.Identity; +using Shiki.Common.Identity.Slug; +using Shiki.Common.Identity.Slug.Formatting.Formatters; using Shiki.Common.Util; using Shiki.ModuleManagement; using UniScan.Device.Device; @@ -67,7 +69,19 @@ public class UniScanServer new RemoteLink(new Uri("https://uniscan.dexrn.me/assets/logo_512x512.png"), "UniScan Web", new Uri("https://uniscan.dexrn.me")), new RemoteLink(new Uri("https://dexrn.me/favicon.png"), "Developer's Website", new Uri("https://dexrn.me")) ]), - new RemoteSocial("Hello, world!", []) + new RemoteSocial("Hello, world!", new Dictionary, RemoteAnnouncement> { + [new Slug("work_in_progress")] = new( + "Work In Progress", + "All of this is still a work in progress, in the future, this will all be configurable.", + DateTimeOffset.UtcNow, + [], + [ + new RemoteAnnouncementAuthor( + "Dexrn ZacAttack", + new Uri("https://github.com/DexrnZacAttack.png?size=64") + ) + ]) + }) ); public UniScanServer(SessionManager sessionManager, ServerSocketInitializer socketInitializer, PacketRegistry packetRegistry, ModuleStorage moduleStorage) @@ -75,7 +89,7 @@ public UniScanServer(SessionManager sessionManager, ServerSocketInitializer sock SessionManager = sessionManager; _moduleStorage = moduleStorage; _packetRegistry = packetRegistry; - _networkGroupManager = new LibUvGroupManager(); //new MultithreadedGroupManager(); + _networkGroupManager = new MultithreadedGroupManager(); ScannerManager = new ScannerHostManager(); diff --git a/Server/UniScan.Server.Host/ScannerMeta.cs b/Server/UniScan.Server.Host/ScannerMeta.cs index 41d6c28..97eb325 100644 --- a/Server/UniScan.Server.Host/ScannerMeta.cs +++ b/Server/UniScan.Server.Host/ScannerMeta.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; +using System.IO; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Schema; +using System.Threading.Tasks; using Shiki.Common.Identity; using Shiki.Common.Identity.Slug; using Shiki.Common.Identity.Slug.Formatting.Formatters; @@ -38,7 +41,8 @@ public async Task, ScannerHostDto>> LoadDtos public async Task SaveAsync(ScannerHostManager hostManager) { string p = Path.Combine(root, "scanners.json"); - - await JsonSerializer.SerializeAsync(File.OpenWrite(p), hostManager.Scanners.ToDictionary(h => h.Key, h => new ScannerHostDto(h.Value)), jsonOptions); + + await using FileStream fs = new(p, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + await JsonSerializer.SerializeAsync(fs, hostManager.Scanners.ToDictionary(h => h.Key, h => new ScannerHostDto(h.Value)), jsonOptions); } } \ No newline at end of file From 7d43c177ca042bb02ec2cb3f2d07501b832016a9 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Thu, 27 Aug 2026 00:26:25 -0700 Subject: [PATCH 10/20] feat: improve disconnectedremotepageview and notconnectedremotepageview --- .../Pages/DisconnectedRemotePageView.axaml | 10 ++++++---- .../Pages/NotConnectedRemotePageView.axaml | 17 +++++++++++++---- .../Pipeline/RemoteConnectionPipeline.Tasks.cs | 6 +++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Client/UniScan.Client.App/Views/Pages/DisconnectedRemotePageView.axaml b/Client/UniScan.Client.App/Views/Pages/DisconnectedRemotePageView.axaml index b001403..c2bf431 100644 --- a/Client/UniScan.Client.App/Views/Pages/DisconnectedRemotePageView.axaml +++ b/Client/UniScan.Client.App/Views/Pages/DisconnectedRemotePageView.axaml @@ -7,11 +7,13 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Pages.DisconnectedRemotePageView" x:DataType="pages1:DisconnectedRemotePageViewModel"> - - - + + + -