From 6f26345b755bdcaf686451d0f090e24e461428e3 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 08:46:11 +0800 Subject: [PATCH] feat(ipc): add IProcessInfoProvider + IUpdateEventListener + event bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IProcessInfoProvider interface for Client-to-Upgrade IPC - NamedPipeProcessInfoProvider — preferred, no file residue - EncryptedFileProcessInfoProvider — AES fallback - IUpdateEventListener — batch event registration interface - ProgressEventArgs — AddListenerProgress event type Closes #340 --- .../Event/IUpdateEventListener.cs | 24 +++++ .../Ipc/IProcessInfoProvider.cs | 96 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs create mode 100644 src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs diff --git a/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs b/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs new file mode 100644 index 00000000..2c450d63 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs @@ -0,0 +1,24 @@ +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Download.Models; +using GeneralUpdate.Core.Event; + +namespace GeneralUpdate.Core.Event; + +/// Batch event registration — implement once, register once. +public interface IUpdateEventListener +{ + void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args); + void OnDownloadCompleted(MultiDownloadCompletedEventArgs args); + void OnDownloadError(MultiDownloadErrorEventArgs args); + void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args); + void OnUpdateInfo(UpdateInfoEventArgs args); + void OnException(ExceptionEventArgs args); + void OnProgress(DownloadProgress progress); +} + +/// Progress event args for AddListenerProgress. +public class ProgressEventArgs : EventArgs +{ + public DownloadProgress Progress { get; } + public ProgressEventArgs(DownloadProgress progress) => Progress = progress; +} diff --git a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs new file mode 100644 index 00000000..676dfbd5 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.JsonContext; + +namespace GeneralUpdate.Core.Ipc; + +/// IPC provider for Client-to-Upgrade process communication. +public interface IProcessInfoProvider +{ + Task SendAsync(ProcessInfo info, CancellationToken token = default); + Task ReceiveAsync(CancellationToken token = default); +} + +/// Named pipe IPC — preferred (no file residue). +public class NamedPipeProcessInfoProvider : IProcessInfoProvider +{ + private readonly string _pipeName; + + public NamedPipeProcessInfoProvider(string pipeName = "GeneralUpdate.IPC") + => _pipeName = pipeName; + + public async Task SendAsync(ProcessInfo info, CancellationToken token = default) + { + using var server = new NamedPipeServerStream(_pipeName, PipeDirection.Out); + await server.WaitForConnectionAsync(token).ConfigureAwait(false); + var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo); + var bytes = Encoding.UTF8.GetBytes(json); + await server.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); + } + + public async Task ReceiveAsync(CancellationToken token = default) + { + using var client = new NamedPipeClientStream(".", _pipeName, PipeDirection.In); + await client.ConnectAsync(5000, token).ConfigureAwait(false); + using var ms = new MemoryStream(); + var buffer = new byte[4096]; + int read; + while ((read = await client.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false)) > 0) + await ms.WriteAsync(buffer, 0, read, token).ConfigureAwait(false); + var json = Encoding.UTF8.GetString(ms.ToArray()); + return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo); + } +} + +/// Encrypted file fallback IPC (AES). +public class EncryptedFileProcessInfoProvider : IProcessInfoProvider +{ + private static readonly byte[] Key = SHA256.Create() + .ComputeHash(Encoding.UTF8.GetBytes("GeneralUpdate.ProcessInfo.IPC.v1")); + private static readonly byte[] IV = new byte[16] { 0x47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + private readonly string _filePath; + + public EncryptedFileProcessInfoProvider(string? basePath = null) + { + var dir = basePath ?? Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc"); + Directory.CreateDirectory(dir); + _filePath = Path.Combine(dir, $"{Guid.NewGuid():N}.enc"); + } + + public Task SendAsync(ProcessInfo info, CancellationToken token = default) + { + var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo); + var plainBytes = Encoding.UTF8.GetBytes(json); + using var aes = Aes.Create(); + aes.Key = Key; aes.IV = IV; + using var enc = aes.CreateEncryptor(); + var cipher = enc.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + File.WriteAllBytes(_filePath, cipher); + return Task.CompletedTask; + } + + public Task ReceiveAsync(CancellationToken token = default) + { + if (!File.Exists(_filePath)) return Task.FromResult(null); + try + { + var cipher = File.ReadAllBytes(_filePath); + using var aes = Aes.Create(); + aes.Key = Key; aes.IV = IV; + using var dec = aes.CreateDecryptor(); + var plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); + var json = Encoding.UTF8.GetString(plain); + return Task.FromResult( + JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo)); + } + finally { try { File.Delete(_filePath); } catch { } } + } +}