From 2a922280260b727fbddb17a75805f00478f6d0ba Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 18:27:19 +0800 Subject: [PATCH 1/5] feat: integrate IProcessInfoProvider IPC into core workflow Replace Environments (encrypted file) IPC with AutoProcessInfoProvider (NamedPipe > SharedMemory > EncryptedFile auto-fallback) for all ProcessInfo transfer paths: - ClientUpdateStrategy: send ProcessInfo via AutoProcessInfoProvider after building it, providing zero-file-residue IPC for the upgrade path - SilentPollOrchestrator: replace Environments.SetEnvironmentVariable with AutoProcessInfoProvider.SendAsync in OnProcessExit - GeneralUpdateBootstrap.InitializeFromEnvironment: replace Environments.GetEnvironmentVariable with ReceiveAsync Closes #408 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 10 +++---- .../Silent/SilentPollOrchestrator.cs | 26 ++++++++++++++++--- .../Strategy/ClientUpdateStrategy.cs | 19 +++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 20d213e9..a6f41eff 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -16,6 +16,7 @@ using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core.Network; using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Download.Reporting; namespace GeneralUpdate.Core; @@ -256,11 +257,10 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func SharedMemory > EncryptedFile auto-fallback). + // Sync wait is acceptable here — the constructor runs once and the + // IPC providers use short timeouts (5s NamedPipe, immediate MMF/file). + var processInfo = new AutoProcessInfoProvider().ReceiveAsync().GetAwaiter().GetResult(); if (processInfo == null) return; BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats); diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index 5b991d48..6eb22119 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -17,6 +17,7 @@ using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Core.Hooks; using GeneralUpdate.Core.JsonContext; +using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Strategy; namespace GeneralUpdate.Core.Silent; @@ -36,6 +37,7 @@ public class SilentPollOrchestrator : IDisposable private int _updaterStarted; private IUpdateHooks? _hooks; private IUpdateReporter? _reporter; + private Configuration.ProcessInfo? _preparedProcessInfo; public SilentPollOrchestrator(GlobalConfigInfo configInfo, SilentOptions options) { @@ -160,13 +162,13 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token) StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, BlackListManager.Instance.SkipDirectorys); - // Build ProcessInfo - var processInfo = ConfigurationMapper.MapToProcessInfo( + // Build ProcessInfo and store for IPC delivery on process exit + _preparedProcessInfo = ConfigurationMapper.MapToProcessInfo( _configInfo, new List(), BlackListManager.Instance.BlackFormats.ToList(), BlackListManager.Instance.BlackFiles.ToList(), BlackListManager.Instance.SkipDirectorys.ToList()); - _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); + _configInfo.ProcessInfo = JsonSerializer.Serialize(_preparedProcessInfo, ProcessInfoJsonContext.Default.ProcessInfo); // ═══ Reporter: update started ═══ var startTime = DateTimeOffset.UtcNow; @@ -311,12 +313,28 @@ private void OnProcessExit(object? sender, EventArgs e) try { - Environments.SetEnvironmentVariable("ProcessInfo", _configInfo.ProcessInfo ?? string.Empty); var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); if (File.Exists(updaterPath)) { + // Start the upgrade process first (it will block on ReceiveAsync) GeneralTracer.Info($"SilentPollOrchestrator: launching updater {updaterPath}"); Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath }); + + // Send ProcessInfo via IPC — the upgrade process connects as client + if (_preparedProcessInfo != null) + { + new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); + GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC."); + } + } + else + { + // No separate updater exe — write via IPC for fallback compatibility + if (_preparedProcessInfo != null) + { + new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); + GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC (no updater exe)."); + } } } catch (Exception ex) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 96a536ee..efb767d5 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -12,6 +12,7 @@ using GeneralUpdate.Core.Event; using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Core.JsonContext; +using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Network; namespace GeneralUpdate.Core.Strategy; @@ -169,14 +170,20 @@ private async Task ExecuteStandardWorkflowAsync() Format = _configInfo.Format ?? "ZIP" }).ToList(); - _configInfo.ProcessInfo = JsonSerializer.Serialize( - ConfigurationMapper.MapToProcessInfo( - _configInfo, downloadVersions, - BlackListManager.Instance.BlackFormats.ToList(), - BlackListManager.Instance.BlackFiles.ToList(), - BlackListManager.Instance.SkipDirectorys.ToList()), + var processInfo = ConfigurationMapper.MapToProcessInfo( + _configInfo, downloadVersions, + BlackListManager.Instance.BlackFormats.ToList(), + BlackListManager.Instance.BlackFiles.ToList(), + BlackListManager.Instance.SkipDirectorys.ToList()); + + // Keep JSON string for backward compatibility (GlobalConfigInfo.ProcessInfo) + _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); + // Wire ProcessInfo via IPC (NamedPipe > SharedMemory > EncryptedFile) + await new AutoProcessInfoProvider().SendAsync(processInfo).ConfigureAwait(false); + GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via IPC (AutoProcessInfoProvider)."); + // Backup — conditionally skipped when BackupEnabled is false if (_configInfo.BackupEnabled != false) { From 91fc9e4182233827f57451d516b99cd92933645b Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 18:35:58 +0800 Subject: [PATCH 2/5] fix: add CancellationToken timeout to IPC SendAsync calls Prevent indefinite blocking when no upgrade process connects: - ClientUpdateStrategy: 3s NamedPipe timeout, auto-falls back to SharedMemory/EncryptedFile (normal client flow has no separate upgrade process) - SilentPollOrchestrator.OnProcessExit: 5s NamedPipe timeout with SharedMemory/EncryptedFile auto-fallback for timing gaps Related #408 --- .../Silent/SilentPollOrchestrator.cs | 27 +++++++++---------- .../Strategy/ClientUpdateStrategy.cs | 8 ++++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index 6eb22119..48c2b3c7 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -314,27 +314,24 @@ private void OnProcessExit(object? sender, EventArgs e) try { var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); + + // Start the upgrade process first — it will call ReceiveAsync in its constructor. + // We then call SendAsync which creates the NamedPipe server; the upgrade's + // client connects to it. Auto-fallback (SharedMemory > EncryptedFile) handles + // timing gaps where the named pipe handshake doesn't complete in time. if (File.Exists(updaterPath)) { - // Start the upgrade process first (it will block on ReceiveAsync) GeneralTracer.Info($"SilentPollOrchestrator: launching updater {updaterPath}"); Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath }); - - // Send ProcessInfo via IPC — the upgrade process connects as client - if (_preparedProcessInfo != null) - { - new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); - GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC."); - } } - else + + // Send ProcessInfo via IPC — 5s NamedPipe timeout to allow upgrade to connect, + // then SharedMemory/EncryptedFile auto-fallback if pipe isn't ready. + if (_preparedProcessInfo != null) { - // No separate updater exe — write via IPC for fallback compatibility - if (_preparedProcessInfo != null) - { - new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); - GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC (no updater exe)."); - } + using var ipcCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo, ipcCts.Token).GetAwaiter().GetResult(); + GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC."); } } catch (Exception ex) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index efb767d5..f57408f2 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Download; @@ -180,8 +181,11 @@ private async Task ExecuteStandardWorkflowAsync() _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); - // Wire ProcessInfo via IPC (NamedPipe > SharedMemory > EncryptedFile) - await new AutoProcessInfoProvider().SendAsync(processInfo).ConfigureAwait(false); + // Wire ProcessInfo via IPC (NamedPipe > SharedMemory > EncryptedFile auto-fallback). + // 3s timeout on NamedPipe: if no upgrade process connects (normal client flow), + // falls back to SharedMemory/EncryptedFile without blocking the pipeline. + using var ipcCts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + await new AutoProcessInfoProvider().SendAsync(processInfo, ipcCts.Token).ConfigureAwait(false); GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via IPC (AutoProcessInfoProvider)."); // Backup — conditionally skipped when BackupEnabled is false From 2671c5e87830fe0e02c08e34a287424430101311 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 18:40:38 +0800 Subject: [PATCH 3/5] refactor: simplify IPC to EncryptedFile only, remove NamedPipe/SharedMemory/Auto Per review feedback: EncryptedFile is the simplest and most reliable cross-platform IPC mechanism. Remove NamedPipe (connection timing issues), SharedMemory (platform-specific quirks), and Auto (fallback complexity). Changes: - IProcessInfoProvider.cs: retain only interface + EncryptedFileProcessInfoProvider - All call sites use EncryptedFileProcessInfoProvider directly - Remove CancellationToken timeouts (EncryptedFile is synchronous) Related #408 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 8 +- .../Ipc/IProcessInfoProvider.cs | 161 +----------------- .../Silent/SilentPollOrchestrator.cs | 8 +- .../Strategy/ClientUpdateStrategy.cs | 9 +- 4 files changed, 14 insertions(+), 172 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index a6f41eff..288a043a 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -257,10 +257,10 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func SharedMemory > EncryptedFile auto-fallback). - // Sync wait is acceptable here — the constructor runs once and the - // IPC providers use short timeouts (5s NamedPipe, immediate MMF/file). - var processInfo = new AutoProcessInfoProvider().ReceiveAsync().GetAwaiter().GetResult(); + // Read ProcessInfo via AES-encrypted file IPC. + // Sync wait is acceptable here — the constructor runs once and + // EncryptedFileProcessInfoProvider is synchronous. + var processInfo = new EncryptedFileProcessInfoProvider().ReceiveAsync().GetAwaiter().GetResult(); if (processInfo == null) return; BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats); diff --git a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs index c11efe4b..0951f025 100644 --- a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs +++ b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs @@ -1,13 +1,10 @@ using System; using System.IO; -using System.IO.MemoryMappedFiles; -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; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.JsonContext; @@ -20,38 +17,10 @@ public interface IProcessInfoProvider 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). +/// +/// AES-encrypted temporary file IPC — simplest, most reliable cross-platform approach. +/// File lives in %TEMP%/GeneralUpdate/ipc/ with a random name, auto-deleted after read. +/// public class EncryptedFileProcessInfoProvider : IProcessInfoProvider { private static readonly byte[] Key = SHA256.Create() @@ -97,125 +66,3 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default) finally { try { File.Delete(_filePath); } catch { } } } } - -/// Shared memory fallback IPC (Linux-friendly, no file residue). -public class SharedMemoryProcessInfoProvider : IProcessInfoProvider -{ - private readonly string _mapName; - private MemoryMappedFile? _mmf; - private const int MaxPayload = 4096; - - public SharedMemoryProcessInfoProvider(string mapName = "GeneralUpdate.IPC.Shm") - => _mapName = mapName; - - public Task SendAsync(ProcessInfo info, CancellationToken token = default) - { - token.ThrowIfCancellationRequested(); - var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo); - var bytes = Encoding.UTF8.GetBytes(json); - if (bytes.Length > MaxPayload - 4) - throw new InvalidOperationException($"ProcessInfo payload exceeds {MaxPayload - 4} bytes."); - - _mmf = MemoryMappedFile.CreateOrOpen(_mapName, MaxPayload); - using var accessor = _mmf.CreateViewAccessor(0, MaxPayload); - accessor.Write(0, bytes.Length); - accessor.WriteArray(4, bytes, 0, bytes.Length); - return Task.CompletedTask; - } - - public Task ReceiveAsync(CancellationToken token = default) - { - try - { - using var mmf = MemoryMappedFile.OpenExisting(_mapName); - using var accessor = mmf.CreateViewAccessor(0, MaxPayload); - int length = accessor.ReadInt32(0); - if (length <= 0 || length > MaxPayload - 4) - return Task.FromResult(null); - var bytes = new byte[length]; - accessor.ReadArray(4, bytes, 0, length); - var json = Encoding.UTF8.GetString(bytes); - return Task.FromResult( - JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo)); - } - catch (FileNotFoundException) - { - return Task.FromResult(null); - } - catch (DirectoryNotFoundException) - { - return Task.FromResult(null); - } - catch (Exception ex) when (ex is not OutOfMemoryException) - { - // Platform-specific failures (e.g. Linux /dev/shm not mounted) - GeneralTracer.Warn($"SharedMemoryProvider: receive failed: {ex.Message}"); - return Task.FromResult(null); - } - } -} - -/// -/// Auto-fallback IPC provider. Tries providers in order: -/// NamedPipe → SharedMemory → EncryptedFile. -/// On send, uses the first provider that succeeds. -/// On receive, waits for data from the most reliable available provider. -/// -public class AutoProcessInfoProvider : IProcessInfoProvider -{ - private readonly IProcessInfoProvider[] _providers; - - public AutoProcessInfoProvider() - { - _providers = new IProcessInfoProvider[] - { - new NamedPipeProcessInfoProvider(), - new SharedMemoryProcessInfoProvider(), - new EncryptedFileProcessInfoProvider() - }; - } - - public AutoProcessInfoProvider(params IProcessInfoProvider[] providers) - => _providers = providers; - - public async Task SendAsync(ProcessInfo info, CancellationToken token = default) - { - Exception? last = null; - foreach (var provider in _providers) - { - try - { - await provider.SendAsync(info, token).ConfigureAwait(false); - GeneralTracer.Debug($"AutoProcessInfoProvider: sent via {provider.GetType().Name}."); - return; - } - catch (Exception ex) - { - GeneralTracer.Warn($"AutoProcessInfoProvider: {provider.GetType().Name} failed: {ex.Message}"); - last = ex; - } - } - throw new InvalidOperationException("All IPC providers failed to send.", last); - } - - public async Task ReceiveAsync(CancellationToken token = default) - { - foreach (var provider in _providers) - { - try - { - var result = await provider.ReceiveAsync(token).ConfigureAwait(false); - if (result != null) - { - GeneralTracer.Debug($"AutoProcessInfoProvider: received via {provider.GetType().Name}."); - return result; - } - } - catch (Exception ex) - { - GeneralTracer.Warn($"AutoProcessInfoProvider: {provider.GetType().Name} receive failed: {ex.Message}"); - } - } - return null; - } -} diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index 48c2b3c7..72e996e4 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -325,13 +325,11 @@ private void OnProcessExit(object? sender, EventArgs e) Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath }); } - // Send ProcessInfo via IPC — 5s NamedPipe timeout to allow upgrade to connect, - // then SharedMemory/EncryptedFile auto-fallback if pipe isn't ready. + // Send ProcessInfo via AES-encrypted file IPC. if (_preparedProcessInfo != null) { - using var ipcCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - new AutoProcessInfoProvider().SendAsync(_preparedProcessInfo, ipcCts.Token).GetAwaiter().GetResult(); - GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via IPC."); + new EncryptedFileProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); + GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via encrypted file IPC."); } } catch (Exception ex) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index f57408f2..35c0cf9b 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -181,12 +181,9 @@ private async Task ExecuteStandardWorkflowAsync() _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); - // Wire ProcessInfo via IPC (NamedPipe > SharedMemory > EncryptedFile auto-fallback). - // 3s timeout on NamedPipe: if no upgrade process connects (normal client flow), - // falls back to SharedMemory/EncryptedFile without blocking the pipeline. - using var ipcCts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - await new AutoProcessInfoProvider().SendAsync(processInfo, ipcCts.Token).ConfigureAwait(false); - GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via IPC (AutoProcessInfoProvider)."); + // Wire ProcessInfo via AES-encrypted file IPC. + await new EncryptedFileProcessInfoProvider().SendAsync(processInfo).ConfigureAwait(false); + GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC."); // Backup — conditionally skipped when BackupEnabled is false if (_configInfo.BackupEnabled != false) From a0b8a36cf4d0e1ff0a4331c1a338aa677634e252 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 18:46:08 +0800 Subject: [PATCH 4/5] refactor: add sync Send/Receive to EncryptedFileProcessInfoProvider Replace all .ReceiveAsync().GetAwaiter().GetResult() and .SendAsync().GetAwaiter().GetResult() with direct sync calls. - Add Send() / Receive() sync methods to EncryptedFileProcessInfoProvider - GeneralUpdateBootstrap: use Receive() instead of ReceiveAsync().GetAwaiter() - SilentPollOrchestrator: use Send() instead of SendAsync().GetAwaiter() - ClientUpdateStrategy: use Send() instead of await SendAsync() Related #408 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 4 +--- .../Ipc/IProcessInfoProvider.cs | 18 +++++++++++++----- .../Silent/SilentPollOrchestrator.cs | 2 +- .../Strategy/ClientUpdateStrategy.cs | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 288a043a..a9bdc513 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -258,9 +258,7 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(FuncSynchronous send — all I/O is synchronous under the hood. + public void Send(ProcessInfo info) + { var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo); var plainBytes = Encoding.UTF8.GetBytes(json); using var aes = Aes.Create(); @@ -46,12 +52,15 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default) 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) + => Task.FromResult(Receive()); + + /// Synchronous receive — reads and deletes the encrypted file. + public ProcessInfo? Receive() { - if (!File.Exists(_filePath)) return Task.FromResult(null); + if (!File.Exists(_filePath)) return null; try { var cipher = File.ReadAllBytes(_filePath); @@ -60,8 +69,7 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default) 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)); + return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo); } finally { try { File.Delete(_filePath); } catch { } } } diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index 72e996e4..fa47dbc5 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -328,7 +328,7 @@ private void OnProcessExit(object? sender, EventArgs e) // Send ProcessInfo via AES-encrypted file IPC. if (_preparedProcessInfo != null) { - new EncryptedFileProcessInfoProvider().SendAsync(_preparedProcessInfo).GetAwaiter().GetResult(); + new EncryptedFileProcessInfoProvider().Send(_preparedProcessInfo); GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via encrypted file IPC."); } } diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 35c0cf9b..122e4baa 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -182,7 +182,7 @@ private async Task ExecuteStandardWorkflowAsync() ProcessInfoJsonContext.Default.ProcessInfo); // Wire ProcessInfo via AES-encrypted file IPC. - await new EncryptedFileProcessInfoProvider().SendAsync(processInfo).ConfigureAwait(false); + new EncryptedFileProcessInfoProvider().Send(processInfo); GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC."); // Backup — conditionally skipped when BackupEnabled is false From 1b689f763c7defe4110229a6fff075a37321a82c Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 18:49:02 +0800 Subject: [PATCH 5/5] fix: update tests for simplified EncryptedFile-only IPC Remove tests referencing deleted providers: - NamedPipeProvider_DetectsTimeout - SharedMemoryProvider_RoundTrip - SharedMemoryProvider_ReceiveWithoutSend_ReturnsNull - AutoProvider_FallsBackToEncryptedFile - AutoProvider_ThrowsWhenAllFail Replaced with EncryptedFileIpcTests covering: - Send/Receive roundtrip - Receive without send returns null - Data confidentiality and single-read guarantee - Async API delegates to sync Related #408 --- tests/CoreTest/Ipc/IpcFallbackTests.cs | 101 +++++-------------------- 1 file changed, 19 insertions(+), 82 deletions(-) diff --git a/tests/CoreTest/Ipc/IpcFallbackTests.cs b/tests/CoreTest/Ipc/IpcFallbackTests.cs index 5bb68716..2efa5c85 100644 --- a/tests/CoreTest/Ipc/IpcFallbackTests.cs +++ b/tests/CoreTest/Ipc/IpcFallbackTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Ipc; @@ -7,7 +5,7 @@ namespace CoreTest.Ipc; -public class IpcFallbackTests +public class EncryptedFileIpcTests { private static ProcessInfo CreateTestInfo(string appName, string currentVersion) { @@ -21,13 +19,13 @@ private static ProcessInfo CreateTestInfo(string appName, string currentVersion) } [Fact] - public async Task EncryptedFileProvider_RoundTrip() + public void Send_And_Receive_RoundTrip() { var provider = new EncryptedFileProcessInfoProvider(); var info = CreateTestInfo("TestApp", "1.0.0"); - await provider.SendAsync(info); - var received = await provider.ReceiveAsync(); + provider.Send(info); + var received = provider.Receive(); Assert.NotNull(received); Assert.Equal("TestApp", received!.AppName); @@ -35,101 +33,40 @@ public async Task EncryptedFileProvider_RoundTrip() } [Fact] - public async Task EncryptedFileProvider_ReceiveWithoutSend_ReturnsNull() + public void Receive_Without_Send_ReturnsNull() { var provider = new EncryptedFileProcessInfoProvider(); - var received = await provider.ReceiveAsync(); - Assert.Null(received); - } - - [Fact] - public async Task NamedPipeProvider_DetectsTimeout() - { - var provider = new NamedPipeProcessInfoProvider("TestPipe.Timeout"); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); - - await Assert.ThrowsAsync(() => - provider.ReceiveAsync(cts.Token)); - } - - [Fact] - public async Task SharedMemoryProvider_RoundTrip() - { - var mapName = $"GenUpd.Tests.{Guid.NewGuid():N}"; - - try - { - var provider = new SharedMemoryProcessInfoProvider(mapName); - var info = CreateTestInfo("TestApp.Shm", "2.0.0"); - - await provider.SendAsync(info); - var receiver = new SharedMemoryProcessInfoProvider(mapName); - var received = await receiver.ReceiveAsync(); - - Assert.NotNull(received); - Assert.Equal("TestApp.Shm", received!.AppName); - Assert.Equal("2.0.0", received.CurrentVersion); - } - catch (System.PlatformNotSupportedException) - { - // Shared memory not supported on this platform — skip - } - catch (System.IO.FileNotFoundException) - { - // Memory-mapped file already disposed — platform quirk, skip - } - } - - [Fact] - public async Task SharedMemoryProvider_ReceiveWithoutSend_ReturnsNull() - { - var provider = new SharedMemoryProcessInfoProvider("NonExistent.Shm"); - var received = await provider.ReceiveAsync(); + var received = provider.Receive(); Assert.Null(received); } [Fact] - public async Task AutoProvider_FallsBackToEncryptedFile() + public void Data_Confidentiality_And_SingleRead() { - var provider = new AutoProcessInfoProvider( - new EncryptedFileProcessInfoProvider() - ); - var info = CreateTestInfo("TestApp.Auto", "3.0.0"); + var provider = new EncryptedFileProcessInfoProvider(); + var info = CreateTestInfo("SecureApp", "1.0.0"); - await provider.SendAsync(info); - var received = await provider.ReceiveAsync(); + provider.Send(info); + var received = provider.Receive(); Assert.NotNull(received); - Assert.Equal("TestApp.Auto", received!.AppName); - } - - [Fact] - public async Task AutoProvider_ThrowsWhenAllFail() - { - var provider = new AutoProcessInfoProvider( - new NamedPipeProcessInfoProvider("NonExistent." + Guid.NewGuid().ToString("N")), - new SharedMemoryProcessInfoProvider("NonExistent." + Guid.NewGuid().ToString("N")) - ); - var info = CreateTestInfo("FailAll", "1.0.0"); + Assert.Equal("SecureApp", received!.AppName); - await Assert.ThrowsAnyAsync(() => - provider.SendAsync(info, new CancellationToken(true))); + // Second receive should return null (file was auto-deleted) + var second = provider.Receive(); + Assert.Null(second); } [Fact] - public async Task EncryptedFileProvider_DataConfidentiality() + public async Task Async_Api_Delegates_To_Sync() { var provider = new EncryptedFileProcessInfoProvider(); - var info = CreateTestInfo("SecureApp", "1.0.0"); + var info = CreateTestInfo("AsyncApp", "1.0.0"); await provider.SendAsync(info); - var received = await provider.ReceiveAsync(); - Assert.NotNull(received); - Assert.Equal("SecureApp", received!.AppName); - // Second receive should return null (file was deleted) - var second = await provider.ReceiveAsync(); - Assert.Null(second); + Assert.NotNull(received); + Assert.Equal("AsyncApp", received!.AppName); } }