diff --git a/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder-Example.cs b/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder-Example.cs deleted file mode 100644 index b67d4d2b..00000000 --- a/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder-Example.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using GeneralUpdate.Core.Configuration; - -namespace ConfiginfoBuilderExample -{ - /// - /// Example demonstrating the ConfiginfoBuilder usage with JSON configuration - /// - class Program - { - static void Main(string[] args) - { - Console.WriteLine("=== ConfiginfoBuilder Usage Examples ===\n"); - - // Example 1: Load configuration from JSON file (recommended) - Console.WriteLine("Example 1: Loading from update_config.json file"); - Console.WriteLine("This example requires an update_config.json file in the running directory."); - Console.WriteLine("The configuration file has the highest priority and must contain all required settings.\n"); - - try - { - // Create update_config.json for demonstration - CreateExampleConfigFile(); - - // Simply call Create() with no parameters - it loads from update_config.json - var config = ConfiginfoBuilder.Create().Build(); - - Console.WriteLine($" UpdateUrl: {config.UpdateUrl}"); - Console.WriteLine($" Token: {config.Token}"); - Console.WriteLine($" Scheme: {config.Scheme}"); - Console.WriteLine($" InstallPath: {config.InstallPath}"); - Console.WriteLine($" AppName: {config.AppName}"); - Console.WriteLine($" ClientVersion: {config.ClientVersion}"); - Console.WriteLine(); - } - catch (FileNotFoundException ex) - { - Console.WriteLine($" Error: {ex.Message}"); - Console.WriteLine(" Please create update_config.json in the running directory."); - Console.WriteLine(); - } - finally - { - CleanupExampleConfigFile(); - } - - // Example 2: Customizing configuration after loading from file - Console.WriteLine("Example 2: Loading from JSON and customizing with method chaining"); - try - { - CreateExampleConfigFile(); - - var customConfig = ConfiginfoBuilder.Create() - .SetAppName("CustomApp.exe") - .SetInstallPath("/custom/path") - .Build(); - - Console.WriteLine($" AppName: {customConfig.AppName}"); - Console.WriteLine($" InstallPath: {customConfig.InstallPath}"); - Console.WriteLine(); - } - catch (FileNotFoundException ex) - { - Console.WriteLine($" Error: {ex.Message}"); - Console.WriteLine(); - } - finally - { - CleanupExampleConfigFile(); - } - - // Example 3: Error handling when config file is missing - Console.WriteLine("Example 3: Error Handling - Missing Configuration File"); - try - { - var config = ConfiginfoBuilder.Create().Build(); - } - catch (FileNotFoundException ex) - { - Console.WriteLine($" Caught expected error: {ex.Message}"); - Console.WriteLine(" This is expected when update_config.json doesn't exist."); - } - Console.WriteLine(); - - Console.WriteLine("\n=== All Examples Completed! ==="); - Console.WriteLine("\nNote: ConfiginfoBuilder now requires update_config.json file."); - Console.WriteLine("See update_config.example.json for a complete example."); - } - - private static void CreateExampleConfigFile() - { - var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update_config.json"); - var exampleConfig = @"{ - ""UpdateUrl"": ""https://api.example.com/updates"", - ""Token"": ""example-auth-token"", - ""Scheme"": ""https"", - ""AppName"": ""Update.exe"", - ""MainAppName"": ""MyApplication.exe"", - ""ClientVersion"": ""1.0.0"", - ""UpgradeClientVersion"": ""1.0.0"", - ""AppSecretKey"": ""example-secret-key"", - ""ProductId"": ""example-product-id"" -}"; - File.WriteAllText(configPath, exampleConfig); - } - - private static void CleanupExampleConfigFile() - { - var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update_config.json"); - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - } - } -} diff --git a/src/c#/GeneralUpdate.Core/Configuration/Environments.cs b/src/c#/GeneralUpdate.Core/Configuration/Environments.cs index 0cf22bfb..bd32d404 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/Environments.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/Environments.cs @@ -2,17 +2,17 @@ using System.IO; using System.Security.Cryptography; using System.Text; +using GeneralUpdate.Core.Ipc; namespace GeneralUpdate.Core.Configuration; /// /// Secure IPC environment variable provider. /// AES-encrypted temp files in a dedicated subdirectory, auto-deleted after read. +/// Encryption is delegated to . /// public static class Environments { - // Fixed key/IV derived from a constant — not crypto-grade, but sufficient for - // ephemeral IPC where the file lives < 1 second and is in a per-user directory. private static readonly byte[] _aesKey = SHA256.Create() .ComputeHash(Encoding.UTF8.GetBytes("GeneralUpdate.IPC.EnvironmentProvider.v1")); private static readonly byte[] _aesIV = new byte[16] { 0x47, 0x55, 0x50, 0x44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -31,33 +31,13 @@ public static void SetEnvironmentVariable(string key, string value) { var filePath = Path.Combine(IpcDir, $"{key}.enc"); var plainBytes = Encoding.UTF8.GetBytes(value); - using var aes = Aes.Create(); - aes.Key = _aesKey; - aes.IV = _aesIV; - using var encryptor = aes.CreateEncryptor(); - var encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); - File.WriteAllBytes(filePath, encrypted); + IpcEncryption.EncryptToFile(plainBytes, filePath, _aesKey, _aesIV); } public static string GetEnvironmentVariable(string key) { var filePath = Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc", $"{key}.enc"); - if (!File.Exists(filePath)) - return string.Empty; - - try - { - var encrypted = File.ReadAllBytes(filePath); - using var aes = Aes.Create(); - aes.Key = _aesKey; - aes.IV = _aesIV; - using var decryptor = aes.CreateDecryptor(); - var plainBytes = decryptor.TransformFinalBlock(encrypted, 0, encrypted.Length); - return Encoding.UTF8.GetString(plainBytes); - } - finally - { - try { File.Delete(filePath); } catch { /* best-effort cleanup */ } - } + var plainBytes = IpcEncryption.DecryptFromFile(filePath, _aesKey, _aesIV); + return plainBytes != null ? Encoding.UTF8.GetString(plainBytes) : string.Empty; } } diff --git a/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiAllDownloadCompletedEventArgs.cs b/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiAllDownloadCompletedEventArgs.cs similarity index 100% rename from src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiAllDownloadCompletedEventArgs.cs rename to src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiAllDownloadCompletedEventArgs.cs diff --git a/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadCompletedEventArgs.cs b/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadCompletedEventArgs.cs similarity index 100% rename from src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadCompletedEventArgs.cs rename to src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadCompletedEventArgs.cs diff --git a/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadErrorEventArgs.cs b/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadErrorEventArgs.cs similarity index 100% rename from src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadErrorEventArgs.cs rename to src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadErrorEventArgs.cs diff --git a/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadStatisticsEventArgs.cs b/src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadStatisticsEventArgs.cs similarity index 100% rename from src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MutiDownloadStatisticsEventArgs.cs rename to src/c#/GeneralUpdate.Core/Download/MultiEventArgs/MultiDownloadStatisticsEventArgs.cs diff --git a/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs b/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs index d5bd92a8..99a23e7c 100644 --- a/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs +++ b/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs @@ -2,7 +2,7 @@ namespace GeneralUpdate.Core.FileSystem; -/// Built-in default blacklist items — previously hardcoded in BlackListManager. +/// Built-in default blacklist items. public static class BlackListDefaults { /// Default blacklisted files (system DLLs that ship with the runtime). diff --git a/src/c#/GeneralUpdate.Core/FileSystem/BlackListManager.cs b/src/c#/GeneralUpdate.Core/FileSystem/BlackListManager.cs deleted file mode 100644 index 887a6f41..00000000 --- a/src/c#/GeneralUpdate.Core/FileSystem/BlackListManager.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using GeneralUpdate.Core.Configuration; - -namespace GeneralUpdate.Core.FileSystem; - -/// Matches files/directories against a blacklist configuration. -public interface IBlackListMatcher -{ - bool IsBlacklisted(string relativeFilePath); - bool IsBlacklistedFormat(string extension); - bool ShouldSkipDirectory(string directoryName); -} - -/// -/// Thread-safe blacklist manager. Uses Lazy<T> singleton. -/// Matching is case-insensitive and supports prefix matching for skip directories. -/// -[Obsolete("Use DefaultBlackListMatcher + StorageManager.BlackListMatcher instead. See #412.")] -public class BlackListManager : IBlackListMatcher -{ - private static readonly Lazy _lazy = new(() => new BlackListManager()); - private readonly object _lock = new(); - - private readonly List _blackFiles = - [ - "Microsoft.Bcl.AsyncInterfaces.dll", - "System.Collections.Immutable.dll", - "System.IO.Pipelines.dll", - "System.Text.Encodings.Web.dll", - "System.Text.Json.dll" - ]; - - private readonly List _blackFormats = [".patch", ".pdb", ".rar", ".tar", ".json", Format.ZIP]; - private readonly List _skipDirs = ["app-", "fail"]; - - private BlackListManager() { } - - public static BlackListManager Instance => _lazy.Value; - - // Read-only accessors - public IReadOnlyList BlackFiles { get { lock (_lock) return _blackFiles.ToList(); } } - public IReadOnlyList BlackFormats { get { lock (_lock) return _blackFormats.ToList(); } } - public IReadOnlyList SkipDirectorys { get { lock (_lock) return _skipDirs.ToList(); } } - - // Mutation - public void AddBlackFiles(List? files) - { if (files == null) return; lock (_lock) { foreach (var f in files) AddBlackFileLocked(f); } } - public void AddBlackFile(string file) - { if (string.IsNullOrWhiteSpace(file)) return; lock (_lock) AddBlackFileLocked(file); } - private void AddBlackFileLocked(string file) - { if (!_blackFiles.Contains(file)) _blackFiles.Add(file); } - - public void AddBlackFormats(List? formats) - { if (formats == null) return; lock (_lock) { foreach (var f in formats) AddBlackFormatLocked(f); } } - public void AddBlackFormat(string format) - { if (string.IsNullOrWhiteSpace(format)) return; lock (_lock) AddBlackFormatLocked(format); } - private void AddBlackFormatLocked(string format) - { if (!_blackFormats.Contains(format)) _blackFormats.Add(format); } - - public void AddSkipDirectorys(List? dirs) - { if (dirs == null) return; lock (_lock) { foreach (var d in dirs) AddSkipDirectoryLocked(d); } } - public void AddSkipDirectory(string dir) - { if (string.IsNullOrWhiteSpace(dir)) return; lock (_lock) AddSkipDirectoryLocked(dir); } - private void AddSkipDirectoryLocked(string dir) - { if (!_skipDirs.Contains(dir)) _skipDirs.Add(dir); } - - // Matching (read operations — no lock needed for immutable reads) - public bool IsBlacklisted(string relativeFilePath) - { - var fileName = Path.GetFileName(relativeFilePath); - var ext = Path.GetExtension(relativeFilePath); - lock (_lock) - return _blackFiles.Contains(fileName) || _blackFormats.Contains(ext); - } - - public bool IsBlacklistedFormat(string extension) - { - lock (_lock) return _blackFormats.Contains(extension); - } - - public bool ShouldSkipDirectory(string directoryName) - { - lock (_lock) return _skipDirs.Any(d => directoryName.Contains(d)); - } -} diff --git a/src/c#/GeneralUpdate.Core/FileSystem/IBlackListMatcher.cs b/src/c#/GeneralUpdate.Core/FileSystem/IBlackListMatcher.cs new file mode 100644 index 00000000..3be745f4 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/FileSystem/IBlackListMatcher.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace GeneralUpdate.Core.FileSystem; + +/// Matches files/directories against a blacklist configuration. +public interface IBlackListMatcher +{ + bool IsBlacklisted(string relativeFilePath); + bool IsBlacklistedFormat(string extension); + bool ShouldSkipDirectory(string directoryName); +} diff --git a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs index bb8b04a0..7afe6771 100644 --- a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs +++ b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs @@ -14,7 +14,7 @@ public sealed class StorageManager private long _fileCount = 0; public const string DirectoryName = "app-"; - /// Optional blacklist matcher. When set, takes precedence over BlackListManager. + /// Optional blacklist matcher. Must be set before any file operation. public static IBlackListMatcher? BlackListMatcher { get; set; } private ComparisonResult ComparisonResult { get; set; } @@ -267,9 +267,7 @@ private IEnumerable ReadFileNode(string path, string rootPath = null) foreach (var subPath in Directory.EnumerateFiles(path)) { -#pragma warning disable CS0618 // Obsolete fallback - if ((BlackListMatcher ?? BlackListManager.Instance).IsBlacklisted(subPath)) continue; -#pragma warning restore CS0618 + if (BlackListMatcher != null && BlackListMatcher.IsBlacklisted(subPath)) continue; var hashAlgorithm = new Sha256HashAlgorithm(); var hash = hashAlgorithm.ComputeHash(subPath); @@ -288,9 +286,7 @@ private IEnumerable ReadFileNode(string path, string rootPath = null) foreach (var subPath in Directory.EnumerateDirectories(path)) { -#pragma warning disable CS0618 // Obsolete fallback - if ((BlackListMatcher ?? BlackListManager.Instance).ShouldSkipDirectory(subPath)) continue; -#pragma warning restore CS0618 + if (BlackListMatcher != null && BlackListMatcher.ShouldSkipDirectory(subPath)) continue; resultFiles.AddRange(ReadFileNode(subPath, rootPath)); } diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj index cf93fe4e..1c1424af 100644 --- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj +++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj @@ -4,7 +4,7 @@ enable GeneralUpdate.Core JusterZhu - GeneralUpdate unified core — client and upgrade bootstrap, download, pipeline, strategies, utilities. + GeneralUpdate unified core �?client and upgrade bootstrap, download, pipeline, strategies, utilities. Copyright 2020-2026 JusterZhu https://github.com/GeneralLibrary/GeneralUpdate https://github.com/GeneralLibrary/GeneralUpdate @@ -30,6 +30,7 @@ + diff --git a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs index bc94643a..26ddfd62 100644 --- a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs +++ b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs @@ -20,6 +20,7 @@ public interface IProcessInfoProvider /// /// 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. +/// Encryption is delegated to . /// public class EncryptedFileProcessInfoProvider : IProcessInfoProvider { @@ -47,11 +48,7 @@ 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); + IpcEncryption.EncryptToFile(plainBytes, _filePath, Key, IV); } public Task ReceiveAsync(CancellationToken token = default) @@ -60,17 +57,9 @@ public void Send(ProcessInfo info) /// Synchronous receive — reads and deletes the encrypted file. public ProcessInfo? Receive() { - if (!File.Exists(_filePath)) return 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 JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo); - } - finally { try { File.Delete(_filePath); } catch { } } + var plain = IpcEncryption.DecryptFromFile(_filePath, Key, IV); + if (plain == null) return null; + var json = Encoding.UTF8.GetString(plain); + return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo); } } diff --git a/src/c#/GeneralUpdate.Core/Ipc/IpcEncryption.cs b/src/c#/GeneralUpdate.Core/Ipc/IpcEncryption.cs new file mode 100644 index 00000000..2b3d90ff --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Ipc/IpcEncryption.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace GeneralUpdate.Core.Ipc; + +/// +/// Shared AES encryption utilities for IPC. +/// Used by both (key-value IPC) and +/// (structured ProcessInfo IPC). +/// +public static class IpcEncryption +{ + /// + /// AES-CBC encrypt and write to . + /// + public static void EncryptToFile(byte[] plainBytes, string filePath, byte[] key, byte[] iv) + { + using var aes = Aes.Create(); + aes.Key = key; + aes.IV = iv; + using var encryptor = aes.CreateEncryptor(); + var cipher = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + File.WriteAllBytes(filePath, cipher); + } + + /// + /// Read, AES-CBC decrypt, and auto-delete the file at . + /// Returns null if the file does not exist. + /// + public static byte[]? DecryptFromFile(string filePath, byte[] key, byte[] iv) + { + if (!File.Exists(filePath)) return null; + + try + { + var cipher = File.ReadAllBytes(filePath); + using var aes = Aes.Create(); + aes.Key = key; + aes.IV = iv; + using var decryptor = aes.CreateDecryptor(); + return decryptor.TransformFinalBlock(cipher, 0, cipher.Length); + } + finally + { + try { File.Delete(filePath); } catch { /* best-effort cleanup */ } + } + } +} diff --git a/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs new file mode 100644 index 00000000..67071a5c --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs @@ -0,0 +1,16 @@ +using System.Net.Http; + +namespace GeneralUpdate.Core.Network; + +/// +/// Provides a shared static instance. +/// Reusing a single HttpClient prevents socket exhaustion. +/// Do NOT dispose clients obtained from here. +/// +public static class HttpClientProvider +{ + private static readonly HttpClient _shared = new(); + + /// Shared instance. Do NOT dispose. + public static HttpClient Shared => _shared; +} diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index bfddc675..3ec758bd 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -23,7 +23,7 @@ namespace GeneralUpdate.Core.Silent; /// -/// Silent update poll orchestrator — periodically checks for updates, +/// Silent update poll orchestrator �?periodically checks for updates, /// downloads them in the background, and optionally auto-installs. /// Replaces the legacy SilentUpdateMode class. /// @@ -126,7 +126,7 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token) return; } - // ═══ Hooks: allow cancellation before starting update ═══ + // ══�?Hooks: allow cancellation before starting update ══�? var updateCtx = new UpdateContext( _configInfo.MainAppName ?? _configInfo.AppName, _configInfo.InstallPath, @@ -173,7 +173,7 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token) _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories); _configInfo.ProcessInfo = JsonSerializer.Serialize(_preparedProcessInfo, ProcessInfoJsonContext.Default.ProcessInfo); - // ═══ Reporter: update started ═══ + // ══�?Reporter: update started ══�? var startTime = DateTimeOffset.UtcNow; if (_reporter != null) { @@ -188,7 +188,7 @@ await _reporter.ReportAsync(new UpdateReport( // Download using new orchestrator GeneralTracer.Info($"SilentPollOrchestrator: downloading {plan.Assets.Count} asset(s)."); - var httpClient = new System.Net.Http.HttpClient(); + var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared; var downloadSuccessCount = 0; var downloadFailedCount = 0; var downloadTotalBytes = 0L; @@ -203,7 +203,7 @@ await _reporter.ReportAsync(new UpdateReport( downloadElapsed = report.TotalDuration; GeneralTracer.Info($"SilentPollOrchestrator: download complete. Success={downloadSuccessCount}, Failed={downloadFailedCount}"); - // ═══ Hooks + Reporter: download completed ═══ + // ══�?Hooks + Reporter: download completed ══�? if (_hooks != null) { try @@ -256,7 +256,7 @@ await _reporter.ReportAsync(new UpdateReport( } return; } - finally { httpClient.Dispose(); } + finally { } // Execute pipeline try @@ -268,7 +268,7 @@ await _reporter.ReportAsync(new UpdateReport( GeneralTracer.Info("SilentPollOrchestrator: update prepared."); Interlocked.Exchange(ref _prepared, 1); - // ═══ Hooks + Reporter: update applied ═══ + // ══�?Hooks + Reporter: update applied ══�? if (_hooks != null) { try { await _hooks.OnAfterUpdateAsync(updateCtx).ConfigureAwait(false); } @@ -318,7 +318,7 @@ private void OnProcessExit(object? sender, EventArgs e) { var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); - // Start the upgrade process first — it will call ReceiveAsync in its constructor. + // 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. diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index f484273c..31ec965b 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -89,7 +89,7 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu private async Task ExecuteWorkflowAsync() { - // Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync(). + // Standard mode �?silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync(). // Runtime options (Encoding, Format, DownloadTimeOut, etc.) are already // populated on _configInfo by Bootstrap.ApplyRuntimeOptions(). await ExecuteStandardWorkflowAsync(); @@ -185,7 +185,7 @@ private async Task ExecuteStandardWorkflowAsync() new EncryptedFileProcessInfoProvider().Send(processInfo); GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC."); - // Backup — conditionally skipped when BackupEnabled is false + // Backup �?conditionally skipped when BackupEnabled is false if (_configInfo.BackupEnabled != false) { Backup(); @@ -197,7 +197,7 @@ private async Task ExecuteStandardWorkflowAsync() _osStrategy!.Create(_configInfo); - // Download via orchestrator — wired with options from GlobalConfigInfo + // Download via orchestrator �?wired with options from GlobalConfigInfo var orchOptions = Download.Models.DownloadOrchestratorOptions.From(_configInfo); GeneralTracer.Info($"ClientUpdateStrategy: downloading {downloadPlan.Assets.Count} asset(s)."); if (_orchestrator != null) @@ -206,13 +206,13 @@ private async Task ExecuteStandardWorkflowAsync() } else { - var httpClient = new System.Net.Http.HttpClient(); + var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared; try { var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient, orchOptions); await orchestrator.ExecuteAsync(downloadPlan, _configInfo.TempPath).ConfigureAwait(false); } - finally { httpClient.Dispose(); } + finally { } } await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index 51a2ff51..89496404 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Core.Compress; using GeneralUpdate.Core.Configuration; @@ -16,11 +17,11 @@ namespace GeneralUpdate.Core.Strategy; /// -/// OSS (Object Storage Service) update strategy — client/upgrade split via AppType. +/// OSS (Object Storage Service) update strategy �?client/upgrade split via AppType. /// -/// — downloads version config, checks for updates, +/// �?downloads version config, checks for updates, /// starts the upgrade process, and exits. -/// — reads version config, downloads packages from OSS, +/// �?reads version config, downloads packages from OSS, /// decompresses them, starts the main app, and exits. /// /// @@ -51,7 +52,7 @@ public async Task ExecuteAsync() if (_configInfo == null) throw new InvalidOperationException("OSSUpdateStrategy not configured. Call Create() first."); - // Dispatch by role — no env-var detection needed. + // Dispatch by role �?no env-var detection needed. if (_role == AppType.OSSUpgrade) { await ExecuteUpgradeAsync(); @@ -217,7 +218,7 @@ private static async Task DownloadVersionConfig(string url, string path) File.SetAttributes(path, FileAttributes.Normal); File.Delete(path); } - using var httpClient = new HttpClient(); + using var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared; var bytes = await httpClient.GetByteArrayAsync(url).ConfigureAwait(false); File.WriteAllBytes(path, bytes); } @@ -240,12 +241,11 @@ private async Task DownloadAssetsAsync(List assets) } else { - using var httpClient = new HttpClient - { - Timeout = TimeSpan.FromSeconds(_configInfo?.DownloadTimeOut > 0 ? _configInfo!.DownloadTimeOut : DefaultTimeOut) - }; + using var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared; + using var cts = new CancellationTokenSource( + TimeSpan.FromSeconds(_configInfo?.DownloadTimeOut > 0 ? _configInfo!.DownloadTimeOut : DefaultTimeOut)); var orchestrator = new DefaultDownloadOrchestrator(httpClient); - await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); + await orchestrator.ExecuteAsync(plan, _appPath, token: cts.Token).ConfigureAwait(false); } }