Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -256,11 +257,8 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs

private void InitializeFromEnvironment()
{
var json = Environments.GetEnvironmentVariable("ProcessInfo");
if (string.IsNullOrWhiteSpace(json)) return;

var processInfo = JsonSerializer.Deserialize(
json, ProcessInfoJsonContext.Default.ProcessInfo);
// Read ProcessInfo via AES-encrypted file IPC.
var processInfo = new EncryptedFileProcessInfoProvider().Receive();
if (processInfo == null) return;

BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats);
Expand Down
179 changes: 17 additions & 162 deletions src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

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

/// <summary>Named pipe IPC — preferred (no file residue).</summary>
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<ProcessInfo?> 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);
}
}

/// <summary>Encrypted file fallback IPC (AES).</summary>
/// <summary>
/// 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.
/// </summary>
public class EncryptedFileProcessInfoProvider : IProcessInfoProvider
{
private static readonly byte[] Key = SHA256.Create()
Expand All @@ -69,20 +38,29 @@ public EncryptedFileProcessInfoProvider(string? basePath = null)

public Task SendAsync(ProcessInfo info, CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
Send(info);
return Task.CompletedTask;
}

/// <summary>Synchronous send — all I/O is synchronous under the hood.</summary>
public void Send(ProcessInfo info)
{
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<ProcessInfo?> ReceiveAsync(CancellationToken token = default)
=> Task.FromResult(Receive());

/// <summary>Synchronous receive — reads and deletes the encrypted file.</summary>
public ProcessInfo? Receive()
{
if (!File.Exists(_filePath)) return Task.FromResult<ProcessInfo?>(null);
if (!File.Exists(_filePath)) return null;
try
{
var cipher = File.ReadAllBytes(_filePath);
Expand All @@ -91,131 +69,8 @@ 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<ProcessInfo?>(
JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo));
return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo);
}
finally { try { File.Delete(_filePath); } catch { } }
}
}

/// <summary>Shared memory fallback IPC (Linux-friendly, no file residue).</summary>
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<ProcessInfo?> 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<ProcessInfo?>(null);
var bytes = new byte[length];
accessor.ReadArray(4, bytes, 0, length);
var json = Encoding.UTF8.GetString(bytes);
return Task.FromResult<ProcessInfo?>(
JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo));
}
catch (FileNotFoundException)
{
return Task.FromResult<ProcessInfo?>(null);
}
catch (DirectoryNotFoundException)
{
return Task.FromResult<ProcessInfo?>(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<ProcessInfo?>(null);
}
}
}

/// <summary>
/// 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.
/// </summary>
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<ProcessInfo?> 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;
}
}
21 changes: 17 additions & 4 deletions src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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<VersionInfo>(),
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;
Expand Down Expand Up @@ -311,13 +313,24 @@ private void OnProcessExit(object? sender, EventArgs e)

try
{
Environments.SetEnvironmentVariable("ProcessInfo", _configInfo.ProcessInfo ?? string.Empty);
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))
{
GeneralTracer.Info($"SilentPollOrchestrator: launching updater {updaterPath}");
Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath });
}

// Send ProcessInfo via AES-encrypted file IPC.
if (_preparedProcessInfo != null)
{
new EncryptedFileProcessInfoProvider().Send(_preparedProcessInfo);
GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via encrypted file IPC.");
}
}
catch (Exception ex)
{
Expand Down
20 changes: 14 additions & 6 deletions src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
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;
using GeneralUpdate.Core.Event;
using GeneralUpdate.Core.FileSystem;
using GeneralUpdate.Core.JsonContext;
using GeneralUpdate.Core.Ipc;
using GeneralUpdate.Core.Network;

namespace GeneralUpdate.Core.Strategy;
Expand Down Expand Up @@ -169,14 +171,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 AES-encrypted file IPC.
new EncryptedFileProcessInfoProvider().Send(processInfo);
GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC.");

// Backup — conditionally skipped when BackupEnabled is false
if (_configInfo.BackupEnabled != false)
{
Expand Down
Loading
Loading