Skip to content

Commit 9a1f15e

Browse files
authored
feat: Integrate IProcessInfoProvider IPC into core workflow (#413)
* 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 * 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 * 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 * 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 * 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
1 parent 209518b commit 9a1f15e

5 files changed

Lines changed: 70 additions & 259 deletions

File tree

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using GeneralUpdate.Core.Strategy;
1717
using GeneralUpdate.Core.Network;
1818
using GeneralUpdate.Core.Hooks;
19+
using GeneralUpdate.Core.Ipc;
1920
using GeneralUpdate.Core.Download.Reporting;
2021

2122
namespace GeneralUpdate.Core;
@@ -256,11 +257,8 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs
256257

257258
private void InitializeFromEnvironment()
258259
{
259-
var json = Environments.GetEnvironmentVariable("ProcessInfo");
260-
if (string.IsNullOrWhiteSpace(json)) return;
261-
262-
var processInfo = JsonSerializer.Deserialize(
263-
json, ProcessInfoJsonContext.Default.ProcessInfo);
260+
// Read ProcessInfo via AES-encrypted file IPC.
261+
var processInfo = new EncryptedFileProcessInfoProvider().Receive();
264262
if (processInfo == null) return;
265263

266264
BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats);
Lines changed: 17 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using System;
22
using System.IO;
3-
using System.IO.MemoryMappedFiles;
4-
using System.IO.Pipes;
53
using System.Security.Cryptography;
64
using System.Text;
75
using System.Text.Json;
86
using System.Threading;
97
using System.Threading.Tasks;
10-
using GeneralUpdate.Core;
118
using GeneralUpdate.Core.Configuration;
129
using GeneralUpdate.Core.JsonContext;
1310

@@ -20,38 +17,10 @@ public interface IProcessInfoProvider
2017
Task<ProcessInfo?> ReceiveAsync(CancellationToken token = default);
2118
}
2219

23-
/// <summary>Named pipe IPC — preferred (no file residue).</summary>
24-
public class NamedPipeProcessInfoProvider : IProcessInfoProvider
25-
{
26-
private readonly string _pipeName;
27-
28-
public NamedPipeProcessInfoProvider(string pipeName = "GeneralUpdate.IPC")
29-
=> _pipeName = pipeName;
30-
31-
public async Task SendAsync(ProcessInfo info, CancellationToken token = default)
32-
{
33-
using var server = new NamedPipeServerStream(_pipeName, PipeDirection.Out);
34-
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
35-
var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo);
36-
var bytes = Encoding.UTF8.GetBytes(json);
37-
await server.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false);
38-
}
39-
40-
public async Task<ProcessInfo?> ReceiveAsync(CancellationToken token = default)
41-
{
42-
using var client = new NamedPipeClientStream(".", _pipeName, PipeDirection.In);
43-
await client.ConnectAsync(5000, token).ConfigureAwait(false);
44-
using var ms = new MemoryStream();
45-
var buffer = new byte[4096];
46-
int read;
47-
while ((read = await client.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false)) > 0)
48-
await ms.WriteAsync(buffer, 0, read, token).ConfigureAwait(false);
49-
var json = Encoding.UTF8.GetString(ms.ToArray());
50-
return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo);
51-
}
52-
}
53-
54-
/// <summary>Encrypted file fallback IPC (AES).</summary>
20+
/// <summary>
21+
/// AES-encrypted temporary file IPC — simplest, most reliable cross-platform approach.
22+
/// File lives in %TEMP%/GeneralUpdate/ipc/ with a random name, auto-deleted after read.
23+
/// </summary>
5524
public class EncryptedFileProcessInfoProvider : IProcessInfoProvider
5625
{
5726
private static readonly byte[] Key = SHA256.Create()
@@ -69,20 +38,29 @@ public EncryptedFileProcessInfoProvider(string? basePath = null)
6938

7039
public Task SendAsync(ProcessInfo info, CancellationToken token = default)
7140
{
72-
token.ThrowIfCancellationRequested();
41+
Send(info);
42+
return Task.CompletedTask;
43+
}
44+
45+
/// <summary>Synchronous send — all I/O is synchronous under the hood.</summary>
46+
public void Send(ProcessInfo info)
47+
{
7348
var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo);
7449
var plainBytes = Encoding.UTF8.GetBytes(json);
7550
using var aes = Aes.Create();
7651
aes.Key = Key; aes.IV = IV;
7752
using var enc = aes.CreateEncryptor();
7853
var cipher = enc.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
7954
File.WriteAllBytes(_filePath, cipher);
80-
return Task.CompletedTask;
8155
}
8256

8357
public Task<ProcessInfo?> ReceiveAsync(CancellationToken token = default)
58+
=> Task.FromResult(Receive());
59+
60+
/// <summary>Synchronous receive — reads and deletes the encrypted file.</summary>
61+
public ProcessInfo? Receive()
8462
{
85-
if (!File.Exists(_filePath)) return Task.FromResult<ProcessInfo?>(null);
63+
if (!File.Exists(_filePath)) return null;
8664
try
8765
{
8866
var cipher = File.ReadAllBytes(_filePath);
@@ -91,131 +69,8 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default)
9169
using var dec = aes.CreateDecryptor();
9270
var plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
9371
var json = Encoding.UTF8.GetString(plain);
94-
return Task.FromResult<ProcessInfo?>(
95-
JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo));
72+
return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo);
9673
}
9774
finally { try { File.Delete(_filePath); } catch { } }
9875
}
9976
}
100-
101-
/// <summary>Shared memory fallback IPC (Linux-friendly, no file residue).</summary>
102-
public class SharedMemoryProcessInfoProvider : IProcessInfoProvider
103-
{
104-
private readonly string _mapName;
105-
private MemoryMappedFile? _mmf;
106-
private const int MaxPayload = 4096;
107-
108-
public SharedMemoryProcessInfoProvider(string mapName = "GeneralUpdate.IPC.Shm")
109-
=> _mapName = mapName;
110-
111-
public Task SendAsync(ProcessInfo info, CancellationToken token = default)
112-
{
113-
token.ThrowIfCancellationRequested();
114-
var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo);
115-
var bytes = Encoding.UTF8.GetBytes(json);
116-
if (bytes.Length > MaxPayload - 4)
117-
throw new InvalidOperationException($"ProcessInfo payload exceeds {MaxPayload - 4} bytes.");
118-
119-
_mmf = MemoryMappedFile.CreateOrOpen(_mapName, MaxPayload);
120-
using var accessor = _mmf.CreateViewAccessor(0, MaxPayload);
121-
accessor.Write(0, bytes.Length);
122-
accessor.WriteArray(4, bytes, 0, bytes.Length);
123-
return Task.CompletedTask;
124-
}
125-
126-
public Task<ProcessInfo?> ReceiveAsync(CancellationToken token = default)
127-
{
128-
try
129-
{
130-
using var mmf = MemoryMappedFile.OpenExisting(_mapName);
131-
using var accessor = mmf.CreateViewAccessor(0, MaxPayload);
132-
int length = accessor.ReadInt32(0);
133-
if (length <= 0 || length > MaxPayload - 4)
134-
return Task.FromResult<ProcessInfo?>(null);
135-
var bytes = new byte[length];
136-
accessor.ReadArray(4, bytes, 0, length);
137-
var json = Encoding.UTF8.GetString(bytes);
138-
return Task.FromResult<ProcessInfo?>(
139-
JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo));
140-
}
141-
catch (FileNotFoundException)
142-
{
143-
return Task.FromResult<ProcessInfo?>(null);
144-
}
145-
catch (DirectoryNotFoundException)
146-
{
147-
return Task.FromResult<ProcessInfo?>(null);
148-
}
149-
catch (Exception ex) when (ex is not OutOfMemoryException)
150-
{
151-
// Platform-specific failures (e.g. Linux /dev/shm not mounted)
152-
GeneralTracer.Warn($"SharedMemoryProvider: receive failed: {ex.Message}");
153-
return Task.FromResult<ProcessInfo?>(null);
154-
}
155-
}
156-
}
157-
158-
/// <summary>
159-
/// Auto-fallback IPC provider. Tries providers in order:
160-
/// NamedPipe → SharedMemory → EncryptedFile.
161-
/// On send, uses the first provider that succeeds.
162-
/// On receive, waits for data from the most reliable available provider.
163-
/// </summary>
164-
public class AutoProcessInfoProvider : IProcessInfoProvider
165-
{
166-
private readonly IProcessInfoProvider[] _providers;
167-
168-
public AutoProcessInfoProvider()
169-
{
170-
_providers = new IProcessInfoProvider[]
171-
{
172-
new NamedPipeProcessInfoProvider(),
173-
new SharedMemoryProcessInfoProvider(),
174-
new EncryptedFileProcessInfoProvider()
175-
};
176-
}
177-
178-
public AutoProcessInfoProvider(params IProcessInfoProvider[] providers)
179-
=> _providers = providers;
180-
181-
public async Task SendAsync(ProcessInfo info, CancellationToken token = default)
182-
{
183-
Exception? last = null;
184-
foreach (var provider in _providers)
185-
{
186-
try
187-
{
188-
await provider.SendAsync(info, token).ConfigureAwait(false);
189-
GeneralTracer.Debug($"AutoProcessInfoProvider: sent via {provider.GetType().Name}.");
190-
return;
191-
}
192-
catch (Exception ex)
193-
{
194-
GeneralTracer.Warn($"AutoProcessInfoProvider: {provider.GetType().Name} failed: {ex.Message}");
195-
last = ex;
196-
}
197-
}
198-
throw new InvalidOperationException("All IPC providers failed to send.", last);
199-
}
200-
201-
public async Task<ProcessInfo?> ReceiveAsync(CancellationToken token = default)
202-
{
203-
foreach (var provider in _providers)
204-
{
205-
try
206-
{
207-
var result = await provider.ReceiveAsync(token).ConfigureAwait(false);
208-
if (result != null)
209-
{
210-
GeneralTracer.Debug($"AutoProcessInfoProvider: received via {provider.GetType().Name}.");
211-
return result;
212-
}
213-
}
214-
catch (Exception ex)
215-
{
216-
GeneralTracer.Warn($"AutoProcessInfoProvider: {provider.GetType().Name} receive failed: {ex.Message}");
217-
}
218-
}
219-
return null;
220-
}
221-
}

src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using GeneralUpdate.Core.FileSystem;
1818
using GeneralUpdate.Core.Hooks;
1919
using GeneralUpdate.Core.JsonContext;
20+
using GeneralUpdate.Core.Ipc;
2021
using GeneralUpdate.Core.Strategy;
2122

2223
namespace GeneralUpdate.Core.Silent;
@@ -36,6 +37,7 @@ public class SilentPollOrchestrator : IDisposable
3637
private int _updaterStarted;
3738
private IUpdateHooks? _hooks;
3839
private IUpdateReporter? _reporter;
40+
private Configuration.ProcessInfo? _preparedProcessInfo;
3941

4042
public SilentPollOrchestrator(GlobalConfigInfo configInfo, SilentOptions options)
4143
{
@@ -160,13 +162,13 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token)
160162
StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory,
161163
BlackListManager.Instance.SkipDirectorys);
162164

163-
// Build ProcessInfo
164-
var processInfo = ConfigurationMapper.MapToProcessInfo(
165+
// Build ProcessInfo and store for IPC delivery on process exit
166+
_preparedProcessInfo = ConfigurationMapper.MapToProcessInfo(
165167
_configInfo, new List<VersionInfo>(),
166168
BlackListManager.Instance.BlackFormats.ToList(),
167169
BlackListManager.Instance.BlackFiles.ToList(),
168170
BlackListManager.Instance.SkipDirectorys.ToList());
169-
_configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo);
171+
_configInfo.ProcessInfo = JsonSerializer.Serialize(_preparedProcessInfo, ProcessInfoJsonContext.Default.ProcessInfo);
170172

171173
// ═══ Reporter: update started ═══
172174
var startTime = DateTimeOffset.UtcNow;
@@ -311,13 +313,24 @@ private void OnProcessExit(object? sender, EventArgs e)
311313

312314
try
313315
{
314-
Environments.SetEnvironmentVariable("ProcessInfo", _configInfo.ProcessInfo ?? string.Empty);
315316
var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName);
317+
318+
// Start the upgrade process first — it will call ReceiveAsync in its constructor.
319+
// We then call SendAsync which creates the NamedPipe server; the upgrade's
320+
// client connects to it. Auto-fallback (SharedMemory > EncryptedFile) handles
321+
// timing gaps where the named pipe handshake doesn't complete in time.
316322
if (File.Exists(updaterPath))
317323
{
318324
GeneralTracer.Info($"SilentPollOrchestrator: launching updater {updaterPath}");
319325
Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath });
320326
}
327+
328+
// Send ProcessInfo via AES-encrypted file IPC.
329+
if (_preparedProcessInfo != null)
330+
{
331+
new EncryptedFileProcessInfoProvider().Send(_preparedProcessInfo);
332+
GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via encrypted file IPC.");
333+
}
321334
}
322335
catch (Exception ex)
323336
{

src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
using System.Runtime.InteropServices;
77
using System.Text;
88
using System.Text.Json;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011
using GeneralUpdate.Core.Configuration;
1112
using GeneralUpdate.Core.Download;
1213
using GeneralUpdate.Core.Event;
1314
using GeneralUpdate.Core.FileSystem;
1415
using GeneralUpdate.Core.JsonContext;
16+
using GeneralUpdate.Core.Ipc;
1517
using GeneralUpdate.Core.Network;
1618

1719
namespace GeneralUpdate.Core.Strategy;
@@ -169,14 +171,20 @@ private async Task ExecuteStandardWorkflowAsync()
169171
Format = _configInfo.Format ?? "ZIP"
170172
}).ToList();
171173

172-
_configInfo.ProcessInfo = JsonSerializer.Serialize(
173-
ConfigurationMapper.MapToProcessInfo(
174-
_configInfo, downloadVersions,
175-
BlackListManager.Instance.BlackFormats.ToList(),
176-
BlackListManager.Instance.BlackFiles.ToList(),
177-
BlackListManager.Instance.SkipDirectorys.ToList()),
174+
var processInfo = ConfigurationMapper.MapToProcessInfo(
175+
_configInfo, downloadVersions,
176+
BlackListManager.Instance.BlackFormats.ToList(),
177+
BlackListManager.Instance.BlackFiles.ToList(),
178+
BlackListManager.Instance.SkipDirectorys.ToList());
179+
180+
// Keep JSON string for backward compatibility (GlobalConfigInfo.ProcessInfo)
181+
_configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo,
178182
ProcessInfoJsonContext.Default.ProcessInfo);
179183

184+
// Wire ProcessInfo via AES-encrypted file IPC.
185+
new EncryptedFileProcessInfoProvider().Send(processInfo);
186+
GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC.");
187+
180188
// Backup — conditionally skipped when BackupEnabled is false
181189
if (_configInfo.BackupEnabled != false)
182190
{

0 commit comments

Comments
 (0)