From 0fdff0c7f09a1b4c74901eb1a9b6cdfd4cc9d3e2 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 18:10:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20reverse=20Core=E2=86=94Differen?= =?UTF-8?q?tial=20dependency=20and=20make=20all=20components=20independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GeneralUpdate.Differential: remove Core dependency, keep only binary diff algorithms (IBinaryDiffer, StreamingHdiffDiffer, BinaryHandler, compressors) - GeneralUpdate.Core: add Differential reference, absorb orchestration types (DefaultCleanStrategy, DefaultDirtyStrategy, DiffPipeline, DifferentialCore) - GeneralUpdate.Bowl: remove Core dependency, self-contain GeneralTracer, StorageHelper, IpcEncryption, Environments, ProcessInfo - GeneralUpdate.Drivelution: remove Core dependency, self-contain GeneralTracer - GeneralUpdate.Extension: remove Core dependency, self-contain GeneralTracer - Remove ICleanStrategy/IDirtyStrategy/IBinaryDiffer from Core.Differential namespace; they live in Core.Differential (strategies) and Differential.Abstractions (IBinaryDiffer) respectively - Update all test projects for new namespaces Co-Authored-By: Claude Opus 4.7 --- .../DifferentialTest/DifferentialCoreTests.cs | 4 +- .../Matchers/DefaultCleanMatcherTests.cs | 2 +- .../Matchers/DefaultCleanStrategyTests.cs | 2 +- .../Matchers/DefaultDirtyMatcherTests.cs | 2 +- .../Matchers/DefaultDirtyStrategyTests.cs | 2 +- .../Models/DiffProgressTests.cs | 2 +- .../Pipeline/DiffPipelineBuilderTests.cs | 6 +- .../Pipeline/DiffPipelineIntegrationTests.cs | 6 +- .../Pipeline/DiffPipelineOptionsTests.cs | 2 +- .../Pipeline/DiffPipelineTests.cs | 6 +- src/c#/GeneralUpdate.Bowl/Bowl.cs | 17 +- .../Configuration/ProcessInfo.cs | 18 + .../FileSystem/StorageHelper.cs | 62 ++++ .../GeneralUpdate.Bowl.csproj | 4 - .../Internal/CrashReporter.cs | 6 +- .../Internal/EnvironmentProvider.cs | 3 +- .../Internal/WindowsSystemInfoProvider.cs | 2 +- src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs | 41 +++ .../GeneralUpdate.Bowl/Ipc/IpcEncryption.cs | 40 +++ .../Strategies/LinuxBowlStrategy.cs | 2 +- .../Strategies/MacBowlStrategy.cs | 2 +- .../Strategies/ProcessRunner.cs | 2 +- .../Strategies/WindowsBowlStrategy.cs | 6 +- .../Strategys/AbstractStrategy.cs | 6 +- .../Strategys/LinuxStrategy.cs | 2 +- .../Strategys/WindowStrategy.cs | 11 +- .../Tracer/GeneralTracer.cs | 163 +++++++++ .../Tracer/TextTraceListener.cs | 63 ++++ .../Tracer/WindowsOutputDebugListener.cs | 31 ++ .../Bootstrap/GeneralUpdateBootstrap.cs | 1 + .../Configuration/AbstractBootstrap.cs | 5 +- .../Differential/DefaultCleanMatcher.cs | 41 +++ .../Differential/DefaultCleanStrategy.cs | 92 +++++ .../Differential/DefaultDirtyMatcher.cs | 34 ++ .../Differential/DefaultDirtyStrategy.cs | 141 ++++++++ .../Differential/IBinaryDiffer.cs | 17 - .../Differential/ICleanMatcher.cs | 38 ++ .../Differential/IDirtyMatcher.cs | 23 ++ src/c#/GeneralUpdate.Core/DifferentialCore.cs | 77 +++++ .../GeneralUpdate.Core.csproj | 4 + .../GeneralUpdate.Core/Models/DiffProgress.cs | 41 +++ .../Pipeline/DiffPipeline.cs | 322 +++++++++++++++++ .../Pipeline/DiffPipelineBuilder.cs | 108 ++++++ .../Pipeline/DiffPipelineOptions.cs | 28 ++ .../Strategy/AbstractStrategy.cs | 1 + .../Strategy/ClientUpdateStrategy.cs | 1 + .../Strategy/UpgradeUpdateStrategy.cs | 1 + .../Abstractions/IBinaryDiffer.cs | 11 +- .../DifferentialCore.cs | 78 ----- .../GeneralUpdate.Differential.csproj | 3 - .../Matchers/DefaultCleanMatcher.cs | 42 --- .../Matchers/DefaultCleanStrategy.cs | 93 ----- .../Matchers/DefaultDirtyMatcher.cs | 37 -- .../Matchers/DefaultDirtyStrategy.cs | 147 -------- .../Matchers/ICleanMatcher.cs | 40 --- .../Matchers/ICleanStrategy.cs | 6 - .../Matchers/IDirtyMatcher.cs | 24 -- .../Matchers/IDirtyStrategy.cs | 6 - .../Models/DiffProgress.cs | 42 --- .../Pipeline/DiffPipeline.cs | 326 ------------------ .../Pipeline/DiffPipelineBuilder.cs | 108 ------ .../Pipeline/DiffPipelineOptions.cs | 29 -- .../Core/DriverUpdaterFactory.cs | 2 +- .../Core/Execution/CommandRunner.cs | 2 +- .../Core/Pipeline/BaseDriverUpdater.cs | 2 +- .../Core/Pipeline/DefaultPipelineSteps.cs | 2 +- .../GeneralDrivelution.cs | 2 +- .../GeneralUpdate.Drivelution.csproj | 5 - .../Linux/Implementation/LinuxDriverBackup.cs | 2 +- .../Implementation/LinuxDriverValidator.cs | 2 +- .../Implementation/LinuxGeneralDrivelution.cs | 2 +- .../Implementation/MacOsGeneralDrivelution.cs | 2 +- .../Tracer/GeneralTracer.cs | 163 +++++++++ .../Tracer/TextTraceListener.cs | 63 ++++ .../Tracer/WindowsOutputDebugListener.cs | 31 ++ .../Implementation/WindowsDriverBackup.cs | 2 +- .../Implementation/WindowsDriverValidator.cs | 2 +- .../WindowsGeneralDrivelution.cs | 2 +- .../Core/GeneralExtensionHost.cs | 2 +- .../Download/DownloadQueueManager.cs | 2 +- .../GeneralUpdate.Extension.csproj | 4 - .../Tracer/GeneralTracer.cs | 163 +++++++++ .../Tracer/TextTraceListener.cs | 63 ++++ .../Tracer/WindowsOutputDebugListener.cs | 31 ++ .../DifferentialTest/DifferentialTest.csproj | 1 + .../DifferentialUpgradeIntegrationTests.cs | 8 +- .../DifferentialTest/Matchers/MatcherTests.cs | 10 +- .../Pipeline/DiffPipelineTests.cs | 4 +- 88 files changed, 1969 insertions(+), 1087 deletions(-) create mode 100644 src/c#/GeneralUpdate.Bowl/Configuration/ProcessInfo.cs create mode 100644 src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs create mode 100644 src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs create mode 100644 src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs create mode 100644 src/c#/GeneralUpdate.Bowl/Tracer/GeneralTracer.cs create mode 100644 src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs create mode 100644 src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/DefaultCleanMatcher.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/DefaultCleanStrategy.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/DefaultDirtyMatcher.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/DefaultDirtyStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/ICleanMatcher.cs create mode 100644 src/c#/GeneralUpdate.Core/Differential/IDirtyMatcher.cs create mode 100644 src/c#/GeneralUpdate.Core/DifferentialCore.cs create mode 100644 src/c#/GeneralUpdate.Core/Models/DiffProgress.cs create mode 100644 src/c#/GeneralUpdate.Core/Pipeline/DiffPipeline.cs create mode 100644 src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs create mode 100644 src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs delete mode 100644 src/c#/GeneralUpdate.Differential/DifferentialCore.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanMatcher.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyMatcher.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/ICleanMatcher.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/ICleanStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/IDirtyMatcher.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Matchers/IDirtyStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Models/DiffProgress.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Pipeline/DiffPipeline.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineBuilder.cs delete mode 100644 src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineOptions.cs create mode 100644 src/c#/GeneralUpdate.Drivelution/Tracer/GeneralTracer.cs create mode 100644 src/c#/GeneralUpdate.Drivelution/Tracer/TextTraceListener.cs create mode 100644 src/c#/GeneralUpdate.Drivelution/Tracer/WindowsOutputDebugListener.cs create mode 100644 src/c#/GeneralUpdate.Extension/Tracer/GeneralTracer.cs create mode 100644 src/c#/GeneralUpdate.Extension/Tracer/TextTraceListener.cs create mode 100644 src/c#/GeneralUpdate.Extension/Tracer/WindowsOutputDebugListener.cs diff --git a/src/c#/DifferentialTest/DifferentialCoreTests.cs b/src/c#/DifferentialTest/DifferentialCoreTests.cs index 6df7618c..577a7da5 100644 --- a/src/c#/DifferentialTest/DifferentialCoreTests.cs +++ b/src/c#/DifferentialTest/DifferentialCoreTests.cs @@ -1,7 +1,7 @@ using Moq; -using GeneralUpdate.Differential; +using GeneralUpdate.Core; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core.Differential; namespace DifferentialTest { diff --git a/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs b/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs index ebf46b5f..c321e347 100644 --- a/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs +++ b/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core.Differential; namespace DifferentialTest.Matchers { diff --git a/src/c#/DifferentialTest/Matchers/DefaultCleanStrategyTests.cs b/src/c#/DifferentialTest/Matchers/DefaultCleanStrategyTests.cs index 9a8b17c3..8a3faac7 100644 --- a/src/c#/DifferentialTest/Matchers/DefaultCleanStrategyTests.cs +++ b/src/c#/DifferentialTest/Matchers/DefaultCleanStrategyTests.cs @@ -1,7 +1,7 @@ using Moq; using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core.Differential; namespace DifferentialTest.Matchers { diff --git a/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs b/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs index e896e12d..c075e7ca 100644 --- a/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs +++ b/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core.Differential; namespace DifferentialTest.Matchers { diff --git a/src/c#/DifferentialTest/Matchers/DefaultDirtyStrategyTests.cs b/src/c#/DifferentialTest/Matchers/DefaultDirtyStrategyTests.cs index 6eb3f981..1bbace47 100644 --- a/src/c#/DifferentialTest/Matchers/DefaultDirtyStrategyTests.cs +++ b/src/c#/DifferentialTest/Matchers/DefaultDirtyStrategyTests.cs @@ -3,7 +3,7 @@ using GeneralUpdate.Core.HashAlgorithms; using GeneralUpdate.Core.JsonContext; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core.Differential; namespace DifferentialTest.Matchers { diff --git a/src/c#/DifferentialTest/Models/DiffProgressTests.cs b/src/c#/DifferentialTest/Models/DiffProgressTests.cs index dbde7eca..409e0083 100644 --- a/src/c#/DifferentialTest/Models/DiffProgressTests.cs +++ b/src/c#/DifferentialTest/Models/DiffProgressTests.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Differential.Models; +using GeneralUpdate.Core.Models; namespace DifferentialTest.Models { diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs index 6135dee9..3862508d 100644 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs +++ b/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs @@ -1,8 +1,8 @@ using Moq; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; -using GeneralUpdate.Differential.Pipeline; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; namespace DifferentialTest.Pipeline { diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs index 0f27cc63..c50950d3 100644 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs +++ b/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs @@ -1,9 +1,9 @@ using System.IO.Compression; using Moq; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; -using GeneralUpdate.Differential.Pipeline; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; namespace DifferentialTest.Pipeline { diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs index cd087655..a3fe499d 100644 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs +++ b/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Differential.Pipeline; +using GeneralUpdate.Core.Pipeline; namespace DifferentialTest.Pipeline { diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs index 9c997629..4dce4b05 100644 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs +++ b/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs @@ -1,8 +1,8 @@ using Moq; using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; -using GeneralUpdate.Differential.Pipeline; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; namespace DifferentialTest.Pipeline { diff --git a/src/c#/GeneralUpdate.Bowl/Bowl.cs b/src/c#/GeneralUpdate.Bowl/Bowl.cs index a2737421..2270e32e 100644 --- a/src/c#/GeneralUpdate.Bowl/Bowl.cs +++ b/src/c#/GeneralUpdate.Bowl/Bowl.cs @@ -6,11 +6,13 @@ using GeneralUpdate.Bowl.Internal; using GeneralUpdate.Bowl.Strategies; using GeneralUpdate.Bowl.Strategys; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Bowl.FileSystem; +using GeneralUpdate.Bowl.Configuration; +using GeneralUpdate.Bowl.Ipc; + +using GeneralUpdate.Bowl; +using GeneralUpdate.Bowl.Configuration; +using GeneralUpdate.Bowl.Ipc; namespace GeneralUpdate.Bowl; @@ -163,7 +165,7 @@ private async Task HandleCrashAsync( { GeneralTracer.Info($"Bowl.HandleCrashAsync: restoring backup from " + $"'{context.BackupDirectory}' to '{context.TargetPath}'."); - StorageManager.Restore(context.BackupDirectory, context.TargetPath); + StorageHelper.Restore(context.BackupDirectory, context.TargetPath); restored = true; GeneralTracer.Info("Bowl.HandleCrashAsync: restore completed."); } @@ -322,8 +324,7 @@ internal static MonitorParameter CreateParameter() "ProcessInfo environment variable not set."); } - var processInfo = JsonSerializer.Deserialize( - json, ProcessInfoJsonContext.Default.ProcessInfo); + var processInfo = JsonSerializer.Deserialize(json); if (processInfo == null) { GeneralTracer.Fatal("Bowl.CreateParameter: failed to deserialize ProcessInfo JSON."); diff --git a/src/c#/GeneralUpdate.Bowl/Configuration/ProcessInfo.cs b/src/c#/GeneralUpdate.Bowl/Configuration/ProcessInfo.cs new file mode 100644 index 00000000..a915ebb5 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Configuration/ProcessInfo.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace GeneralUpdate.Bowl.Configuration; + +/// +/// Minimal ProcessInfo for Bowl — only the fields needed for crash monitoring and rollback. +/// +public class ProcessInfo +{ + [JsonPropertyName("AppName")] + public string AppName { get; set; } = string.Empty; + + [JsonPropertyName("InstallPath")] + public string InstallPath { get; set; } = string.Empty; + + [JsonPropertyName("LastVersion")] + public string LastVersion { get; set; } = string.Empty; +} diff --git a/src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs b/src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs new file mode 100644 index 00000000..3e028d86 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace GeneralUpdate.Bowl.FileSystem; + +/// +/// Minimal file system utilities for Bowl (backup restore, directory cleanup, JSON serialization). +/// +public static class StorageHelper +{ + public static void Restore(string backupPath, string sourcePath) + { + if (!Directory.Exists(sourcePath)) + Directory.CreateDirectory(sourcePath); + CopyDirectory(backupPath, sourcePath); + } + + private static void CopyDirectory(string sourceDir, string targetDir) + { + foreach (string dirPath in Directory.GetDirectories(sourceDir, "*", SearchOption.TopDirectoryOnly)) + { + string newTargetDir = Path.Combine(targetDir, Path.GetFileName(dirPath)); + Directory.CreateDirectory(newTargetDir); + CopyDirectory(dirPath, newTargetDir); + } + + foreach (string filePath in Directory.GetFiles(sourceDir, "*.*", SearchOption.TopDirectoryOnly)) + { + string newFilePath = Path.Combine(targetDir, Path.GetFileName(filePath)); + File.Copy(filePath, newFilePath, true); + } + } + + public static void DeleteDirectory(string targetDir) + { + foreach (var file in Directory.GetFiles(targetDir)) + { + File.SetAttributes(file, FileAttributes.Normal); + File.Delete(file); + } + + foreach (var dir in Directory.GetDirectories(targetDir)) + { + DeleteDirectory(dir); + } + + Directory.Delete(targetDir, false); + } + + public static void CreateJson(string targetPath, T obj) where T : class + { + var folderPath = Path.GetDirectoryName(targetPath) ?? + throw new ArgumentException("invalid path", nameof(targetPath)); + + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + var jsonString = JsonSerializer.Serialize(obj); + File.WriteAllText(targetPath, jsonString); + } +} diff --git a/src/c#/GeneralUpdate.Bowl/GeneralUpdate.Bowl.csproj b/src/c#/GeneralUpdate.Bowl/GeneralUpdate.Bowl.csproj index 2379360a..ba3fd836 100644 --- a/src/c#/GeneralUpdate.Bowl/GeneralUpdate.Bowl.csproj +++ b/src/c#/GeneralUpdate.Bowl/GeneralUpdate.Bowl.csproj @@ -18,10 +18,6 @@ netstandard2.0; - - - - diff --git a/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs b/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs index 2aba8195..ce258c8a 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs @@ -3,8 +3,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl.FileSystem; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Internal; @@ -38,7 +38,7 @@ public Task GenerateReportAsync( }; var failJsonPath = Path.Combine(context.FailDirectory, context.FailFileName); - StorageManager.CreateJson(failJsonPath, crash, CrashJsonContext.Default.Crash); + StorageHelper.CreateJson(failJsonPath, crash); GeneralTracer.Info($"CrashReporter.GenerateReportAsync: report written to {failJsonPath}."); return Task.FromResult(failJsonPath); diff --git a/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs b/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs index fad2ae8a..2b4ff58b 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Bowl.Configuration; +using GeneralUpdate.Bowl.Ipc; namespace GeneralUpdate.Bowl.Internal; diff --git a/src/c#/GeneralUpdate.Bowl/Internal/WindowsSystemInfoProvider.cs b/src/c#/GeneralUpdate.Bowl/Internal/WindowsSystemInfoProvider.cs index c5fcd292..e9136078 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/WindowsSystemInfoProvider.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/WindowsSystemInfoProvider.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Internal; diff --git a/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs b/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs new file mode 100644 index 00000000..0f1a82b6 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace GeneralUpdate.Bowl.Ipc; + +/// +/// Secure IPC environment variable provider. +/// AES-encrypted temp files in a dedicated subdirectory, auto-deleted after read. +/// +public static class Environments +{ + 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 }; + + private static string IpcDir + { + get + { + var dir = Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc"); + Directory.CreateDirectory(dir); + return dir; + } + } + + public static void SetEnvironmentVariable(string key, string value) + { + var filePath = Path.Combine(IpcDir, $"{key}.enc"); + var plainBytes = Encoding.UTF8.GetBytes(value); + IpcEncryption.EncryptToFile(plainBytes, filePath, _aesKey, _aesIV); + } + + public static string GetEnvironmentVariable(string key) + { + var filePath = Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc", $"{key}.enc"); + var plainBytes = IpcEncryption.DecryptFromFile(filePath, _aesKey, _aesIV); + return plainBytes != null ? Encoding.UTF8.GetString(plainBytes) : string.Empty; + } +} diff --git a/src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs b/src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs new file mode 100644 index 00000000..f5509c67 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace GeneralUpdate.Bowl.Ipc; + +/// +/// Shared AES encryption utilities for IPC. +/// +public static class IpcEncryption +{ + 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); + } + + 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.Bowl/Strategies/LinuxBowlStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs index 6a48f1e1..e7b55322 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategies; diff --git a/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs index 9d5f3198..ac3f7b28 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategies; diff --git a/src/c#/GeneralUpdate.Bowl/Strategies/ProcessRunner.cs b/src/c#/GeneralUpdate.Bowl/Strategies/ProcessRunner.cs index 2b679897..5d5cfa52 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/ProcessRunner.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/ProcessRunner.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategies; diff --git a/src/c#/GeneralUpdate.Bowl/Strategies/WindowsBowlStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategies/WindowsBowlStrategy.cs index dd1c2bfe..8b0795f8 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/WindowsBowlStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/WindowsBowlStrategy.cs @@ -4,8 +4,8 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl.FileSystem; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategies; @@ -64,7 +64,7 @@ public Task PostProcessAsync(in BowlContext context, private static void EnsureDirectory(string path) { if (Directory.Exists(path)) - StorageManager.DeleteDirectory(path); + StorageHelper.DeleteDirectory(path); Directory.CreateDirectory(path); } } diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs index b207b8a1..696e3560 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl.FileSystem; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategys; @@ -27,7 +27,7 @@ private void Startup(string appName, string arguments) if (Directory.Exists(_parameter.FailDirectory)) { GeneralTracer.Info($"AbstractStrategy.Startup: removing existing fail directory: {_parameter.FailDirectory}"); - StorageManager.DeleteDirectory(_parameter.FailDirectory); + StorageHelper.DeleteDirectory(_parameter.FailDirectory); } Directory.CreateDirectory(_parameter.FailDirectory); GeneralTracer.Info($"AbstractStrategy.Startup: fail directory created: {_parameter.FailDirectory}"); diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs index 03af8d7e..5244276c 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategys; diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs index dd8a2130..f57a3da4 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs @@ -4,9 +4,10 @@ using System.IO; using System.Runtime.InteropServices; using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core; +using GeneralUpdate.Bowl.FileSystem; +using GeneralUpdate.Bowl.Configuration; +using GeneralUpdate.Bowl.Ipc; +using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategys; @@ -85,7 +86,7 @@ private void CreateCrash() ProcdumpOutPutLines = OutputList }; var failJsonPath = Path.Combine(_parameter.FailDirectory, _parameter.FailFileName); - StorageManager.CreateJson(failJsonPath, crash, CrashJsonContext.Default.Crash); + StorageHelper.CreateJson(failJsonPath, crash); GeneralTracer.Info($"WindowStrategy.CreateCrash: crash report written to {failJsonPath}."); } @@ -115,7 +116,7 @@ private void Restore() if (string.Equals(_parameter.WorkModel, WorkModel)) { GeneralTracer.Info($"WindowStrategy.Restore: restoring backup from {_parameter.BackupDirectory} to {_parameter.TargetPath}."); - StorageManager.Restore(_parameter.BackupDirectory, _parameter.TargetPath); + StorageHelper.Restore(_parameter.BackupDirectory, _parameter.TargetPath); GeneralTracer.Info("WindowStrategy.Restore: restore completed successfully."); } else diff --git a/src/c#/GeneralUpdate.Bowl/Tracer/GeneralTracer.cs b/src/c#/GeneralUpdate.Bowl/Tracer/GeneralTracer.cs new file mode 100644 index 00000000..11084795 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Tracer/GeneralTracer.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Bowl; + +public static class GeneralTracer +{ + private static readonly object _lockObj = new(); + private static bool _isTracingEnabled; + private static string _currentLogDate; + private static TextTraceListener _fileListener; + + static GeneralTracer() + { + Trace.Listeners.Clear(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Trace.Listeners.Add(new WindowsOutputDebugListener()); + } + + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out) { Name = "ConsoleListener" }); + + InitializeFileListener(); + + if (Debugger.IsAttached) + Trace.Listeners.Add(new DefaultTraceListener()); + + Trace.AutoFlush = true; + _isTracingEnabled = true; + } + + private static void InitializeFileListener() + { + //Ensure that log files are rotated on a daily basis + var today = DateTime.Now.ToString("yyyy-MM-dd"); + if (today == _currentLogDate && _fileListener != null) + return; + + if (_fileListener != null) + { + Trace.Listeners.Remove(_fileListener); + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + } + + var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + Directory.CreateDirectory(logDir); + + var logFileName = Path.Combine(logDir, $"generalupdate-trace {today}.log"); + _fileListener = new TextTraceListener(logFileName); + + Trace.Listeners.Add(_fileListener); + _currentLogDate = today; + } + + public static void Debug(string message) => WriteTraceMessage(TraceLevel.Verbose, message); + + public static void Info(string message) => WriteTraceMessage(TraceLevel.Info, message); + + public static void Warn(string message) => WriteTraceMessage(TraceLevel.Warning, message); + + public static void Error(string message) => WriteTraceMessage(TraceLevel.Error, message); + + public static void Fatal(string message) => WriteTraceMessage(TraceLevel.Off, message); + + public static void Error(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Error, fullMessage); + } + + public static void Fatal(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Off, fullMessage); + } + + public static void SetTracingEnabled(bool enabled) + { + lock (_lockObj) + { + Trace.AutoFlush = enabled; + _isTracingEnabled = enabled; + foreach (TraceListener listener in Trace.Listeners) + { + listener.Filter = enabled ? null : new EventTypeFilter(SourceLevels.Off); + } + } + } + + public static bool IsTracingEnabled() + { + lock (_lockObj) + { + return _isTracingEnabled; + } + } + + private static void WriteTraceMessage(TraceLevel level, string message) + { + if(!IsTracingEnabled()) + return; + + InitializeFileListener(); + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var levelName = GetLevelName(level); + var fullMessage = string.Empty; + + try + { + var stackFrame = new StackFrame(2, true); + var method = stackFrame.GetMethod(); + var className = method.DeclaringType?.Name ?? "UnknownType"; + var methodName = method.Name; + var lineNumber = stackFrame.GetFileLineNumber(); + var lineInfo = lineNumber > 0 ? $"Line {lineNumber}" : "Line N/A (Line numbers may not be displayed in Release mode)"; + fullMessage = $"[{timestamp}] [{levelName}] {className}.{methodName} ({lineInfo}): {message}"; + } + catch + { + fullMessage = $"[{timestamp}] [{levelName}] : {message}"; + } + + Trace.WriteLine(fullMessage); + } + + private static string GetLevelName(TraceLevel level) => level switch + { + TraceLevel.Verbose => "DEBUG", + TraceLevel.Info => "INFO", + TraceLevel.Warning => "WARN", + TraceLevel.Error => "ERROR", + TraceLevel.Off => "FATAL", + _ => "UNKNOWN" + }; + + public static void Dispose() + { + try + { + lock (_lockObj) + { + if (_fileListener is not null) + { + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + _fileListener = null; + } + + Trace.Listeners.Clear(); + } + } + catch + { + // ignored + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs b/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs new file mode 100644 index 00000000..bd513e72 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Concurrent; +using System.Threading; + +public class TextTraceListener : TraceListener, IDisposable +{ + private readonly string _filePath; + private readonly BlockingCollection _messageQueue; + private readonly Thread _loggingThread; + private volatile bool _isDisposed; + + public TextTraceListener(string filePath) + { + _filePath = filePath; + _messageQueue = new BlockingCollection(); + _loggingThread = new Thread(ProcessQueue); + _loggingThread.IsBackground = true; + _loggingThread.Start(); + } + + public override void Write(string? message) + { + QueueMessage(message); + } + + public override void WriteLine(string? message) + { + QueueMessage(message + Environment.NewLine); + } + + private void QueueMessage(string? message) + { + if (!_isDisposed && message != null) + { + _messageQueue.Add(message); + } + } + + private void ProcessQueue() + { + foreach (var message in _messageQueue.GetConsumingEnumerable()) + { + using (var fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) + using (var writer = new StreamWriter(fileStream)) + { + writer.Write(message); + } + } + } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + _messageQueue.CompleteAdding(); + _loggingThread.Join(); + _messageQueue.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs b/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs new file mode 100644 index 00000000..0dc55624 --- /dev/null +++ b/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Bowl; + +public class WindowsOutputDebugListener : TraceListener +{ + /// + /// Does not affect .NET AOT compilation and runtime on the Windows platform, provided that the following conditions are met: + /// The target platform is restricted to Windows (due to the dependency on kernel32.dll); + /// The function declaration is static (not dynamically generated). + /// This syntax is safe in an AOT environment and can properly work with Dbgview.exe to capture logs. + /// + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern void OutputDebugString(string lpOutputString); + + public override void Write(string message) + { + if (!string.IsNullOrEmpty(message)) + { + OutputDebugString(message); + } + } + + public override void WriteLine(string message) + { + Write($"{message}{Environment.NewLine}"); + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 027af08c..24690498 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -19,6 +19,7 @@ using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Download.Reporting; using GeneralUpdate.Core.Differential; +using GeneralUpdate.Differential.Abstractions; namespace GeneralUpdate.Core; diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs index 36c396c6..2790c66c 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs @@ -6,6 +6,7 @@ using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Differential.Abstractions; namespace GeneralUpdate.Core.Configuration { @@ -86,8 +87,8 @@ protected T GetOption(UpdateOption? option) public TBootstrap DirtyStrategy() where T : Differential.IDirtyStrategy, new() { _extensions[typeof(Differential.IDirtyStrategy)] = typeof(T); return (TBootstrap)this; } - public TBootstrap BinaryDiffer() where T : Differential.IBinaryDiffer, new() - { _extensions[typeof(Differential.IBinaryDiffer)] = typeof(T); return (TBootstrap)this; } + public TBootstrap BinaryDiffer() where T : IBinaryDiffer, new() + { _extensions[typeof(IBinaryDiffer)] = typeof(T); return (TBootstrap)this; } public TBootstrap ConfigureBlackList(BlackListConfig config) { diff --git a/src/c#/GeneralUpdate.Core/Differential/DefaultCleanMatcher.cs b/src/c#/GeneralUpdate.Core/Differential/DefaultCleanMatcher.cs new file mode 100644 index 00000000..fe8d4a55 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/DefaultCleanMatcher.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GeneralUpdate.Core.FileSystem; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Default implementation of that preserves the +/// original behaviour of the Clean phase. +/// +/// delegates to . +/// delegates to . +/// considers a new file matched to an old file when both share the +/// same name, both exist on disk, and they reside at the same relative path. +/// +/// +public class DefaultCleanMatcher : ICleanMatcher +{ + private readonly StorageManager _storageManager = new StorageManager(); + + /// + public ComparisonResult Compare(string sourcePath, string targetPath) + => _storageManager.Compare(sourcePath, targetPath); + + /// + public IEnumerable? Except(string sourcePath, string targetPath) + => _storageManager.Except(sourcePath, targetPath); + + /// + public FileNode? Match(FileNode newFile, IEnumerable leftNodes) + { + var oldFile = leftNodes.FirstOrDefault(i => + string.Equals(i.Name, newFile.Name, System.StringComparison.OrdinalIgnoreCase) && + string.Equals(i.RelativePath, newFile.RelativePath, System.StringComparison.OrdinalIgnoreCase)); + if (oldFile is null) return null; + if (!File.Exists(oldFile.FullName)) return null; + if (!File.Exists(newFile.FullName)) return null; + return oldFile; + } +} diff --git a/src/c#/GeneralUpdate.Core/Differential/DefaultCleanStrategy.cs b/src/c#/GeneralUpdate.Core/Differential/DefaultCleanStrategy.cs new file mode 100644 index 00000000..cabf23cc --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/DefaultCleanStrategy.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using GeneralUpdate.Core.FileSystem; +using GeneralUpdate.Core.JsonContext; +using GeneralUpdate.Differential.Abstractions; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Default implementation of that preserves the original +/// behaviour of the Clean (patch-generation) phase. +/// +/// An optional can be supplied to customise the directory +/// comparison, deleted-file detection, or per-file matching logic without replacing the +/// entire execution flow. When no matcher is provided, +/// is used. +/// +/// +/// An optional can be supplied to customise the binary diff +/// algorithm. Defaults to with Deflate compression +/// for optimal patch size and speed. +/// +/// +public class DefaultCleanStrategy : ICleanStrategy +{ + private const string PatchFormat = ".patch"; + private const string DeleteFilesName = "generalupdate_delete_files.json"; + + private readonly ICleanMatcher _matcher; + private readonly IBinaryDiffer _binaryDiffer; + + /// + /// Initialises a new instance using StreamingHdiffDiffer with Deflate compression by default. + /// + public DefaultCleanStrategy() + : this(null, null) + { + } + + /// + /// Initialises a new instance, optionally using a custom file-matching strategy + /// and/or a custom binary differ. + /// If no matcher is provided, is used. + /// If no binary differ is provided, (Deflate) is used. + /// + public DefaultCleanStrategy(ICleanMatcher? matcher = null, IBinaryDiffer? binaryDiffer = null) + { + _matcher = matcher ?? new DefaultCleanMatcher(); + _binaryDiffer = binaryDiffer ?? new GeneralUpdate.Differential.Differ.StreamingHdiffDiffer(); + } + + /// + public async Task ExecuteAsync(string sourcePath, string targetPath, string patchPath) + { + var comparisonResult = _matcher.Compare(sourcePath, targetPath); + foreach (var file in comparisonResult.DifferentNodes) + { + var tempDir = GetTempDirectory(file, targetPath, patchPath); + var oldFile = _matcher.Match(file, comparisonResult.LeftNodes); + var newFile = file; + + if (oldFile is not null) + { + if (!StorageManager.HashEquals(oldFile.FullName, newFile.FullName)) + { + var tempPatchPath = Path.Combine(tempDir, $"{file.Name}{PatchFormat}"); + await _binaryDiffer.CleanAsync(oldFile.FullName, newFile.FullName, tempPatchPath); + } + } + else + { + File.Copy(newFile.FullName, Path.Combine(tempDir, Path.GetFileName(newFile.FullName)), true); + } + } + + var exceptFiles = _matcher.Except(sourcePath, targetPath)?.ToList(); + if (exceptFiles is not null && exceptFiles.Any()) + { + var path = Path.Combine(patchPath, DeleteFilesName); + StorageManager.CreateJson(path, exceptFiles, FileNodesJsonContext.Default.ListFileNode); + } + } + + private static string GetTempDirectory(FileNode file, string targetPath, string patchPath) + { + var tempPath = file.FullName.Replace(targetPath, "").Replace(Path.GetFileName(file.FullName), "").Trim(Path.DirectorySeparatorChar); + var tempDir = string.IsNullOrEmpty(tempPath) ? patchPath : Path.Combine(patchPath, tempPath); + Directory.CreateDirectory(tempDir); + return tempDir; + } +} diff --git a/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyMatcher.cs b/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyMatcher.cs new file mode 100644 index 00000000..b8f58458 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyMatcher.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Default implementation of : +/// a patch file matches an application file when the patch file's name +/// (without the .patch extension, case-insensitive) equals the application file's name, +/// and the patch file carries the .patch extension. +/// +public class DefaultDirtyMatcher : IDirtyMatcher +{ + private const string PatchFormat = ".patch"; + + /// + public FileInfo? Match(FileInfo oldFile, IEnumerable patchFiles) + { + var findFile = patchFiles.FirstOrDefault(f => + { + var name = Path.GetFileNameWithoutExtension(f.Name); + if (name.EndsWith(PatchFormat, System.StringComparison.OrdinalIgnoreCase)) + name = name.Substring(0, name.Length - PatchFormat.Length); + return name.Equals(oldFile.Name, System.StringComparison.OrdinalIgnoreCase); + }); + + if (findFile != null && + Path.GetExtension(findFile.FullName).Equals(PatchFormat, System.StringComparison.OrdinalIgnoreCase)) + return findFile; + + return null; + } +} diff --git a/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyStrategy.cs b/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyStrategy.cs new file mode 100644 index 00000000..fca48b57 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/DefaultDirtyStrategy.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using GeneralUpdate.Core.FileSystem; +using GeneralUpdate.Core.HashAlgorithms; +using GeneralUpdate.Core.JsonContext; +using GeneralUpdate.Differential.Abstractions; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Default implementation of for the Dirty (patch-application) phase. +/// +/// +/// An optional can be supplied to customise how an +/// application file is matched to its corresponding patch file. +/// An optional can be supplied to customise the binary diff +/// algorithm. Defaults to with Deflate compression. +/// +/// File replacement strategy: after patching, the strategy performs an atomic +/// delete-and-replace of the original file with the patched output. +/// This ensures correctness regardless of the IBinaryDiffer implementation used. +/// +public class DefaultDirtyStrategy : IDirtyStrategy +{ + private const string DeleteFilesName = "generalupdate_delete_files.json"; + + private readonly IDirtyMatcher _matcher; + private readonly IBinaryDiffer _binaryDiffer; + + /// + /// Initialises a new instance using StreamingHdiffDiffer with Deflate compression by default. + /// + public DefaultDirtyStrategy() + : this(null, null) + { + } + + /// + /// Initialises a new instance, optionally using a custom file-matching strategy + /// and/or a custom binary differ. + /// + public DefaultDirtyStrategy(IDirtyMatcher? matcher = null, IBinaryDiffer? binaryDiffer = null) + { + _matcher = matcher ?? new DefaultDirtyMatcher(); + _binaryDiffer = binaryDiffer ?? new GeneralUpdate.Differential.Differ.StreamingHdiffDiffer(); + } + + /// + public async Task ExecuteAsync(string appPath, string patchPath) + { + if (!Directory.Exists(appPath) || !Directory.Exists(patchPath)) return; + + var skipDirectory = BlackListDefaults.DefaultSkipDirectories; + var patchFiles = StorageManager.GetAllFiles(patchPath, skipDirectory); + var oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory); + HandleDeleteList(patchFiles, oldFiles); + oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory); + foreach (var oldFile in oldFiles) + { + var findFile = _matcher.Match(oldFile, patchFiles); + if (findFile != null) + await ApplyPatch(oldFile.FullName, findFile.FullName); + } + + await CopyUnknownFiles(appPath, patchPath); + } + + private static void HandleDeleteList(IEnumerable patchFiles, IEnumerable oldFiles) + { + var json = patchFiles.FirstOrDefault(i => i.Name.Equals(DeleteFilesName)); + if (json == null) return; + + var deleteFiles = StorageManager.GetJson>(json.FullName, FileNodesJsonContext.Default.ListFileNode); + if (deleteFiles == null) return; + + var hashAlgorithm = new Sha256HashAlgorithm(); + var toDelete = oldFiles + .Where(old => deleteFiles.Any(del => del.Hash.SequenceEqual(hashAlgorithm.ComputeHash(old.FullName)))) + .ToList(); + + foreach (var file in toDelete) + { + if (!File.Exists(file.FullName)) continue; + File.SetAttributes(file.FullName, FileAttributes.Normal); + File.Delete(file.FullName); + } + } + + /// + /// Applies a patch to a single file, then atomically replaces the original with the patched version. + /// The IBinaryDiffer writes to a temp path; this method handles the replacement. + /// + private async Task ApplyPatch(string appFilePath, string patchFilePath) + { + if (!File.Exists(appFilePath) || !File.Exists(patchFilePath)) return; + + var tempPath = Path.Combine( + Path.GetDirectoryName(appFilePath)!, + $"{Path.GetRandomFileName()}_{Path.GetFileName(appFilePath)}"); + + await _binaryDiffer.DirtyAsync(appFilePath, tempPath, patchFilePath); + + if (File.Exists(tempPath) && !File.Exists(appFilePath)) + { + File.Move(tempPath, appFilePath); + } + else if (File.Exists(tempPath) && File.Exists(appFilePath)) + { + File.SetAttributes(appFilePath, FileAttributes.Normal); + File.Delete(appFilePath); + File.Move(tempPath, appFilePath); + } + } + + private static async Task CopyUnknownFiles(string appPath, string patchPath) + { + await Task.Run(() => + { + var fileManager = new StorageManager(); + var comparisonResult = fileManager.Compare(appPath, patchPath); + foreach (var file in comparisonResult.DifferentNodes) + { + var extensionName = Path.GetExtension(file.FullName); + if (BlackListDefaults.DefaultBlackFormats.Contains(extensionName)) continue; + + var targetFileName = file.FullName.Replace(patchPath, "").TrimStart(Path.DirectorySeparatorChar); + var targetPath = Path.Combine(appPath, targetFileName); + var parentFolder = Directory.GetParent(targetPath); + if (parentFolder?.Exists == false) + parentFolder.Create(); + + File.Copy(file.FullName, targetPath, true); + } + + if (Directory.Exists(patchPath)) + StorageManager.DeleteDirectory(patchPath); + }); + } +} diff --git a/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs b/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs deleted file mode 100644 index 1cb56e23..00000000 --- a/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace GeneralUpdate.Core.Differential; - -/// -/// Pluggable file-level binary patch-application algorithm. -/// Implement this to customize how individual files are patched (BSDIFF, HDiffPatch, etc.). -/// -/// For full directory-level control, inject instead. -/// -public interface IBinaryDiffer -{ - /// Applies a binary patch: oldFile + patchFile → newFile. - Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath, - CancellationToken cancellationToken = default); -} diff --git a/src/c#/GeneralUpdate.Core/Differential/ICleanMatcher.cs b/src/c#/GeneralUpdate.Core/Differential/ICleanMatcher.cs new file mode 100644 index 00000000..547d054f --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/ICleanMatcher.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using GeneralUpdate.Core.FileSystem; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Defines the complete matching strategy used during the Clean (diff-generation) phase. +/// Implementations are responsible for directory comparison, identifying deleted files, +/// and matching individual new files to their corresponding old files. +/// +public interface ICleanMatcher +{ + /// + /// Compares the source and target directories and returns the set of changed files. + /// + /// The source (old-version) directory. + /// The target (new-version) directory. + ComparisonResult Compare(string sourcePath, string targetPath); + + /// + /// Returns the files that exist only in the source directory (i.e. files to be deleted). + /// + /// The source (old-version) directory. + /// The target (new-version) directory. + IEnumerable? Except(string sourcePath, string targetPath); + + /// + /// Attempts to find the corresponding old file node for + /// from the left-side (source) node collection. + /// + /// The new file node from the target directory. + /// All file nodes from the source directory. + /// + /// The matching old , or null if no match is found + /// (indicating the file is brand-new and should be copied directly). + /// + FileNode? Match(FileNode newFile, IEnumerable leftNodes); +} diff --git a/src/c#/GeneralUpdate.Core/Differential/IDirtyMatcher.cs b/src/c#/GeneralUpdate.Core/Differential/IDirtyMatcher.cs new file mode 100644 index 00000000..43f7347c --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/IDirtyMatcher.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.IO; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Defines the matching logic used during the Dirty (patch-application) phase to +/// find the patch file that corresponds to an existing application file. +/// +public interface IDirtyMatcher +{ + /// + /// Attempts to find the patch file for from the + /// collection of available patch files. + /// + /// The existing application file to be patched. + /// All files available in the patch directory. + /// + /// The matching patch , or null if no patch exists + /// for the given file. + /// + FileInfo? Match(FileInfo oldFile, IEnumerable patchFiles); +} diff --git a/src/c#/GeneralUpdate.Core/DifferentialCore.cs b/src/c#/GeneralUpdate.Core/DifferentialCore.cs new file mode 100644 index 00000000..9779d292 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/DifferentialCore.cs @@ -0,0 +1,77 @@ +using System.Threading.Tasks; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Differential.Abstractions; + +namespace GeneralUpdate.Core; + +/// +/// Entry point for differential update operations. +/// +/// +/// Defaults to with Deflate compression +/// for smaller patches and faster application than the legacy BSDIFF algorithm. +/// +/// Pass a custom to change the diff algorithm, +/// or a custom / +/// for full control over execution flow. +/// +/// Legacy BSDIFF patches remain readable via . +/// +public static class DifferentialCore +{ + /// + /// Generates a binary patch from (old version) + /// to (new version), writing to . + /// + /// + /// The binary differ to use. Defaults to . + /// When is non-null, the strategy owns the differ + /// and this parameter is ignored. + /// + /// + /// Full execution strategy override. Defaults to . + /// + public static Task Clean( + string sourcePath, + string targetPath, + string patchPath, + IBinaryDiffer? binaryDiffer = null, + ICleanStrategy? strategy = null) + { + var usedStrategy = strategy ?? new DefaultCleanStrategy( + matcher: null, + binaryDiffer: binaryDiffer); + return usedStrategy.ExecuteAsync(sourcePath, targetPath, patchPath); + } + + // Backward-compatible overload: 4th positional argument is a strategy. + public static Task Clean(string sourcePath, string targetPath, string patchPath, ICleanStrategy? strategy) + => Clean(sourcePath, targetPath, patchPath, binaryDiffer: null, strategy: strategy); + + /// + /// Applies a binary patch from to . + /// + /// + /// The binary differ to use. Defaults to . + /// When is non-null, the strategy owns the differ + /// and this parameter is ignored. + /// + /// + /// Full execution strategy override. Defaults to . + /// + public static Task Dirty( + string appPath, + string patchPath, + IBinaryDiffer? binaryDiffer = null, + IDirtyStrategy? strategy = null) + { + var usedStrategy = strategy ?? new DefaultDirtyStrategy( + matcher: null, + binaryDiffer: binaryDiffer); + return usedStrategy.ExecuteAsync(appPath, patchPath); + } + + // Backward-compatible overload: 3rd positional argument is a strategy. + public static Task Dirty(string appPath, string patchPath, IDirtyStrategy? strategy) + => Dirty(appPath, patchPath, binaryDiffer: null, strategy: strategy); +} diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj index cf93fe4e..b8397dd9 100644 --- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj +++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj @@ -39,4 +39,8 @@ + + + + diff --git a/src/c#/GeneralUpdate.Core/Models/DiffProgress.cs b/src/c#/GeneralUpdate.Core/Models/DiffProgress.cs new file mode 100644 index 00000000..5dd40415 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Models/DiffProgress.cs @@ -0,0 +1,41 @@ +namespace GeneralUpdate.Core.Models; + +/// +/// Progress information for differential pipeline operations. +/// +public readonly struct DiffProgress +{ + /// Number of files processed so far. + public int Completed { get; } + + /// Total number of files to process. + public int Total { get; } + + /// Name of the file currently being processed. + public string? CurrentFile { get; } + + /// Percentage complete (0-100). + public double Percentage => Total > 0 ? (double)Completed / Total * 100.0 : 100.0; + + /// Whether the operation is complete. + public bool IsComplete => Completed >= Total; + + /// Error message if the current file failed (null on success). + public string? Error { get; } + + public DiffProgress(int completed, int total, string? currentFile, string? error = null) + { + Completed = completed; + Total = total; + CurrentFile = currentFile; + Error = error; + } + + /// Creates a completion marker. + public static DiffProgress Complete(int total) => new DiffProgress(total, total, null); + + public override string ToString() + => IsComplete + ? $"Complete: {Completed}/{Total} files" + : $"[{Percentage:F1}%] {Completed}/{Total} -- {CurrentFile ?? "..."}{(Error != null ? $" (failed: {Error})" : "")}"; +} diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipeline.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipeline.cs new file mode 100644 index 00000000..c89ae2b3 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipeline.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.FileSystem; +using GeneralUpdate.Core.HashAlgorithms; +using GeneralUpdate.Core.JsonContext; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; + +namespace GeneralUpdate.Core.Pipeline; + +/// +/// Parallel differential pipeline with configurable parallelism, progress reporting, +/// pluggable matchers, and cancellation. +/// +/// +/// Use for fluent configuration. +/// Direct construction is also supported for scenarios where a builder is unnecessary. +/// +public class DiffPipeline +{ + private readonly DiffPipelineOptions _options; + private readonly IBinaryDiffer _binaryDiffer; + private readonly ICleanMatcher _cleanMatcher; + private readonly IDirtyMatcher _dirtyMatcher; + private readonly IProgress? _progress; + + private const string PatchExtension = ".patch"; + private const string DeleteListFileName = "generalupdate_delete_files.json"; + + /// + /// Initialises a new pipeline with default options and matchers. + /// + public DiffPipeline() + : this(new DiffPipelineOptions(), new StreamingHdiffDiffer(), null, null, null) + { + } + + /// + /// Initialises a new pipeline with the specified options. + /// + public DiffPipeline(DiffPipelineOptions options) + : this(options, new StreamingHdiffDiffer(), null, null, null) + { + } + + /// + /// Initialises a new pipeline with full configuration. + /// + /// Pipeline options. Must not be null. + /// Binary differ. Must not be null. + /// Clean-phase file matcher. Defaults to . + /// Dirty-phase file matcher. Defaults to . + /// Optional progress reporter. + public DiffPipeline( + DiffPipelineOptions options, + IBinaryDiffer binaryDiffer, + ICleanMatcher? cleanMatcher = null, + IDirtyMatcher? dirtyMatcher = null, + IProgress? progress = null) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _binaryDiffer = binaryDiffer ?? throw new ArgumentNullException(nameof(binaryDiffer)); + _cleanMatcher = cleanMatcher ?? new DefaultCleanMatcher(); + _dirtyMatcher = dirtyMatcher ?? new DefaultDirtyMatcher(); + _progress = progress; + } + + /// + /// Initialises a new pipeline (backward-compatible constructor, preserved for binary compatibility). + /// + public DiffPipeline(DiffPipelineOptions options, IBinaryDiffer binaryDiffer, IProgress? progress = null) + : this(options, binaryDiffer, null, null, progress) + { + } + + /// + /// Compares source and target directories using the configured clean matcher, + /// generating patch files in parallel. + /// + public async Task CleanAsync( + string sourcePath, + string targetPath, + string patchPath, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + var reporter = progress ?? _progress; + ValidateDirectories(sourcePath, targetPath, patchPath); + + var comparisonResult = _cleanMatcher.Compare(sourcePath, targetPath); + var differentFiles = comparisonResult.DifferentNodes.ToList(); + var leftNodes = comparisonResult.LeftNodes.ToList(); + + int total = differentFiles.Count; + if (total == 0) + { + reporter?.Report(DiffProgress.Complete(0)); + return; + } + + int completed = 0; + var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism); + + var tasks = differentFiles.Select(file => Task.Run(async () => + { + await semaphore.WaitAsync(cancellationToken); + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var tempDir = GetTempDirectory(file, targetPath, patchPath); + var oldFile = _cleanMatcher.Match(file, leftNodes); + + if (oldFile != null) + { + if (!StorageManager.HashEquals(oldFile.FullName, file.FullName)) + { + var tempPatchPath = Path.Combine(tempDir, $"{file.Name}{PatchExtension}"); + await _binaryDiffer.CleanAsync(oldFile.FullName, file.FullName, tempPatchPath, cancellationToken); + } + } + else + { + File.Copy(file.FullName, Path.Combine(tempDir, Path.GetFileName(file.FullName)), true); + } + + int done = Interlocked.Increment(ref completed); + reporter?.Report(new DiffProgress(done, total, file.Name)); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (!_options.StopOnFirstError) + { + int done = Interlocked.Increment(ref completed); + reporter?.Report(new DiffProgress(done, total, file.Name, ex.Message)); + } + finally + { + semaphore.Release(); + } + }, cancellationToken)); + + await Task.WhenAll(tasks); + + var exceptFiles = _cleanMatcher.Except(sourcePath, targetPath)?.ToList(); + if (exceptFiles is { Count: > 0 }) + { + var deletePath = Path.Combine(patchPath, DeleteListFileName); + StorageManager.CreateJson(deletePath, exceptFiles, FileNodesJsonContext.Default.ListFileNode); + } + + reporter?.Report(DiffProgress.Complete(total)); + } + + /// + /// Applies patches from patchPath to appPath in parallel, + /// using the configured dirty matcher. + /// + public async Task DirtyAsync( + string appPath, + string patchPath, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + var reporter = progress ?? _progress; + if (!Directory.Exists(appPath) || !Directory.Exists(patchPath)) return; + + var skipDirectory = BlackListDefaults.DefaultSkipDirectories; + var patchFiles = StorageManager.GetAllFiles(patchPath, skipDirectory).ToList(); + var oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory).ToList(); + + HandleDeleteList(patchFiles, oldFiles); + oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory).ToList(); + + int total = oldFiles.Count; + if (total == 0) + { + reporter?.Report(DiffProgress.Complete(0)); + await CopyUnknownFiles(appPath, patchPath); + return; + } + + int completed = 0; + var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism); + + var matchedPairs = new List<(FileInfo OldFile, FileInfo PatchFile)>(); + foreach (var oldFile in oldFiles) + { + var patchFile = _dirtyMatcher.Match(oldFile, patchFiles); + if (patchFile != null) + matchedPairs.Add((oldFile, patchFile)); + } + + var tasks = matchedPairs.Select(pair => Task.Run(async () => + { + await semaphore.WaitAsync(cancellationToken); + try + { + cancellationToken.ThrowIfCancellationRequested(); + await ApplyPatch(pair.OldFile.FullName, pair.PatchFile.FullName, cancellationToken); + + int done = Interlocked.Increment(ref completed); + reporter?.Report(new DiffProgress(done, total, pair.OldFile.Name)); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (!_options.StopOnFirstError) + { + int done = Interlocked.Increment(ref completed); + reporter?.Report(new DiffProgress(done, total, pair.OldFile.Name, ex.Message)); + } + finally + { + semaphore.Release(); + } + }, cancellationToken)); + + await Task.WhenAll(tasks); + reporter?.Report(DiffProgress.Complete(total)); + await CopyUnknownFiles(appPath, patchPath); + } + + private async Task ApplyPatch(string appFilePath, string patchFilePath, CancellationToken ct) + { + if (!File.Exists(appFilePath) || !File.Exists(patchFilePath)) return; + + var tempPath = Path.Combine( + Path.GetDirectoryName(appFilePath)!, + $"{Path.GetRandomFileName()}_{Path.GetFileName(appFilePath)}"); + + await _binaryDiffer.DirtyAsync(appFilePath, tempPath, patchFilePath, ct); + + if (File.Exists(tempPath)) + { + if (File.Exists(appFilePath)) + { + File.SetAttributes(appFilePath, FileAttributes.Normal); + File.Delete(appFilePath); + } + File.Move(tempPath, appFilePath); + } + } + + private static void HandleDeleteList(IEnumerable patchFiles, IEnumerable oldFiles) + { + var json = patchFiles.FirstOrDefault(i => i.Name.Equals(DeleteListFileName, StringComparison.OrdinalIgnoreCase)); + if (json == null) return; + + var deleteFiles = StorageManager.GetJson>(json.FullName, FileNodesJsonContext.Default.ListFileNode); + if (deleteFiles == null) return; + + var hashAlgorithm = new Sha256HashAlgorithm(); + var toDelete = oldFiles + .Where(old => deleteFiles.Any(del => del.Hash.SequenceEqual(hashAlgorithm.ComputeHash(old.FullName)))) + .ToList(); + + foreach (var file in toDelete) + { + if (!File.Exists(file.FullName)) continue; + File.SetAttributes(file.FullName, FileAttributes.Normal); + File.Delete(file.FullName); + } + } + + private static Task CopyUnknownFiles(string appPath, string patchPath) + { + return Task.Run(() => + { + var fileManager = new StorageManager(); + var comparisonResult = fileManager.Compare(appPath, patchPath); + foreach (var file in comparisonResult.DifferentNodes) + { + var extensionName = Path.GetExtension(file.FullName); + if (BlackListDefaults.DefaultBlackFormats.Contains(extensionName)) continue; + + var targetFileName = file.FullName.Replace(patchPath, "").TrimStart(Path.DirectorySeparatorChar); + var targetPath = Path.Combine(appPath, targetFileName); + var parentFolder = Directory.GetParent(targetPath); + if (parentFolder?.Exists == false) + parentFolder.Create(); + + File.Copy(file.FullName, targetPath, true); + } + + if (Directory.Exists(patchPath)) + StorageManager.DeleteDirectory(patchPath); + }); + } + + private static string GetTempDirectory(FileNode file, string targetPath, string patchPath) + { + var tempPath = file.FullName + .Replace(targetPath, "") + .Replace(Path.GetFileName(file.FullName), "") + .Trim(Path.DirectorySeparatorChar); + var tempDir = string.IsNullOrEmpty(tempPath) ? patchPath : Path.Combine(patchPath, tempPath); + Directory.CreateDirectory(tempDir); + return tempDir; + } + + private static void ValidateDirectories(string sourcePath, string targetPath, string patchPath) + { + if (string.IsNullOrWhiteSpace(sourcePath)) throw new ArgumentNullException(nameof(sourcePath)); + if (string.IsNullOrWhiteSpace(targetPath)) throw new ArgumentNullException(nameof(targetPath)); + if (string.IsNullOrWhiteSpace(patchPath)) throw new ArgumentNullException(nameof(patchPath)); + + if (!Directory.Exists(sourcePath)) + throw new DirectoryNotFoundException($"Source directory not found: {sourcePath}"); + if (!Directory.Exists(targetPath)) + throw new DirectoryNotFoundException($"Target directory not found: {targetPath}"); + } +} diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs new file mode 100644 index 00000000..9b876487 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs @@ -0,0 +1,108 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Differential.Abstractions; +using GeneralUpdate.Differential.Differ; + +namespace GeneralUpdate.Core.Pipeline; + +/// +/// Fluent builder for instances. +/// +/// +/// Usage: +/// +/// var pipeline = new DiffPipelineBuilder() +/// .UseDiffer(new StreamingHdiffDiffer()) +/// .UseCleanMatcher(new DefaultCleanMatcher()) +/// .WithParallelism(Environment.ProcessorCount) +/// .Build(); +/// await pipeline.CleanAsync(src, tgt, patch); +/// +/// +public class DiffPipelineBuilder +{ + private IBinaryDiffer? _differ; + private ICleanMatcher? _cleanMatcher; + private IDirtyMatcher? _dirtyMatcher; + private int _maxParallelism = Environment.ProcessorCount; + private bool _stopOnFirstError; + private IProgress? _progress; + + /// + /// Sets the binary differ to use. Defaults to if not set. + /// + public DiffPipelineBuilder UseDiffer(IBinaryDiffer differ) + { + _differ = differ ?? throw new ArgumentNullException(nameof(differ)); + return this; + } + + /// + /// Sets a custom clean matcher for directory comparison and file matching during patch generation. + /// Defaults to if not set. + /// + public DiffPipelineBuilder UseCleanMatcher(ICleanMatcher matcher) + { + _cleanMatcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); + return this; + } + + /// + /// Sets a custom dirty matcher for matching patch files to application files during patch application. + /// Defaults to if not set. + /// + public DiffPipelineBuilder UseDirtyMatcher(IDirtyMatcher matcher) + { + _dirtyMatcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); + return this; + } + + /// + /// Sets the maximum degree of parallelism for file processing. + /// Default: . + /// + public DiffPipelineBuilder WithParallelism(int maxDegreeOfParallelism) + { + if (maxDegreeOfParallelism < 1) + throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); + _maxParallelism = maxDegreeOfParallelism; + return this; + } + + /// + /// Sets whether to stop processing on the first file error. + /// Default: false (continue processing, report errors via progress). + /// + public DiffPipelineBuilder WithStopOnFirstError(bool stopOnFirstError = true) + { + _stopOnFirstError = stopOnFirstError; + return this; + } + + /// + /// Attaches a progress reporter for real-time file-level status updates. + /// + public DiffPipelineBuilder WithProgress(IProgress progress) + { + _progress = progress ?? throw new ArgumentNullException(nameof(progress)); + return this; + } + + /// + /// Builds the configured . + /// + public DiffPipeline Build() + { + var options = new DiffPipelineOptions + { + MaxDegreeOfParallelism = _maxParallelism, + StopOnFirstError = _stopOnFirstError + }; + + var differ = _differ ?? new StreamingHdiffDiffer(); + return new DiffPipeline(options, differ, _cleanMatcher, _dirtyMatcher, _progress); + } +} diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs new file mode 100644 index 00000000..64200856 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs @@ -0,0 +1,28 @@ +using System; + +namespace GeneralUpdate.Core.Pipeline; + +/// +/// Configuration options for . +/// +public sealed class DiffPipelineOptions +{ + /// + /// Gets or sets the maximum number of files to process concurrently. + /// Default: . + /// Set to 1 for sequential processing. + /// + public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; + + /// + /// Gets or sets whether to stop processing on first error. + /// Default: false (continue processing other files, report errors via progress). + /// + public bool StopOnFirstError { get; set; } = false; + + /// + /// Gets or sets whether to delete the patch directory after successful apply. + /// Default: true. + /// + public bool DeletePatchAfterApply { get; set; } = true; +} diff --git a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs index 08a01cf1..750ea70e 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using GeneralUpdate.Core.Differential; using GeneralUpdate.Core.FileSystem; +using GeneralUpdate.Differential.Abstractions; using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Pipeline; using GeneralUpdate.Core; diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 465f720e..f3efd281 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -1,4 +1,5 @@ using GeneralUpdate.Core.Differential; +using GeneralUpdate.Differential.Abstractions; using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 7f52fc05..1a171abb 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -1,4 +1,5 @@ using GeneralUpdate.Core.Differential; +using GeneralUpdate.Differential.Abstractions; using System; using System.Runtime.InteropServices; using System.Text; diff --git a/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs b/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs index 637053ca..759c66cd 100644 --- a/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs +++ b/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs @@ -1,15 +1,18 @@ using System.Threading; using System.Threading.Tasks; -using CoreBinaryDiffer = GeneralUpdate.Core.Differential.IBinaryDiffer; namespace GeneralUpdate.Differential.Abstractions; /// -/// Binary differential algorithm with both patch generation and application. -/// Extends (DirtyAsync) with CleanAsync. +/// Pluggable file-level binary patch algorithm. +/// Implement this to customize how individual files are patched (BSDIFF, HDiffPatch, etc.). /// -public interface IBinaryDiffer : CoreBinaryDiffer +public interface IBinaryDiffer { + /// Applies a binary patch: oldFile + patchFile → newFile. + Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default); + /// Generates a patch: oldFile vs newFile → patchFile. Task CleanAsync(string oldFilePath, string newFilePath, string patchFilePath, CancellationToken cancellationToken = default); diff --git a/src/c#/GeneralUpdate.Differential/DifferentialCore.cs b/src/c#/GeneralUpdate.Differential/DifferentialCore.cs deleted file mode 100644 index 2ffb8e96..00000000 --- a/src/c#/GeneralUpdate.Differential/DifferentialCore.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Threading.Tasks; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; - -namespace GeneralUpdate.Differential -{ - /// - /// Entry point for differential update operations. - /// - /// - /// Defaults to with Deflate compression - /// for smaller patches and faster application than the legacy BSDIFF algorithm. - /// - /// Pass a custom to change the diff algorithm, - /// or a custom / - /// for full control over execution flow. - /// - /// Legacy BSDIFF patches remain readable via . - /// - public static class DifferentialCore - { - /// - /// Generates a binary patch from (old version) - /// to (new version), writing to . - /// - /// - /// The binary differ to use. Defaults to . - /// When is non-null, the strategy owns the differ - /// and this parameter is ignored. - /// - /// - /// Full execution strategy override. Defaults to . - /// - public static Task Clean( - string sourcePath, - string targetPath, - string patchPath, - IBinaryDiffer? binaryDiffer = null, - ICleanStrategy? strategy = null) - { - var usedStrategy = strategy ?? new DefaultCleanStrategy( - matcher: null, - binaryDiffer: binaryDiffer); - return usedStrategy.ExecuteAsync(sourcePath, targetPath, patchPath); - } - - // Backward-compatible overload: 4th positional argument is a strategy. - public static Task Clean(string sourcePath, string targetPath, string patchPath, ICleanStrategy? strategy) - => Clean(sourcePath, targetPath, patchPath, binaryDiffer: null, strategy: strategy); - - /// - /// Applies a binary patch from to . - /// - /// - /// The binary differ to use. Defaults to . - /// When is non-null, the strategy owns the differ - /// and this parameter is ignored. - /// - /// - /// Full execution strategy override. Defaults to . - /// - public static Task Dirty( - string appPath, - string patchPath, - IBinaryDiffer? binaryDiffer = null, - IDirtyStrategy? strategy = null) - { - var usedStrategy = strategy ?? new DefaultDirtyStrategy( - matcher: null, - binaryDiffer: binaryDiffer); - return usedStrategy.ExecuteAsync(appPath, patchPath); - } - - // Backward-compatible overload: 3rd positional argument is a strategy. - public static Task Dirty(string appPath, string patchPath, IDirtyStrategy? strategy) - => Dirty(appPath, patchPath, binaryDiffer: null, strategy: strategy); - } -} diff --git a/src/c#/GeneralUpdate.Differential/GeneralUpdate.Differential.csproj b/src/c#/GeneralUpdate.Differential/GeneralUpdate.Differential.csproj index bd01266c..10d1a204 100644 --- a/src/c#/GeneralUpdate.Differential/GeneralUpdate.Differential.csproj +++ b/src/c#/GeneralUpdate.Differential/GeneralUpdate.Differential.csproj @@ -18,9 +18,6 @@ true netstandard2.0; - - - True diff --git a/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanMatcher.cs b/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanMatcher.cs deleted file mode 100644 index 86d690af..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanMatcher.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using GeneralUpdate.Core.FileSystem; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Default implementation of that preserves the - /// original behaviour of DifferentialCore.Clean. - /// - /// delegates to . - /// delegates to . - /// considers a new file matched to an old file when both share the - /// same name, both exist on disk, and they reside at the same relative path. - /// - /// - public class DefaultCleanMatcher : ICleanMatcher - { - private readonly StorageManager _storageManager = new StorageManager(); - - /// - public ComparisonResult Compare(string sourcePath, string targetPath) - => _storageManager.Compare(sourcePath, targetPath); - - /// - public IEnumerable? Except(string sourcePath, string targetPath) - => _storageManager.Except(sourcePath, targetPath); - - /// - public FileNode? Match(FileNode newFile, IEnumerable leftNodes) - { - var oldFile = leftNodes.FirstOrDefault(i => - string.Equals(i.Name, newFile.Name, System.StringComparison.OrdinalIgnoreCase) && - string.Equals(i.RelativePath, newFile.RelativePath, System.StringComparison.OrdinalIgnoreCase)); - if (oldFile is null) return null; - if (!File.Exists(oldFile.FullName)) return null; - if (!File.Exists(newFile.FullName)) return null; - return oldFile; - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanStrategy.cs b/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanStrategy.cs deleted file mode 100644 index f702c9b6..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/DefaultCleanStrategy.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Differential.Abstractions; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Default implementation of that preserves the original - /// behaviour of the Clean (patch-generation) phase. - /// - /// An optional can be supplied to customise the directory - /// comparison, deleted-file detection, or per-file matching logic without replacing the - /// entire execution flow. When no matcher is provided, - /// is used. - /// - /// - /// An optional can be supplied to customise the binary diff - /// algorithm. Defaults to with Deflate compression - /// for optimal patch size and speed. - /// - /// - public class DefaultCleanStrategy : ICleanStrategy - { - private const string PatchFormat = ".patch"; - private const string DeleteFilesName = "generalupdate_delete_files.json"; - - private readonly ICleanMatcher _matcher; - private readonly IBinaryDiffer _binaryDiffer; - - /// - /// Initialises a new instance using StreamingHdiffDiffer with Deflate compression by default. - /// - public DefaultCleanStrategy() - : this(null, null) - { - } - - /// - /// Initialises a new instance, optionally using a custom file-matching strategy - /// and/or a custom binary differ. - /// If no matcher is provided, is used. - /// If no binary differ is provided, (Deflate) is used. - /// - public DefaultCleanStrategy(ICleanMatcher? matcher = null, IBinaryDiffer? binaryDiffer = null) - { - _matcher = matcher ?? new DefaultCleanMatcher(); - _binaryDiffer = binaryDiffer ?? new Differ.StreamingHdiffDiffer(); - } - - /// - public async Task ExecuteAsync(string sourcePath, string targetPath, string patchPath) - { - var comparisonResult = _matcher.Compare(sourcePath, targetPath); - foreach (var file in comparisonResult.DifferentNodes) - { - var tempDir = GetTempDirectory(file, targetPath, patchPath); - var oldFile = _matcher.Match(file, comparisonResult.LeftNodes); - var newFile = file; - - if (oldFile is not null) - { - if (!StorageManager.HashEquals(oldFile.FullName, newFile.FullName)) - { - var tempPatchPath = Path.Combine(tempDir, $"{file.Name}{PatchFormat}"); - await _binaryDiffer.CleanAsync(oldFile.FullName, newFile.FullName, tempPatchPath); - } - } - else - { - File.Copy(newFile.FullName, Path.Combine(tempDir, Path.GetFileName(newFile.FullName)), true); - } - } - - var exceptFiles = _matcher.Except(sourcePath, targetPath)?.ToList(); - if (exceptFiles is not null && exceptFiles.Any()) - { - var path = Path.Combine(patchPath, DeleteFilesName); - StorageManager.CreateJson(path, exceptFiles, FileNodesJsonContext.Default.ListFileNode); - } - } - - private static string GetTempDirectory(FileNode file, string targetPath, string patchPath) - { - var tempPath = file.FullName.Replace(targetPath, "").Replace(Path.GetFileName(file.FullName), "").Trim(Path.DirectorySeparatorChar); - var tempDir = string.IsNullOrEmpty(tempPath) ? patchPath : Path.Combine(patchPath, tempPath); - Directory.CreateDirectory(tempDir); - return tempDir; - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyMatcher.cs b/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyMatcher.cs deleted file mode 100644 index b6ca75d5..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyMatcher.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Default implementation of that preserves the - /// original matching behaviour of DifferentialCore.Dirty: - /// a patch file matches an application file when the patch file's name - /// (without the .patch extension, case-insensitive) equals the application file's name, - /// and the patch file carries the .patch extension. - /// - public class DefaultDirtyMatcher : IDirtyMatcher - { - private const string PatchFormat = ".patch"; - - /// - public FileInfo? Match(FileInfo oldFile, IEnumerable patchFiles) - { - var findFile = patchFiles.FirstOrDefault(f => - { - var name = Path.GetFileNameWithoutExtension(f.Name); - // Strip only a trailing .patch extension (case-insensitive) - if (name.EndsWith(PatchFormat, System.StringComparison.OrdinalIgnoreCase)) - name = name.Substring(0, name.Length - PatchFormat.Length); - return name.Equals(oldFile.Name, System.StringComparison.OrdinalIgnoreCase); - }); - - if (findFile != null && - Path.GetExtension(findFile.FullName).Equals(PatchFormat, System.StringComparison.OrdinalIgnoreCase)) - return findFile; - - return null; - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyStrategy.cs b/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyStrategy.cs deleted file mode 100644 index 7bb6b755..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/DefaultDirtyStrategy.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core.HashAlgorithms; -using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Differential.Abstractions; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Default implementation of for the Dirty (patch-application) phase. - /// - /// - /// An optional can be supplied to customise how an - /// application file is matched to its corresponding patch file. - /// An optional can be supplied to customise the binary diff - /// algorithm. Defaults to with Deflate compression. - /// - /// File replacement strategy: after patching, the strategy performs an atomic - /// delete-and-replace of the original file with the patched output. - /// This ensures correctness regardless of the IBinaryDiffer implementation used. - /// - public class DefaultDirtyStrategy : IDirtyStrategy - { - private const string DeleteFilesName = "generalupdate_delete_files.json"; - - private readonly IDirtyMatcher _matcher; - private readonly IBinaryDiffer _binaryDiffer; - - /// - /// Initialises a new instance using StreamingHdiffDiffer with Deflate compression by default. - /// - public DefaultDirtyStrategy() - : this(null, null) - { - } - - /// - /// Initialises a new instance, optionally using a custom file-matching strategy - /// and/or a custom binary differ. - /// - public DefaultDirtyStrategy(IDirtyMatcher? matcher = null, IBinaryDiffer? binaryDiffer = null) - { - _matcher = matcher ?? new DefaultDirtyMatcher(); - _binaryDiffer = binaryDiffer ?? new Differ.StreamingHdiffDiffer(); - } - - /// - public async Task ExecuteAsync(string appPath, string patchPath) - { - if (!Directory.Exists(appPath) || !Directory.Exists(patchPath)) return; - - var skipDirectory = BlackListDefaults.DefaultSkipDirectories; - var patchFiles = StorageManager.GetAllFiles(patchPath, skipDirectory); - var oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory); - HandleDeleteList(patchFiles, oldFiles); - oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory); - foreach (var oldFile in oldFiles) - { - var findFile = _matcher.Match(oldFile, patchFiles); - if (findFile != null) - await ApplyPatch(oldFile.FullName, findFile.FullName); - } - - await CopyUnknownFiles(appPath, patchPath); - } - - private static void HandleDeleteList(IEnumerable patchFiles, IEnumerable oldFiles) - { - var json = patchFiles.FirstOrDefault(i => i.Name.Equals(DeleteFilesName)); - if (json == null) return; - - var deleteFiles = StorageManager.GetJson>(json.FullName, FileNodesJsonContext.Default.ListFileNode); - if (deleteFiles == null) return; - - var hashAlgorithm = new Sha256HashAlgorithm(); - var toDelete = oldFiles - .Where(old => deleteFiles.Any(del => del.Hash.SequenceEqual(hashAlgorithm.ComputeHash(old.FullName)))) - .ToList(); - - foreach (var file in toDelete) - { - if (!File.Exists(file.FullName)) continue; - File.SetAttributes(file.FullName, FileAttributes.Normal); - File.Delete(file.FullName); - } - } - - /// - /// Applies a patch to a single file, then atomically replaces the original with the patched version. - /// The IBinaryDiffer writes to a temp path; this method handles the replacement. - /// - private async Task ApplyPatch(string appFilePath, string patchFilePath) - { - if (!File.Exists(appFilePath) || !File.Exists(patchFilePath)) return; - - var tempPath = Path.Combine( - Path.GetDirectoryName(appFilePath)!, - $"{Path.GetRandomFileName()}_{Path.GetFileName(appFilePath)}"); - - // Let the differ produce the new file at tempPath - await _binaryDiffer.DirtyAsync(appFilePath, tempPath, patchFilePath); - - // Atomic replacement: if the differ didn't already replace the file, - // copy the patched output back over the original, then clean up. - if (File.Exists(tempPath) && !File.Exists(appFilePath)) - { - // Differ already replaced (e.g., BinaryHandler built-in behavior) - File.Move(tempPath, appFilePath); - } - else if (File.Exists(tempPath) && File.Exists(appFilePath)) - { - // Differ wrote to temp but didn't replace -- we do the replacement - File.SetAttributes(appFilePath, FileAttributes.Normal); - File.Delete(appFilePath); - File.Move(tempPath, appFilePath); - } - } - - private static async Task CopyUnknownFiles(string appPath, string patchPath) - { - await Task.Run(() => - { - var fileManager = new StorageManager(); - var comparisonResult = fileManager.Compare(appPath, patchPath); - foreach (var file in comparisonResult.DifferentNodes) - { - var extensionName = Path.GetExtension(file.FullName); - if (BlackListDefaults.DefaultBlackFormats.Contains(extensionName)) continue; - - var targetFileName = file.FullName.Replace(patchPath, "").TrimStart(Path.DirectorySeparatorChar); - var targetPath = Path.Combine(appPath, targetFileName); - var parentFolder = Directory.GetParent(targetPath); - if (parentFolder?.Exists == false) - parentFolder.Create(); - - File.Copy(file.FullName, targetPath, true); - } - - if (Directory.Exists(patchPath)) - StorageManager.DeleteDirectory(patchPath); - }); - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/ICleanMatcher.cs b/src/c#/GeneralUpdate.Differential/Matchers/ICleanMatcher.cs deleted file mode 100644 index 5c6495ad..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/ICleanMatcher.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using GeneralUpdate.Core.FileSystem; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Defines the complete matching strategy used during the Clean (diff-generation) phase. - /// Implementations are responsible for directory comparison, identifying deleted files, - /// and matching individual new files to their corresponding old files. - /// - public interface ICleanMatcher - { - /// - /// Compares the source and target directories and returns the set of changed files. - /// - /// The source (old-version) directory. - /// The target (new-version) directory. - ComparisonResult Compare(string sourcePath, string targetPath); - - /// - /// Returns the files that exist only in the source directory (i.e. files to be deleted). - /// - /// The source (old-version) directory. - /// The target (new-version) directory. - IEnumerable? Except(string sourcePath, string targetPath); - - /// - /// Attempts to find the corresponding old file node for - /// from the left-side (source) node collection. - /// - /// The new file node from the target directory. - /// All file nodes from the source directory. - /// - /// The matching old , or null if no match is found - /// (indicating the file is brand-new and should be copied directly). - /// - FileNode? Match(FileNode newFile, IEnumerable leftNodes); - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/ICleanStrategy.cs b/src/c#/GeneralUpdate.Differential/Matchers/ICleanStrategy.cs deleted file mode 100644 index e9eefe2d..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/ICleanStrategy.cs +++ /dev/null @@ -1,6 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace GeneralUpdate.Differential.Matchers -{ - /// Backward-compatible alias for . - public interface ICleanStrategy : GeneralUpdate.Core.Differential.ICleanStrategy { } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/IDirtyMatcher.cs b/src/c#/GeneralUpdate.Differential/Matchers/IDirtyMatcher.cs deleted file mode 100644 index 6289ffac..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/IDirtyMatcher.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace GeneralUpdate.Differential.Matchers -{ - /// - /// Defines the matching logic used during the Dirty (patch-application) phase to - /// find the patch file that corresponds to an existing application file. - /// - public interface IDirtyMatcher - { - /// - /// Attempts to find the patch file for from the - /// collection of available patch files. - /// - /// The existing application file to be patched. - /// All files available in the patch directory. - /// - /// The matching patch , or null if no patch exists - /// for the given file. - /// - FileInfo? Match(FileInfo oldFile, IEnumerable patchFiles); - } -} diff --git a/src/c#/GeneralUpdate.Differential/Matchers/IDirtyStrategy.cs b/src/c#/GeneralUpdate.Differential/Matchers/IDirtyStrategy.cs deleted file mode 100644 index e8d5ed9a..00000000 --- a/src/c#/GeneralUpdate.Differential/Matchers/IDirtyStrategy.cs +++ /dev/null @@ -1,6 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace GeneralUpdate.Differential.Matchers -{ - /// Backward-compatible alias for . - public interface IDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyStrategy { } -} diff --git a/src/c#/GeneralUpdate.Differential/Models/DiffProgress.cs b/src/c#/GeneralUpdate.Differential/Models/DiffProgress.cs deleted file mode 100644 index 1fc2fd1a..00000000 --- a/src/c#/GeneralUpdate.Differential/Models/DiffProgress.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace GeneralUpdate.Differential.Models -{ - /// - /// Progress information for differential pipeline operations. - /// - public readonly struct DiffProgress - { - /// Number of files processed so far. - public int Completed { get; } - - /// Total number of files to process. - public int Total { get; } - - /// Name of the file currently being processed. - public string? CurrentFile { get; } - - /// Percentage complete (0-100). - public double Percentage => Total > 0 ? (double)Completed / Total * 100.0 : 100.0; - - /// Whether the operation is complete. - public bool IsComplete => Completed >= Total; - - /// Error message if the current file failed (null on success). - public string? Error { get; } - - public DiffProgress(int completed, int total, string? currentFile, string? error = null) - { - Completed = completed; - Total = total; - CurrentFile = currentFile; - Error = error; - } - - /// Creates a completion marker. - public static DiffProgress Complete(int total) => new DiffProgress(total, total, null); - - public override string ToString() - => IsComplete - ? $"Complete: {Completed}/{Total} files" - : $"[{Percentage:F1}%] {Completed}/{Total} -- {CurrentFile ?? "..."}{(Error != null ? $" (failed: {Error})" : "")}"; - } -} diff --git a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipeline.cs b/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipeline.cs deleted file mode 100644 index 539c411b..00000000 --- a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipeline.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core.HashAlgorithms; -using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; - -namespace GeneralUpdate.Differential.Pipeline -{ - /// - /// Parallel differential pipeline with configurable parallelism, progress reporting, - /// pluggable matchers, and cancellation. - /// - /// - /// Use for fluent configuration. - /// Direct construction is also supported for scenarios where a builder is unnecessary. - /// - public class DiffPipeline - { - private readonly DiffPipelineOptions _options; - private readonly IBinaryDiffer _binaryDiffer; - private readonly ICleanMatcher _cleanMatcher; - private readonly IDirtyMatcher _dirtyMatcher; - private readonly IProgress? _progress; - - private const string PatchExtension = ".patch"; - private const string DeleteListFileName = "generalupdate_delete_files.json"; - - /// - /// Initialises a new pipeline with default options and matchers. - /// - public DiffPipeline() - : this(new DiffPipelineOptions(), new Differ.StreamingHdiffDiffer(), null, null, null) - { - } - - /// - /// Initialises a new pipeline with the specified options. - /// - public DiffPipeline(DiffPipelineOptions options) - : this(options, new Differ.StreamingHdiffDiffer(), null, null, null) - { - } - - /// - /// Initialises a new pipeline with full configuration. - /// - /// Pipeline options. Must not be null. - /// Binary differ. Must not be null. - /// Clean-phase file matcher. Defaults to . - /// Dirty-phase file matcher. Defaults to . - /// Optional progress reporter. - public DiffPipeline( - DiffPipelineOptions options, - IBinaryDiffer binaryDiffer, - ICleanMatcher? cleanMatcher = null, - IDirtyMatcher? dirtyMatcher = null, - IProgress? progress = null) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _binaryDiffer = binaryDiffer ?? throw new ArgumentNullException(nameof(binaryDiffer)); - _cleanMatcher = cleanMatcher ?? new DefaultCleanMatcher(); - _dirtyMatcher = dirtyMatcher ?? new DefaultDirtyMatcher(); - _progress = progress; - } - - /// - /// Initialises a new pipeline (backward-compatible constructor, preserved for binary compatibility). - /// - public DiffPipeline(DiffPipelineOptions options, IBinaryDiffer binaryDiffer, IProgress? progress = null) - : this(options, binaryDiffer, null, null, progress) - { - } - - /// - /// Compares source and target directories using the configured clean matcher, - /// generating patch files in parallel. - /// - public async Task CleanAsync( - string sourcePath, - string targetPath, - string patchPath, - IProgress? progress = null, - CancellationToken cancellationToken = default) - { - var reporter = progress ?? _progress; - ValidateDirectories(sourcePath, targetPath, patchPath); - - var comparisonResult = _cleanMatcher.Compare(sourcePath, targetPath); - var differentFiles = comparisonResult.DifferentNodes.ToList(); - var leftNodes = comparisonResult.LeftNodes.ToList(); - - int total = differentFiles.Count; - if (total == 0) - { - reporter?.Report(DiffProgress.Complete(0)); - return; - } - - int completed = 0; - var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism); - - var tasks = differentFiles.Select(file => Task.Run(async () => - { - await semaphore.WaitAsync(cancellationToken); - try - { - cancellationToken.ThrowIfCancellationRequested(); - - var tempDir = GetTempDirectory(file, targetPath, patchPath); - var oldFile = _cleanMatcher.Match(file, leftNodes); - - if (oldFile != null) - { - if (!StorageManager.HashEquals(oldFile.FullName, file.FullName)) - { - var tempPatchPath = Path.Combine(tempDir, $"{file.Name}{PatchExtension}"); - await _binaryDiffer.CleanAsync(oldFile.FullName, file.FullName, tempPatchPath, cancellationToken); - } - } - else - { - File.Copy(file.FullName, Path.Combine(tempDir, Path.GetFileName(file.FullName)), true); - } - - int done = Interlocked.Increment(ref completed); - reporter?.Report(new DiffProgress(done, total, file.Name)); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (!_options.StopOnFirstError) - { - int done = Interlocked.Increment(ref completed); - reporter?.Report(new DiffProgress(done, total, file.Name, ex.Message)); - } - finally - { - semaphore.Release(); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - - var exceptFiles = _cleanMatcher.Except(sourcePath, targetPath)?.ToList(); - if (exceptFiles is { Count: > 0 }) - { - var deletePath = Path.Combine(patchPath, DeleteListFileName); - StorageManager.CreateJson(deletePath, exceptFiles, FileNodesJsonContext.Default.ListFileNode); - } - - reporter?.Report(DiffProgress.Complete(total)); - } - - /// - /// Applies patches from patchPath to appPath in parallel, - /// using the configured dirty matcher. - /// - public async Task DirtyAsync( - string appPath, - string patchPath, - IProgress? progress = null, - CancellationToken cancellationToken = default) - { - var reporter = progress ?? _progress; - if (!Directory.Exists(appPath) || !Directory.Exists(patchPath)) return; - - var skipDirectory = BlackListDefaults.DefaultSkipDirectories; - var patchFiles = StorageManager.GetAllFiles(patchPath, skipDirectory).ToList(); - var oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory).ToList(); - - HandleDeleteList(patchFiles, oldFiles); - oldFiles = StorageManager.GetAllFiles(appPath, skipDirectory).ToList(); - - int total = oldFiles.Count; - if (total == 0) - { - reporter?.Report(DiffProgress.Complete(0)); - await CopyUnknownFiles(appPath, patchPath); - return; - } - - int completed = 0; - var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism); - - // Match old files to patches using the pluggable dirty matcher - var matchedPairs = new List<(FileInfo OldFile, FileInfo PatchFile)>(); - foreach (var oldFile in oldFiles) - { - var patchFile = _dirtyMatcher.Match(oldFile, patchFiles); - if (patchFile != null) - matchedPairs.Add((oldFile, patchFile)); - } - - var tasks = matchedPairs.Select(pair => Task.Run(async () => - { - await semaphore.WaitAsync(cancellationToken); - try - { - cancellationToken.ThrowIfCancellationRequested(); - await ApplyPatch(pair.OldFile.FullName, pair.PatchFile.FullName, cancellationToken); - - int done = Interlocked.Increment(ref completed); - reporter?.Report(new DiffProgress(done, total, pair.OldFile.Name)); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (!_options.StopOnFirstError) - { - int done = Interlocked.Increment(ref completed); - reporter?.Report(new DiffProgress(done, total, pair.OldFile.Name, ex.Message)); - } - finally - { - semaphore.Release(); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - reporter?.Report(DiffProgress.Complete(total)); - await CopyUnknownFiles(appPath, patchPath); - } - - private async Task ApplyPatch(string appFilePath, string patchFilePath, CancellationToken ct) - { - if (!File.Exists(appFilePath) || !File.Exists(patchFilePath)) return; - - var tempPath = Path.Combine( - Path.GetDirectoryName(appFilePath)!, - $"{Path.GetRandomFileName()}_{Path.GetFileName(appFilePath)}"); - - await _binaryDiffer.DirtyAsync(appFilePath, tempPath, patchFilePath, ct); - - // Some differ implementations (e.g., BinaryHandler) perform atomic replacement - // internally: they delete oldPath and copy tempPath back to oldPath, then delete tempPath. - // We only handle the replacement ourselves when the differ left tempPath intact. - if (File.Exists(tempPath)) - { - if (File.Exists(appFilePath)) - { - File.SetAttributes(appFilePath, FileAttributes.Normal); - File.Delete(appFilePath); - } - File.Move(tempPath, appFilePath); - } - } - - private static void HandleDeleteList(IEnumerable patchFiles, IEnumerable oldFiles) - { - var json = patchFiles.FirstOrDefault(i => i.Name.Equals(DeleteListFileName, StringComparison.OrdinalIgnoreCase)); - if (json == null) return; - - var deleteFiles = StorageManager.GetJson>(json.FullName, FileNodesJsonContext.Default.ListFileNode); - if (deleteFiles == null) return; - - var hashAlgorithm = new Sha256HashAlgorithm(); - var toDelete = oldFiles - .Where(old => deleteFiles.Any(del => del.Hash.SequenceEqual(hashAlgorithm.ComputeHash(old.FullName)))) - .ToList(); - - foreach (var file in toDelete) - { - if (!File.Exists(file.FullName)) continue; - File.SetAttributes(file.FullName, FileAttributes.Normal); - File.Delete(file.FullName); - } - } - - private static Task CopyUnknownFiles(string appPath, string patchPath) - { - return Task.Run(() => - { - var fileManager = new StorageManager(); - var comparisonResult = fileManager.Compare(appPath, patchPath); - foreach (var file in comparisonResult.DifferentNodes) - { - var extensionName = Path.GetExtension(file.FullName); - if (BlackListDefaults.DefaultBlackFormats.Contains(extensionName)) continue; - - var targetFileName = file.FullName.Replace(patchPath, "").TrimStart(Path.DirectorySeparatorChar); - var targetPath = Path.Combine(appPath, targetFileName); - var parentFolder = Directory.GetParent(targetPath); - if (parentFolder?.Exists == false) - parentFolder.Create(); - - File.Copy(file.FullName, targetPath, true); - } - - if (Directory.Exists(patchPath)) - StorageManager.DeleteDirectory(patchPath); - }); - } - - private static string GetTempDirectory(FileNode file, string targetPath, string patchPath) - { - var tempPath = file.FullName - .Replace(targetPath, "") - .Replace(Path.GetFileName(file.FullName), "") - .Trim(Path.DirectorySeparatorChar); - var tempDir = string.IsNullOrEmpty(tempPath) ? patchPath : Path.Combine(patchPath, tempPath); - Directory.CreateDirectory(tempDir); - return tempDir; - } - - private static void ValidateDirectories(string sourcePath, string targetPath, string patchPath) - { - if (string.IsNullOrWhiteSpace(sourcePath)) throw new ArgumentNullException(nameof(sourcePath)); - if (string.IsNullOrWhiteSpace(targetPath)) throw new ArgumentNullException(nameof(targetPath)); - if (string.IsNullOrWhiteSpace(patchPath)) throw new ArgumentNullException(nameof(patchPath)); - - if (!Directory.Exists(sourcePath)) - throw new DirectoryNotFoundException($"Source directory not found: {sourcePath}"); - if (!Directory.Exists(targetPath)) - throw new DirectoryNotFoundException($"Target directory not found: {targetPath}"); - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineBuilder.cs b/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineBuilder.cs deleted file mode 100644 index a13712c7..00000000 --- a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineBuilder.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; - -namespace GeneralUpdate.Differential.Pipeline -{ - /// - /// Fluent builder for instances. - /// - /// - /// Usage: - /// - /// var pipeline = new DiffPipelineBuilder() - /// .UseDiffer(new StreamingHdiffDiffer()) - /// .UseCleanMatcher(new DefaultCleanMatcher()) - /// .WithParallelism(Environment.ProcessorCount) - /// .Build(); - /// await pipeline.CleanAsync(src, tgt, patch); - /// - /// - public class DiffPipelineBuilder - { - private IBinaryDiffer? _differ; - private ICleanMatcher? _cleanMatcher; - private IDirtyMatcher? _dirtyMatcher; - private int _maxParallelism = Environment.ProcessorCount; - private bool _stopOnFirstError; - private IProgress? _progress; - - /// - /// Sets the binary differ to use. Defaults to if not set. - /// - public DiffPipelineBuilder UseDiffer(IBinaryDiffer differ) - { - _differ = differ ?? throw new ArgumentNullException(nameof(differ)); - return this; - } - - /// - /// Sets a custom clean matcher for directory comparison and file matching during patch generation. - /// Defaults to if not set. - /// - public DiffPipelineBuilder UseCleanMatcher(ICleanMatcher matcher) - { - _cleanMatcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); - return this; - } - - /// - /// Sets a custom dirty matcher for matching patch files to application files during patch application. - /// Defaults to if not set. - /// - public DiffPipelineBuilder UseDirtyMatcher(IDirtyMatcher matcher) - { - _dirtyMatcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); - return this; - } - - /// - /// Sets the maximum degree of parallelism for file processing. - /// Default: . - /// - public DiffPipelineBuilder WithParallelism(int maxDegreeOfParallelism) - { - if (maxDegreeOfParallelism < 1) - throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); - _maxParallelism = maxDegreeOfParallelism; - return this; - } - - /// - /// Sets whether to stop processing on the first file error. - /// Default: false (continue processing, report errors via progress). - /// - public DiffPipelineBuilder WithStopOnFirstError(bool stopOnFirstError = true) - { - _stopOnFirstError = stopOnFirstError; - return this; - } - - /// - /// Attaches a progress reporter for real-time file-level status updates. - /// - public DiffPipelineBuilder WithProgress(IProgress progress) - { - _progress = progress ?? throw new ArgumentNullException(nameof(progress)); - return this; - } - - /// - /// Builds the configured . - /// - public DiffPipeline Build() - { - var options = new DiffPipelineOptions - { - MaxDegreeOfParallelism = _maxParallelism, - StopOnFirstError = _stopOnFirstError - }; - - var differ = _differ ?? new Differ.StreamingHdiffDiffer(); - return new DiffPipeline(options, differ, _cleanMatcher, _dirtyMatcher, _progress); - } - } -} diff --git a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineOptions.cs b/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineOptions.cs deleted file mode 100644 index 3d85c17e..00000000 --- a/src/c#/GeneralUpdate.Differential/Pipeline/DiffPipelineOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace GeneralUpdate.Differential.Pipeline -{ - /// - /// Configuration options for . - /// - public sealed class DiffPipelineOptions - { - /// - /// Gets or sets the maximum number of files to process concurrently. - /// Default: . - /// Set to 1 for sequential processing. - /// - public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; - - /// - /// Gets or sets whether to stop processing on first error. - /// Default: false (continue processing other files, report errors via progress). - /// - public bool StopOnFirstError { get; set; } = false; - - /// - /// Gets or sets whether to delete the patch directory after successful apply. - /// Default: true. - /// - public bool DeletePatchAfterApply { get; set; } = true; - } -} diff --git a/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs b/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs index ef8b8a59..a1ef2712 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs @@ -1,5 +1,5 @@ using System.Runtime.InteropServices; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Core.Execution; diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs b/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs index 21c72e24..766899d3 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using System.Text; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; namespace GeneralUpdate.Drivelution.Core.Execution; diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs index 4853c642..b7e5d88f 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs index f20e8fc8..04ff4dbd 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Models; using GeneralUpdate.Drivelution.Core.Utilities; diff --git a/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs index 1b848880..3d65ff11 100644 --- a/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs @@ -1,4 +1,4 @@ -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Models; diff --git a/src/c#/GeneralUpdate.Drivelution/GeneralUpdate.Drivelution.csproj b/src/c#/GeneralUpdate.Drivelution/GeneralUpdate.Drivelution.csproj index 9c59a340..e2d3ac79 100644 --- a/src/c#/GeneralUpdate.Drivelution/GeneralUpdate.Drivelution.csproj +++ b/src/c#/GeneralUpdate.Drivelution/GeneralUpdate.Drivelution.csproj @@ -33,11 +33,6 @@ - - - - - diff --git a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverBackup.cs b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverBackup.cs index 4cc2901c..98c380a8 100644 --- a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverBackup.cs +++ b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverBackup.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverValidator.cs b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverValidator.cs index 1e1a6416..f6e1c1d9 100644 --- a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverValidator.cs +++ b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxDriverValidator.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Models; using GeneralUpdate.Drivelution.Core.Utilities; diff --git a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxGeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxGeneralDrivelution.cs index af73c40a..9d18f964 100644 --- a/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxGeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/Linux/Implementation/LinuxGeneralDrivelution.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Drivelution/MacOS/Implementation/MacOsGeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/MacOS/Implementation/MacOsGeneralDrivelution.cs index e8c4a6a5..def27c6d 100644 --- a/src/c#/GeneralUpdate.Drivelution/MacOS/Implementation/MacOsGeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/MacOS/Implementation/MacOsGeneralDrivelution.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Drivelution/Tracer/GeneralTracer.cs b/src/c#/GeneralUpdate.Drivelution/Tracer/GeneralTracer.cs new file mode 100644 index 00000000..def1ad9f --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Tracer/GeneralTracer.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Drivelution; + +public static class GeneralTracer +{ + private static readonly object _lockObj = new(); + private static bool _isTracingEnabled; + private static string _currentLogDate; + private static TextTraceListener _fileListener; + + static GeneralTracer() + { + Trace.Listeners.Clear(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Trace.Listeners.Add(new WindowsOutputDebugListener()); + } + + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out) { Name = "ConsoleListener" }); + + InitializeFileListener(); + + if (Debugger.IsAttached) + Trace.Listeners.Add(new DefaultTraceListener()); + + Trace.AutoFlush = true; + _isTracingEnabled = true; + } + + private static void InitializeFileListener() + { + //Ensure that log files are rotated on a daily basis + var today = DateTime.Now.ToString("yyyy-MM-dd"); + if (today == _currentLogDate && _fileListener != null) + return; + + if (_fileListener != null) + { + Trace.Listeners.Remove(_fileListener); + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + } + + var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + Directory.CreateDirectory(logDir); + + var logFileName = Path.Combine(logDir, $"generalupdate-trace {today}.log"); + _fileListener = new TextTraceListener(logFileName); + + Trace.Listeners.Add(_fileListener); + _currentLogDate = today; + } + + public static void Debug(string message) => WriteTraceMessage(TraceLevel.Verbose, message); + + public static void Info(string message) => WriteTraceMessage(TraceLevel.Info, message); + + public static void Warn(string message) => WriteTraceMessage(TraceLevel.Warning, message); + + public static void Error(string message) => WriteTraceMessage(TraceLevel.Error, message); + + public static void Fatal(string message) => WriteTraceMessage(TraceLevel.Off, message); + + public static void Error(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Error, fullMessage); + } + + public static void Fatal(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Off, fullMessage); + } + + public static void SetTracingEnabled(bool enabled) + { + lock (_lockObj) + { + Trace.AutoFlush = enabled; + _isTracingEnabled = enabled; + foreach (TraceListener listener in Trace.Listeners) + { + listener.Filter = enabled ? null : new EventTypeFilter(SourceLevels.Off); + } + } + } + + public static bool IsTracingEnabled() + { + lock (_lockObj) + { + return _isTracingEnabled; + } + } + + private static void WriteTraceMessage(TraceLevel level, string message) + { + if(!IsTracingEnabled()) + return; + + InitializeFileListener(); + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var levelName = GetLevelName(level); + var fullMessage = string.Empty; + + try + { + var stackFrame = new StackFrame(2, true); + var method = stackFrame.GetMethod(); + var className = method.DeclaringType?.Name ?? "UnknownType"; + var methodName = method.Name; + var lineNumber = stackFrame.GetFileLineNumber(); + var lineInfo = lineNumber > 0 ? $"Line {lineNumber}" : "Line N/A (Line numbers may not be displayed in Release mode)"; + fullMessage = $"[{timestamp}] [{levelName}] {className}.{methodName} ({lineInfo}): {message}"; + } + catch + { + fullMessage = $"[{timestamp}] [{levelName}] : {message}"; + } + + Trace.WriteLine(fullMessage); + } + + private static string GetLevelName(TraceLevel level) => level switch + { + TraceLevel.Verbose => "DEBUG", + TraceLevel.Info => "INFO", + TraceLevel.Warning => "WARN", + TraceLevel.Error => "ERROR", + TraceLevel.Off => "FATAL", + _ => "UNKNOWN" + }; + + public static void Dispose() + { + try + { + lock (_lockObj) + { + if (_fileListener is not null) + { + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + _fileListener = null; + } + + Trace.Listeners.Clear(); + } + } + catch + { + // ignored + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Drivelution/Tracer/TextTraceListener.cs b/src/c#/GeneralUpdate.Drivelution/Tracer/TextTraceListener.cs new file mode 100644 index 00000000..bd513e72 --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Tracer/TextTraceListener.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Concurrent; +using System.Threading; + +public class TextTraceListener : TraceListener, IDisposable +{ + private readonly string _filePath; + private readonly BlockingCollection _messageQueue; + private readonly Thread _loggingThread; + private volatile bool _isDisposed; + + public TextTraceListener(string filePath) + { + _filePath = filePath; + _messageQueue = new BlockingCollection(); + _loggingThread = new Thread(ProcessQueue); + _loggingThread.IsBackground = true; + _loggingThread.Start(); + } + + public override void Write(string? message) + { + QueueMessage(message); + } + + public override void WriteLine(string? message) + { + QueueMessage(message + Environment.NewLine); + } + + private void QueueMessage(string? message) + { + if (!_isDisposed && message != null) + { + _messageQueue.Add(message); + } + } + + private void ProcessQueue() + { + foreach (var message in _messageQueue.GetConsumingEnumerable()) + { + using (var fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) + using (var writer = new StreamWriter(fileStream)) + { + writer.Write(message); + } + } + } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + _messageQueue.CompleteAdding(); + _loggingThread.Join(); + _messageQueue.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Drivelution/Tracer/WindowsOutputDebugListener.cs b/src/c#/GeneralUpdate.Drivelution/Tracer/WindowsOutputDebugListener.cs new file mode 100644 index 00000000..79e2eb61 --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Tracer/WindowsOutputDebugListener.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Drivelution; + +public class WindowsOutputDebugListener : TraceListener +{ + /// + /// Does not affect .NET AOT compilation and runtime on the Windows platform, provided that the following conditions are met: + /// The target platform is restricted to Windows (due to the dependency on kernel32.dll); + /// The function declaration is static (not dynamically generated). + /// This syntax is safe in an AOT environment and can properly work with Dbgview.exe to capture logs. + /// + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern void OutputDebugString(string lpOutputString); + + public override void Write(string message) + { + if (!string.IsNullOrEmpty(message)) + { + OutputDebugString(message); + } + } + + public override void WriteLine(string message) + { + Write($"{message}{Environment.NewLine}"); + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverBackup.cs b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverBackup.cs index 530ad80f..2b114cb5 100644 --- a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverBackup.cs +++ b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverBackup.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverValidator.cs b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverValidator.cs index 9997a972..d836f1d7 100644 --- a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverValidator.cs +++ b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsDriverValidator.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Exceptions; using GeneralUpdate.Drivelution.Abstractions.Models; diff --git a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs index 59693570..486f29c7 100644 --- a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs @@ -1,5 +1,5 @@ using System.Runtime.Versioning; -using GeneralUpdate.Core; +using GeneralUpdate.Drivelution; using GeneralUpdate.Drivelution.Abstractions; using GeneralUpdate.Drivelution.Abstractions.Configuration; using GeneralUpdate.Drivelution.Abstractions.Exceptions; diff --git a/src/c#/GeneralUpdate.Extension/Core/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/Core/GeneralExtensionHost.cs index c1f0246f..8a9a7c01 100644 --- a/src/c#/GeneralUpdate.Extension/Core/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/Core/GeneralExtensionHost.cs @@ -7,7 +7,7 @@ using GeneralUpdate.Extension.Dependencies; using GeneralUpdate.Extension.Communication; using GeneralUpdate.Extension.Common.Models; -using GeneralUpdate.Core; +using GeneralUpdate.Extension; using Newtonsoft.Json; using System; diff --git a/src/c#/GeneralUpdate.Extension/Download/DownloadQueueManager.cs b/src/c#/GeneralUpdate.Extension/Download/DownloadQueueManager.cs index 0ebc8068..ee9195eb 100644 --- a/src/c#/GeneralUpdate.Extension/Download/DownloadQueueManager.cs +++ b/src/c#/GeneralUpdate.Extension/Download/DownloadQueueManager.cs @@ -7,7 +7,7 @@ using GeneralUpdate.Extension.Dependencies; using GeneralUpdate.Extension.Communication; using GeneralUpdate.Extension.Common.Models; -using GeneralUpdate.Core; +using GeneralUpdate.Extension; using System; using System.Collections.Generic; diff --git a/src/c#/GeneralUpdate.Extension/GeneralUpdate.Extension.csproj b/src/c#/GeneralUpdate.Extension/GeneralUpdate.Extension.csproj index a2677d11..681edcdd 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralUpdate.Extension.csproj +++ b/src/c#/GeneralUpdate.Extension/GeneralUpdate.Extension.csproj @@ -22,10 +22,6 @@ true - - - - diff --git a/src/c#/GeneralUpdate.Extension/Tracer/GeneralTracer.cs b/src/c#/GeneralUpdate.Extension/Tracer/GeneralTracer.cs new file mode 100644 index 00000000..4371baea --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/Tracer/GeneralTracer.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Extension; + +public static class GeneralTracer +{ + private static readonly object _lockObj = new(); + private static bool _isTracingEnabled; + private static string _currentLogDate; + private static TextTraceListener _fileListener; + + static GeneralTracer() + { + Trace.Listeners.Clear(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Trace.Listeners.Add(new WindowsOutputDebugListener()); + } + + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out) { Name = "ConsoleListener" }); + + InitializeFileListener(); + + if (Debugger.IsAttached) + Trace.Listeners.Add(new DefaultTraceListener()); + + Trace.AutoFlush = true; + _isTracingEnabled = true; + } + + private static void InitializeFileListener() + { + //Ensure that log files are rotated on a daily basis + var today = DateTime.Now.ToString("yyyy-MM-dd"); + if (today == _currentLogDate && _fileListener != null) + return; + + if (_fileListener != null) + { + Trace.Listeners.Remove(_fileListener); + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + } + + var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + Directory.CreateDirectory(logDir); + + var logFileName = Path.Combine(logDir, $"generalupdate-trace {today}.log"); + _fileListener = new TextTraceListener(logFileName); + + Trace.Listeners.Add(_fileListener); + _currentLogDate = today; + } + + public static void Debug(string message) => WriteTraceMessage(TraceLevel.Verbose, message); + + public static void Info(string message) => WriteTraceMessage(TraceLevel.Info, message); + + public static void Warn(string message) => WriteTraceMessage(TraceLevel.Warning, message); + + public static void Error(string message) => WriteTraceMessage(TraceLevel.Error, message); + + public static void Fatal(string message) => WriteTraceMessage(TraceLevel.Off, message); + + public static void Error(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Error, fullMessage); + } + + public static void Fatal(string message, Exception ex) + { + var fullMessage = $"{message}{Environment.NewLine} Exception Details: {ex}"; + WriteTraceMessage(TraceLevel.Off, fullMessage); + } + + public static void SetTracingEnabled(bool enabled) + { + lock (_lockObj) + { + Trace.AutoFlush = enabled; + _isTracingEnabled = enabled; + foreach (TraceListener listener in Trace.Listeners) + { + listener.Filter = enabled ? null : new EventTypeFilter(SourceLevels.Off); + } + } + } + + public static bool IsTracingEnabled() + { + lock (_lockObj) + { + return _isTracingEnabled; + } + } + + private static void WriteTraceMessage(TraceLevel level, string message) + { + if(!IsTracingEnabled()) + return; + + InitializeFileListener(); + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var levelName = GetLevelName(level); + var fullMessage = string.Empty; + + try + { + var stackFrame = new StackFrame(2, true); + var method = stackFrame.GetMethod(); + var className = method.DeclaringType?.Name ?? "UnknownType"; + var methodName = method.Name; + var lineNumber = stackFrame.GetFileLineNumber(); + var lineInfo = lineNumber > 0 ? $"Line {lineNumber}" : "Line N/A (Line numbers may not be displayed in Release mode)"; + fullMessage = $"[{timestamp}] [{levelName}] {className}.{methodName} ({lineInfo}): {message}"; + } + catch + { + fullMessage = $"[{timestamp}] [{levelName}] : {message}"; + } + + Trace.WriteLine(fullMessage); + } + + private static string GetLevelName(TraceLevel level) => level switch + { + TraceLevel.Verbose => "DEBUG", + TraceLevel.Info => "INFO", + TraceLevel.Warning => "WARN", + TraceLevel.Error => "ERROR", + TraceLevel.Off => "FATAL", + _ => "UNKNOWN" + }; + + public static void Dispose() + { + try + { + lock (_lockObj) + { + if (_fileListener is not null) + { + _fileListener.Flush(); + _fileListener.Close(); + _fileListener.Dispose(); + _fileListener = null; + } + + Trace.Listeners.Clear(); + } + } + catch + { + // ignored + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Extension/Tracer/TextTraceListener.cs b/src/c#/GeneralUpdate.Extension/Tracer/TextTraceListener.cs new file mode 100644 index 00000000..bd513e72 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/Tracer/TextTraceListener.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Concurrent; +using System.Threading; + +public class TextTraceListener : TraceListener, IDisposable +{ + private readonly string _filePath; + private readonly BlockingCollection _messageQueue; + private readonly Thread _loggingThread; + private volatile bool _isDisposed; + + public TextTraceListener(string filePath) + { + _filePath = filePath; + _messageQueue = new BlockingCollection(); + _loggingThread = new Thread(ProcessQueue); + _loggingThread.IsBackground = true; + _loggingThread.Start(); + } + + public override void Write(string? message) + { + QueueMessage(message); + } + + public override void WriteLine(string? message) + { + QueueMessage(message + Environment.NewLine); + } + + private void QueueMessage(string? message) + { + if (!_isDisposed && message != null) + { + _messageQueue.Add(message); + } + } + + private void ProcessQueue() + { + foreach (var message in _messageQueue.GetConsumingEnumerable()) + { + using (var fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) + using (var writer = new StreamWriter(fileStream)) + { + writer.Write(message); + } + } + } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + _messageQueue.CompleteAdding(); + _loggingThread.Join(); + _messageQueue.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Extension/Tracer/WindowsOutputDebugListener.cs b/src/c#/GeneralUpdate.Extension/Tracer/WindowsOutputDebugListener.cs new file mode 100644 index 00000000..9f0784e9 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/Tracer/WindowsOutputDebugListener.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace GeneralUpdate.Extension; + +public class WindowsOutputDebugListener : TraceListener +{ + /// + /// Does not affect .NET AOT compilation and runtime on the Windows platform, provided that the following conditions are met: + /// The target platform is restricted to Windows (due to the dependency on kernel32.dll); + /// The function declaration is static (not dynamically generated). + /// This syntax is safe in an AOT environment and can properly work with Dbgview.exe to capture logs. + /// + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern void OutputDebugString(string lpOutputString); + + public override void Write(string message) + { + if (!string.IsNullOrEmpty(message)) + { + OutputDebugString(message); + } + } + + public override void WriteLine(string message) + { + Write($"{message}{Environment.NewLine}"); + } +} \ No newline at end of file diff --git a/tests/DifferentialTest/DifferentialTest.csproj b/tests/DifferentialTest/DifferentialTest.csproj index 61bef53e..9b8cd467 100644 --- a/tests/DifferentialTest/DifferentialTest.csproj +++ b/tests/DifferentialTest/DifferentialTest.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/DifferentialTest/DifferentialUpgradeIntegrationTests.cs b/tests/DifferentialTest/DifferentialUpgradeIntegrationTests.cs index 1e79de80..450f7eb9 100644 --- a/tests/DifferentialTest/DifferentialUpgradeIntegrationTests.cs +++ b/tests/DifferentialTest/DifferentialUpgradeIntegrationTests.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Differential; +using GeneralUpdate.Core; using GeneralUpdate.Differential.Abstractions; using GeneralUpdate.Differential.Binary; -using GeneralUpdate.Differential.Matchers; -using GeneralUpdate.Differential.Models; -using GeneralUpdate.Differential.Pipeline; +using GeneralUpdate.Core.Differential; +using GeneralUpdate.Core.Models; +using GeneralUpdate.Core.Pipeline; using Xunit; using Xunit.Abstractions; diff --git a/tests/DifferentialTest/Matchers/MatcherTests.cs b/tests/DifferentialTest/Matchers/MatcherTests.cs index b2c4f1b5..cf1a9fa8 100644 --- a/tests/DifferentialTest/Matchers/MatcherTests.cs +++ b/tests/DifferentialTest/Matchers/MatcherTests.cs @@ -3,8 +3,8 @@ using System.IO; using System.Threading.Tasks; using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Differential; -using GeneralUpdate.Differential.Matchers; +using GeneralUpdate.Core; +using GeneralUpdate.Core.Differential; using Xunit; namespace DifferentialTest.Matchers @@ -32,7 +32,7 @@ public void Dispose() } } - #region DefaultCleanMatcher �Compare and Except + #region DefaultCleanMatcher �?Compare and Except [Fact] public void DefaultCleanMatcher_Compare_ReturnsDifferentNodes_WhenFilesChange() @@ -324,7 +324,7 @@ public async Task Clean_WithCustomStrategy_UsesCustomMatchingLogic() File.WriteAllText(Path.Combine(targetDir, "file.txt"), "new"); // Inject a DefaultCleanStrategy that wraps a matcher which always treats every - // file as new (Match always returns null �file is copied directly, not patched). + // file as new (Match always returns null �?file is copied directly, not patched). var strategy = new DefaultCleanStrategy(new AlwaysNewFileMatcher()); // Act @@ -350,7 +350,7 @@ public async Task Dirty_WithCustomStrategy_UsesCustomMatchingLogic() // Inject a DefaultDirtyStrategy that wraps a matcher which never finds a patch file. var strategy = new DefaultDirtyStrategy(new NeverMatchDirtyMatcher()); - // Act �should not throw even though a patch file exists, because the custom matcher skips it + // Act �?should not throw even though a patch file exists, because the custom matcher skips it await DifferentialCore.Dirty(appDir, patchDir, strategy); // Assert: original file is unchanged diff --git a/tests/DifferentialTest/Pipeline/DiffPipelineTests.cs b/tests/DifferentialTest/Pipeline/DiffPipelineTests.cs index 5b9285d4..c0219c8d 100644 --- a/tests/DifferentialTest/Pipeline/DiffPipelineTests.cs +++ b/tests/DifferentialTest/Pipeline/DiffPipelineTests.cs @@ -4,8 +4,8 @@ using System.Threading.Tasks; using GeneralUpdate.Differential.Abstractions; using GeneralUpdate.Differential.Binary; -using GeneralUpdate.Differential.Pipeline; -using GeneralUpdate.Differential.Models; +using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Core.Models; using Xunit; namespace DifferentialTest.Pipeline From c24e25c6f1df88f14870ee6b7fe8a0afe985809c Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 18:20:39 +0800 Subject: [PATCH 2/2] fix: address Copilot review feedback - Remove duplicate using directives in Bowl.cs - Fix garbled characters in MatcherTests.cs region comment - Remove unused using directives in DiffPipelineBuilder.cs Co-Authored-By: Claude Opus 4.7 --- src/c#/GeneralUpdate.Bowl/Bowl.cs | 4 ---- src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs | 2 -- tests/DifferentialTest/Matchers/MatcherTests.cs | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/c#/GeneralUpdate.Bowl/Bowl.cs b/src/c#/GeneralUpdate.Bowl/Bowl.cs index 2270e32e..d71d91a5 100644 --- a/src/c#/GeneralUpdate.Bowl/Bowl.cs +++ b/src/c#/GeneralUpdate.Bowl/Bowl.cs @@ -10,10 +10,6 @@ using GeneralUpdate.Bowl.Configuration; using GeneralUpdate.Bowl.Ipc; -using GeneralUpdate.Bowl; -using GeneralUpdate.Bowl.Configuration; -using GeneralUpdate.Bowl.Ipc; - namespace GeneralUpdate.Bowl; /// diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs index 9b876487..814155e4 100644 --- a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs +++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs @@ -1,6 +1,4 @@ using System; -using System.Threading; -using System.Threading.Tasks; using GeneralUpdate.Core.Differential; using GeneralUpdate.Core.Models; using GeneralUpdate.Differential.Abstractions; diff --git a/tests/DifferentialTest/Matchers/MatcherTests.cs b/tests/DifferentialTest/Matchers/MatcherTests.cs index cf1a9fa8..d80a54b9 100644 --- a/tests/DifferentialTest/Matchers/MatcherTests.cs +++ b/tests/DifferentialTest/Matchers/MatcherTests.cs @@ -32,7 +32,7 @@ public void Dispose() } } - #region DefaultCleanMatcher �?Compare and Except + #region DefaultCleanMatcher — Compare and Except [Fact] public void DefaultCleanMatcher_Compare_ReturnsDifferentNodes_WhenFilesChange()