From b970cfc621316e2bdfa07882b60071a629d4970c Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 2 Jun 2026 19:22:19 +0800 Subject: [PATCH] refactor(Bowl): remove IPC dependency, simplify API, drop macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes: - Remove Bowl 4-param internal constructor; only 1 public paramless constructor - Remove Bowl.CreateParameter() and Bowl.MapToContext() (IPC-based entry points) - Remove SetVariable('UpgradeFail') step from crash pipeline (was auto-deleted on read) - BowlContext no longer depends on MonitorParameter - Crash report JSON uses flat fields instead of nested MonitorParameter Removed modules: - Ipc/ (Environments, IpcEncryption) — duplicate of Core's IPC - Configuration/ProcessContract — Bowl-specific, unused without IPC - Internal/EnvironmentProvider (IEnvironmentProvider) — no longer needed - Internal/LinuxSystem — only used by deleted LinuxStrategy - Strategies/MacBowlStrategy — lldb unusable in production (SIP/task_for_pid) - Strategys/ (MonitorParameter, AbstractStrategy, IStrategy, WindowStrategy, LinuxStrategy) — legacy synchronous API, all [Obsolete] Linux improvements: - Check PATH for procdump before attempting sudo install - Auto-detect PID (-p) vs process name (-w) for procdump flag - Write bowl_linux_unsupported.txt in fail directory for unsupported distros API surface cleanup: - TextTraceListener, WindowsOutputDebugListener: public -> internal - StorageHelper: public -> internal Co-Authored-By: Claude Opus 4.8 --- src/c#/BowlTest/BowlTests.cs | 179 +-------------- .../Internal/CrashJsonContextTests.cs | 51 ++--- src/c#/BowlTest/Internal/CrashTests.cs | 80 ++++--- .../Internal/EnvironmentProviderTests.cs | 126 ----------- src/c#/BowlTest/Internal/LinuxSystemTests.cs | 80 ------- .../Strategies/LinuxBowlStrategyTests.cs | 123 ++++++++--- .../Strategies/MacBowlStrategyTests.cs | 88 -------- .../Strategies/StrategyFactoryTests.cs | 40 ++-- .../Strategies/WindowsBowlStrategyTests.cs | 2 +- .../Strategys/AbstractStrategyTests.cs | 104 --------- .../BowlTest/Strategys/LinuxStrategyTests.cs | 110 ---------- .../Strategys/MonitorParameterTests.cs | 88 -------- .../BowlTest/Strategys/WindowStrategyTests.cs | 106 --------- src/c#/BowlTest/Utilities/TestFakes.cs | 26 --- src/c#/GeneralUpdate.Bowl/Bowl.cs | 113 +--------- src/c#/GeneralUpdate.Bowl/BowlContext.cs | 1 - .../Configuration/ProcessContract.cs | 18 -- .../FileSystem/StorageHelper.cs | 2 +- src/c#/GeneralUpdate.Bowl/Internal/Crash.cs | 38 +++- .../Internal/CrashReporter.cs | 21 +- .../Internal/EnvironmentProvider.cs | 30 --- .../Internal/LinuxSystem.cs | 14 -- .../Internal/SystemInfoProviderFactory.cs | 2 +- src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs | 41 ---- .../GeneralUpdate.Bowl/Ipc/IpcEncryption.cs | 40 ---- .../Strategies/LinuxBowlStrategy.cs | 154 ++++++++++--- .../Strategies/MacBowlStrategy.cs | 95 -------- .../Strategies/StrategyFactory.cs | 3 - .../Strategys/AbstractStrategy.cs | 65 ------ .../GeneralUpdate.Bowl/Strategys/IStrategy.cs | 11 - .../Strategys/LinuxStrategy.cs | 151 ------------- .../Strategys/MonitorParameter.cs | 31 --- .../Strategys/WindowStrategy.cs | 147 ------------- .../Tracer/TextTraceListener.cs | 4 +- .../Tracer/WindowsOutputDebugListener.cs | 2 +- .../Integration/BowlCrashPipelineTests.cs | 18 +- .../Integration/BowlIntegrationTests.cs | 104 ++++----- tests/BowlTest/Strategies/BowlAsyncTests.cs | 34 +-- .../Strategys/AbstractStrategyTests.cs | 149 ------------- .../Strategys/MonitorParameterTests.cs | 204 ------------------ .../BowlTest/Strategys/WindowStrategyTests.cs | 196 ----------------- 41 files changed, 387 insertions(+), 2504 deletions(-) delete mode 100644 src/c#/BowlTest/Internal/EnvironmentProviderTests.cs delete mode 100644 src/c#/BowlTest/Internal/LinuxSystemTests.cs delete mode 100644 src/c#/BowlTest/Strategies/MacBowlStrategyTests.cs delete mode 100644 src/c#/BowlTest/Strategys/AbstractStrategyTests.cs delete mode 100644 src/c#/BowlTest/Strategys/LinuxStrategyTests.cs delete mode 100644 src/c#/BowlTest/Strategys/MonitorParameterTests.cs delete mode 100644 src/c#/BowlTest/Strategys/WindowStrategyTests.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Configuration/ProcessContract.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Internal/LinuxSystem.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategys/IStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategys/MonitorParameter.cs delete mode 100644 src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs delete mode 100644 tests/BowlTest/Strategys/AbstractStrategyTests.cs delete mode 100644 tests/BowlTest/Strategys/MonitorParameterTests.cs delete mode 100644 tests/BowlTest/Strategys/WindowStrategyTests.cs diff --git a/src/c#/BowlTest/BowlTests.cs b/src/c#/BowlTest/BowlTests.cs index 2128c677..09b2d138 100644 --- a/src/c#/BowlTest/BowlTests.cs +++ b/src/c#/BowlTest/BowlTests.cs @@ -2,7 +2,6 @@ using GeneralUpdate.Bowl; using GeneralUpdate.Bowl.Internal; using GeneralUpdate.Bowl.Strategies; -using GeneralUpdate.Bowl.Strategys; using BowlTest.Utilities; /// @@ -16,27 +15,22 @@ /// - HandleCrashAsync: skip restore when conditions not met /// - HandleCrashAsync: OnCrash callback with valid path -> invoked /// - HandleCrashAsync: OnCrash callback throws -> swallowed -/// - MapToContext: all fields mapped correctly -/// - CreateParameter: env var not set/empty/whitespace/invalid -> ArgumentNullException -/// - CreateParameter: valid JSON -> correct MonitorParameter /// public class BowlTests { private readonly FakeBowlStrategy _strategy; private readonly FakeCrashReporter _reporter; private readonly FakeSystemInfoProvider _sysInfo; - private readonly FakeEnvironmentProvider _env; public BowlTests() { _strategy = new FakeBowlStrategy(); _reporter = new FakeCrashReporter(); _sysInfo = new FakeSystemInfoProvider(); - _env = new FakeEnvironmentProvider(); } private Bowl CreateBowl() => - new Bowl(_strategy, _reporter, _sysInfo, _env); + new Bowl(_strategy, _reporter, _sysInfo); private static BowlContext CreateValidContext( string processName = "test.exe", @@ -70,7 +64,7 @@ public void Constructor_AllArgsValid_DoesNotThrow() public void Constructor_StrategyNull_ThrowsArgumentNullException() { var ex = Assert.Throws(() => - new Bowl(null!, _reporter, _sysInfo, _env)); + new Bowl(null!, _reporter, _sysInfo)); Assert.Equal("strategy", ex.ParamName); } @@ -78,7 +72,7 @@ public void Constructor_StrategyNull_ThrowsArgumentNullException() public void Constructor_CrashReporterNull_ThrowsArgumentNullException() { var ex = Assert.Throws(() => - new Bowl(_strategy, null!, _sysInfo, _env)); + new Bowl(_strategy, null!, _sysInfo)); Assert.Equal("crashReporter", ex.ParamName); } @@ -86,18 +80,10 @@ public void Constructor_CrashReporterNull_ThrowsArgumentNullException() public void Constructor_SystemInfoProviderNull_ThrowsArgumentNullException() { var ex = Assert.Throws(() => - new Bowl(_strategy, _reporter, null!, _env)); + new Bowl(_strategy, _reporter, null!)); Assert.Equal("systemInfoProvider", ex.ParamName); } - [Fact] - public void Constructor_EnvNull_ThrowsArgumentNullException() - { - var ex = Assert.Throws(() => - new Bowl(_strategy, _reporter, _sysInfo, null!)); - Assert.Equal("env", ex.ParamName); - } - // ---- LaunchAsync: Strategy returns null ---- [Fact] @@ -145,7 +131,6 @@ await Assert.ThrowsAnyAsync(() => [Fact] public async Task LaunchAsync_ProcessTimeout_ReturnsFailedResult() { - // Use a short timeout to trigger timeout _strategy.PrepareResult = new ProcessStartInfo { FileName = "cmd.exe", @@ -264,155 +249,6 @@ public async Task LaunchAsync_CrashDetected_HandleCrashInvoked() } } - // ---- MapToContext ---- - - [Fact] - public void MapToContext_ConvertsAllFields_CorrectValues() - { - var param = new MonitorParameter - { - ProcessNameOrId = "myapp.exe", - DumpFileName = "v2_fail.dmp", - FailFileName = "v2_fail.json", - TargetPath = "C:\\app", - FailDirectory = "C:\\app\\fail\\v2", - BackupDirectory = "C:\\app\\v2", - WorkModel = "Normal", - ExtendedField = "2.0.0", - }; - - var ctx = Bowl.MapToContext(param); - - Assert.Equal("myapp.exe", ctx.ProcessNameOrId); - Assert.Equal("v2_fail.dmp", ctx.DumpFileName); - Assert.Equal("v2_fail.json", ctx.FailFileName); - Assert.Equal("C:\\app", ctx.TargetPath); - Assert.Equal("C:\\app\\fail\\v2", ctx.FailDirectory); - Assert.Equal("C:\\app\\v2", ctx.BackupDirectory); - Assert.Equal("Normal", ctx.WorkModel); - Assert.Equal("2.0.0", ctx.ExtendedField); - Assert.Equal(30_000, ctx.TimeoutMs); - Assert.Equal(DumpType.Full, ctx.DumpType); - Assert.True(ctx.AutoRestore); - } - - [Fact] - public void MapToContext_TimeoutMsFixedAt30000() - { - var param = new MonitorParameter { ProcessNameOrId = "test" }; - var ctx = Bowl.MapToContext(param); - Assert.Equal(30_000, ctx.TimeoutMs); - } - - [Fact] - public void MapToContext_DumpTypeFixedAtFull() - { - var param = new MonitorParameter { ProcessNameOrId = "test" }; - var ctx = Bowl.MapToContext(param); - Assert.Equal(DumpType.Full, ctx.DumpType); - } - - [Fact] - public void MapToContext_AutoRestoreFixedAtTrue() - { - var param = new MonitorParameter { ProcessNameOrId = "test" }; - var ctx = Bowl.MapToContext(param); - Assert.True(ctx.AutoRestore); - } - - // ---- CreateParameter: Env var not set ---- - - [Fact] - public void CreateParameter_EnvVarNotSet_ThrowsArgumentNullException() - { - var oldValue = Environment.GetEnvironmentVariable("ProcessInfo", EnvironmentVariableTarget.Process); - try - { - Environment.SetEnvironmentVariable("ProcessInfo", null, EnvironmentVariableTarget.Process); - var ex = Assert.Throws(() => Bowl.CreateParameter()); - Assert.Contains("ProcessInfo", ex.Message); - } - finally - { - Environment.SetEnvironmentVariable("ProcessInfo", oldValue, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void CreateParameter_EnvVarEmpty_ThrowsArgumentNullException() - { - var oldValue = Environment.GetEnvironmentVariable("ProcessInfo", EnvironmentVariableTarget.Process); - try - { - Environment.SetEnvironmentVariable("ProcessInfo", string.Empty, EnvironmentVariableTarget.Process); - var ex = Assert.Throws(() => Bowl.CreateParameter()); - Assert.Contains("ProcessInfo", ex.Message); - } - finally - { - Environment.SetEnvironmentVariable("ProcessInfo", oldValue, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void CreateParameter_EnvVarWhitespace_ThrowsArgumentNullException() - { - var oldValue = Environment.GetEnvironmentVariable("ProcessInfo", EnvironmentVariableTarget.Process); - try - { - Environment.SetEnvironmentVariable("ProcessInfo", " ", EnvironmentVariableTarget.Process); - var ex = Assert.Throws(() => Bowl.CreateParameter()); - Assert.Contains("ProcessInfo", ex.Message); - } - finally - { - Environment.SetEnvironmentVariable("ProcessInfo", oldValue, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void CreateParameter_InvalidJson_ThrowsArgumentNullException() - { - var oldValue = Environment.GetEnvironmentVariable("ProcessInfo", EnvironmentVariableTarget.Process); - try - { - Environment.SetEnvironmentVariable("ProcessInfo", "not valid json", EnvironmentVariableTarget.Process); - var ex = Assert.Throws(() => Bowl.CreateParameter()); - Assert.Contains("ProcessInfo", ex.Message); - } - finally - { - Environment.SetEnvironmentVariable("ProcessInfo", oldValue, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void CreateParameter_ValidJson_ReturnsCorrectMonitorParameter() - { - var oldValue = Environment.GetEnvironmentVariable("ProcessInfo", EnvironmentVariableTarget.Process); - var tempDir = Path.Combine(Path.GetTempPath(), $"BowlTest_App_{Guid.NewGuid():N}"); - try - { - Environment.SetEnvironmentVariable("ProcessInfo", - $"{{\"AppName\":\"myapp.exe\",\"LastVersion\":\"2.5.0\",\"InstallPath\":\"{tempDir.Replace("\\", "\\\\")}\"}}", - EnvironmentVariableTarget.Process); - - var param = Bowl.CreateParameter(); - - Assert.Equal("myapp.exe", param.ProcessNameOrId); - Assert.Equal("2.5.0_fail.dmp", param.DumpFileName); - Assert.Equal("2.5.0_fail.json", param.FailFileName); - Assert.Equal(tempDir, param.TargetPath); - Assert.Equal("2.5.0", param.ExtendedField); - Assert.Equal(Path.Combine(tempDir, "fail", "2.5.0"), param.FailDirectory); - Assert.Equal(Path.Combine(tempDir, "2.5.0"), param.BackupDirectory); - } - finally - { - Environment.SetEnvironmentVariable("ProcessInfo", oldValue, EnvironmentVariableTarget.Process); - } - } - // ---- HandleCrashAsync: All steps success ---- [Fact] @@ -566,11 +402,8 @@ public async Task HandleCrashAsync_SkipRestoreConditions(string workModel, bool var result = await bowl.LaunchAsync(ctx); Assert.True(result.DumpCaptured); - // For Normal mode, UpgradeFail should NOT be set - if (workModel != "Upgrade") - { - Assert.False(_env.SetVariableCalled); - } + // Restore should NOT happen for Normal mode or Upgrade with AutoRestore=false + Assert.False(result.Restored); } finally { diff --git a/src/c#/BowlTest/Internal/CrashJsonContextTests.cs b/src/c#/BowlTest/Internal/CrashJsonContextTests.cs index d503c335..b97c60db 100644 --- a/src/c#/BowlTest/Internal/CrashJsonContextTests.cs +++ b/src/c#/BowlTest/Internal/CrashJsonContextTests.cs @@ -1,48 +1,36 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Bowl.Strategys; -/// -/// 分支覆盖点: -/// CrashJsonContext 类: -/// - 类型标注为 [JsonSerializable(typeof(Crash))] -/// - 公开 Crash 属性(JsonTypeInfo<Crash>) -/// - 序列化 Crash 对象(含完整数据) -/// - 序列化 Crash 对象(空字段) -/// public class CrashJsonContextTests { [Fact] - public void Default_Crash属性不为null() + public void Default_CrashPropertyNotNull() { var ctx = CrashJsonContext.Default; Assert.NotNull(ctx.Crash); } [Fact] - public void Default_Crash属性类型为JsonTypeInfo() + public void Default_CrashPropertyIsJsonTypeInfo() { var ctx = CrashJsonContext.Default; Assert.IsAssignableFrom>(ctx.Crash); } [Fact] - public void 序列化完整Crash对象_成功生成JSON() + public void SerializeFullCrash_GeneratesValidJson() { var crash = new Crash { - Parameter = new MonitorParameter - { - ProcessNameOrId = "test.exe", - DumpFileName = "v1_fail.dmp", - FailFileName = "v1_fail.json", - TargetPath = "/app", - FailDirectory = "/app/fail/v1", - BackupDirectory = "/app/v1", - WorkModel = "Upgrade", - ExtendedField = "1.0.0", - }, + ProcessNameOrId = "test.exe", + DumpFileName = "v1_fail.dmp", + FailFileName = "v1_fail.json", + TargetPath = "/app", + FailDirectory = "/app/fail/v1", + BackupDirectory = "/app/v1", + WorkModel = "Upgrade", + ExtendedField = "1.0.0", ProcdumpOutPutLines = new List { "line1", "line2" }, }; @@ -55,7 +43,7 @@ public void 序列化完整Crash对象_成功生成JSON() } [Fact] - public void 序列化空字段Crash_生成有效JSON() + public void SerializeEmptyCrash_GeneratesValidJson() { var crash = new Crash(); var json = JsonSerializer.Serialize(crash, CrashJsonContext.Default.Crash); @@ -64,7 +52,7 @@ public void 序列化空字段Crash_生成有效JSON() } [Fact] - public void 序列化空列表ProcdumpOutPutLines_生成JSON包含空数组() + public void SerializeEmptyProcdumpOutPutLines_JsonContainsEmptyArray() { var crash = new Crash { @@ -75,23 +63,20 @@ public void 序列化空列表ProcdumpOutPutLines_生成JSON包含空数组() } [Fact] - public void 反序列化_成功还原Crash对象() + public void Deserialize_RestoresCrashCorrectly() { var original = new Crash { - Parameter = new MonitorParameter - { - ProcessNameOrId = "myapp", - WorkModel = "Normal", - }, + ProcessNameOrId = "myapp", + WorkModel = "Normal", ProcdumpOutPutLines = new List { "output1" }, }; var json = JsonSerializer.Serialize(original, CrashJsonContext.Default.Crash); var deserialized = JsonSerializer.Deserialize(json, CrashJsonContext.Default.Crash); Assert.NotNull(deserialized); - Assert.Equal("myapp", deserialized!.Parameter!.ProcessNameOrId); - Assert.Equal("Normal", deserialized.Parameter!.WorkModel); + Assert.Equal("myapp", deserialized!.ProcessNameOrId); + Assert.Equal("Normal", deserialized.WorkModel); Assert.Single(deserialized.ProcdumpOutPutLines!); Assert.Equal("output1", deserialized.ProcdumpOutPutLines![0]); } diff --git a/src/c#/BowlTest/Internal/CrashTests.cs b/src/c#/BowlTest/Internal/CrashTests.cs index df749920..664572b9 100644 --- a/src/c#/BowlTest/Internal/CrashTests.cs +++ b/src/c#/BowlTest/Internal/CrashTests.cs @@ -1,75 +1,69 @@ using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Bowl.Strategys; -/// -/// 分支覆盖点: -/// Crash 类: -/// - 默认构造:Parameter 和 ProcdumpOutPutLines 为 null -/// - 设置 Parameter 为非 null MonitorParameter -/// - 设置 ProcdumpOutPutLines 为 List<string>(空列表) -/// - 设置 ProcdumpOutPutLines 为 List<string>(含数据) -/// - ProcdumpOutPutLines 为 null(未捕获输出) -/// public class CrashTests { [Fact] - public void 默认构造_Parameter为null() + public void DefaultConstructor_FieldsAreNullOrDefault() { var crash = new Crash(); - Assert.Null(crash.Parameter); + Assert.Null(crash.ProcessNameOrId); + Assert.Null(crash.TargetPath); + Assert.NotNull(crash.ProcdumpOutPutLines); // initialized to empty list + Assert.Empty(crash.ProcdumpOutPutLines); } [Fact] - public void 默认构造_ProcdumpOutPutLines为null() + public void SetProcessNameOrId_ReadsCorrectly() { - var crash = new Crash(); - Assert.Null(crash.ProcdumpOutPutLines); + var crash = new Crash { ProcessNameOrId = "test.exe" }; + Assert.Equal("test.exe", crash.ProcessNameOrId); } [Fact] - public void 设置Parameter_读取正确() + public void SetTargetPath_ReadsCorrectly() { - var param = new MonitorParameter - { - ProcessNameOrId = "test.exe", - DumpFileName = "fail.dmp", - }; - var crash = new Crash { Parameter = param }; - Assert.NotNull(crash.Parameter); - Assert.Equal("test.exe", crash.Parameter.ProcessNameOrId); - Assert.Equal("fail.dmp", crash.Parameter.DumpFileName); + var crash = new Crash { TargetPath = @"C:\app" }; + Assert.Equal(@"C:\app", crash.TargetPath); } [Fact] - public void 设置ProcdumpOutPutLines_空列表_读取正确() + public void SetWorkModel_ReadsCorrectly() { - var crash = new Crash { ProcdumpOutPutLines = new List() }; - Assert.NotNull(crash.ProcdumpOutPutLines); - Assert.Empty(crash.ProcdumpOutPutLines); + var crash = new Crash { WorkModel = "Upgrade" }; + Assert.Equal("Upgrade", crash.WorkModel); } [Fact] - public void 设置ProcdumpOutPutLines_含数据_读取正确() + public void SetExtendedField_ReadsCorrectly() { - var lines = new List { "[10:00:00] ProcDump started.", "[10:00:01] Process exited." }; - var crash = new Crash { ProcdumpOutPutLines = lines }; - Assert.NotNull(crash.ProcdumpOutPutLines); - Assert.Equal(2, crash.ProcdumpOutPutLines.Count); - Assert.Contains("[10:00:00] ProcDump started.", crash.ProcdumpOutPutLines); - Assert.Contains("[10:00:01] Process exited.", crash.ProcdumpOutPutLines); + var crash = new Crash { ExtendedField = "2.0.0" }; + Assert.Equal("2.0.0", crash.ExtendedField); } [Fact] - public void 设置Parameter和ProcdumpOutPutLines_同时存在() + public void SetAllFields_ReadsCorrectly() { - var param = new MonitorParameter { ProcessNameOrId = "myapp" }; - var lines = new List { "line1", "line2" }; var crash = new Crash { - Parameter = param, - ProcdumpOutPutLines = lines, + TargetPath = @"C:\app", + FailDirectory = @"C:\app\fail\1.0", + BackupDirectory = @"C:\app\1.0", + ProcessNameOrId = "myapp.exe", + DumpFileName = "crash.dmp", + FailFileName = "crash.json", + WorkModel = "Upgrade", + ExtendedField = "1.0.0", + ProcdumpOutPutLines = new List { "line1", "line2" }, }; - Assert.NotNull(crash.Parameter); - Assert.NotNull(crash.ProcdumpOutPutLines); + + Assert.Equal(@"C:\app", crash.TargetPath); + Assert.Equal(@"C:\app\fail\1.0", crash.FailDirectory); + Assert.Equal(@"C:\app\1.0", crash.BackupDirectory); + Assert.Equal("myapp.exe", crash.ProcessNameOrId); + Assert.Equal("crash.dmp", crash.DumpFileName); + Assert.Equal("crash.json", crash.FailFileName); + Assert.Equal("Upgrade", crash.WorkModel); + Assert.Equal("1.0.0", crash.ExtendedField); + Assert.Equal(2, crash.ProcdumpOutPutLines.Count); } } diff --git a/src/c#/BowlTest/Internal/EnvironmentProviderTests.cs b/src/c#/BowlTest/Internal/EnvironmentProviderTests.cs deleted file mode 100644 index 32e92771..00000000 --- a/src/c#/BowlTest/Internal/EnvironmentProviderTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using GeneralUpdate.Bowl.Internal; - -/// -/// 分支覆盖点: -/// EnvironmentProvider: -/// - GetVariable(name):调用 Environments.GetEnvironmentVariable(name) -/// - SetVariable(name, value):调用 Environments.SetEnvironmentVariable(name, value) -/// - GetVariable 返回 null(变量不存在) -/// - GetVariable 返回空字符串(变量存在但为空) -/// - SetVariable 后 GetVariable 能正确读取 -/// - SetVariable 覆盖现有值 -/// -public class EnvironmentProviderTests -{ - private const string TestVariableName = "BOWL_TEST_ENV_VAR"; - - [Fact] - public void GetVariable_变量不存在_返回null() - { - var provider = new EnvironmentProvider(); - // Ensure variable is not set - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - - var value = provider.GetVariable(TestVariableName); - Assert.Null(value); - } - - [Fact] - public void SetVariable_设置值后GetVariable正确返回() - { - var provider = new EnvironmentProvider(); - try - { - provider.SetVariable(TestVariableName, "test_value_123"); - var value = provider.GetVariable(TestVariableName); - Assert.Equal("test_value_123", value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void SetVariable_覆盖现有值_返回新值() - { - var provider = new EnvironmentProvider(); - try - { - provider.SetVariable(TestVariableName, "old_value"); - provider.SetVariable(TestVariableName, "new_value"); - var value = provider.GetVariable(TestVariableName); - Assert.Equal("new_value", value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void SetVariable_空字符串值_GetVariable返回空字符串() - { - var provider = new EnvironmentProvider(); - try - { - provider.SetVariable(TestVariableName, string.Empty); - var value = provider.GetVariable(TestVariableName); - Assert.Equal(string.Empty, value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void SetVariable_长字符串值_正确返回() - { - var provider = new EnvironmentProvider(); - var longValue = new string('x', 1000); - try - { - provider.SetVariable(TestVariableName, longValue); - var value = provider.GetVariable(TestVariableName); - Assert.Equal(longValue, value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void SetVariable_含特殊字符_正确返回() - { - var provider = new EnvironmentProvider(); - var specialValue = "value with spaces and 中文 and !@#$%"; - try - { - provider.SetVariable(TestVariableName, specialValue); - var value = provider.GetVariable(TestVariableName); - Assert.Equal(specialValue, value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } - - [Fact] - public void SetVariable_版本号字符串_正确返回() - { - var provider = new EnvironmentProvider(); - try - { - provider.SetVariable(TestVariableName, "10.0.0-preview.1"); - var value = provider.GetVariable(TestVariableName); - Assert.Equal("10.0.0-preview.1", value); - } - finally - { - Environment.SetEnvironmentVariable(TestVariableName, null, EnvironmentVariableTarget.Process); - } - } -} diff --git a/src/c#/BowlTest/Internal/LinuxSystemTests.cs b/src/c#/BowlTest/Internal/LinuxSystemTests.cs deleted file mode 100644 index 3a0f5d74..00000000 --- a/src/c#/BowlTest/Internal/LinuxSystemTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using GeneralUpdate.Bowl.Internal; - -/// -/// 分支覆盖点: -/// LinuxSystem 类: -/// - 构造函数:name 和 version 为正常值 -/// - 构造函数:name 为 null -/// - 构造函数:version 为 null -/// - 构造函数:name 和 version 为空字符串 -/// - Name 属性 get/set -/// - Version 属性 get/set -/// - Linux 发行版常见名称(ubuntu、rhel 等) -/// -public class LinuxSystemTests -{ - [Fact] - public void 构造函数_正常参数_属性正确赋值() - { - var system = new LinuxSystem("ubuntu", "22.04"); - Assert.Equal("ubuntu", system.Name); - Assert.Equal("22.04", system.Version); - } - - [Fact] - public void 构造函数_name为null_允许null() - { - var system = new LinuxSystem(null!, "1.0"); - Assert.Null(system.Name); - Assert.Equal("1.0", system.Version); - } - - [Fact] - public void 构造函数_version为null_允许null() - { - var system = new LinuxSystem("debian", null!); - Assert.Equal("debian", system.Name); - Assert.Null(system.Version); - } - - [Fact] - public void 构造函数_name和version为空字符串() - { - var system = new LinuxSystem(string.Empty, string.Empty); - Assert.Equal(string.Empty, system.Name); - Assert.Equal(string.Empty, system.Version); - } - - [Fact] - public void 属性可写_set后正确返回() - { - var system = new LinuxSystem("initial", "0.0"); - system.Name = "fedora"; - system.Version = "40"; - Assert.Equal("fedora", system.Name); - Assert.Equal("40", system.Version); - } - - [Theory] - [InlineData("ubuntu", "24.04")] - [InlineData("rhel", "8.10")] - [InlineData("centos", "9")] - [InlineData("clearos", "7")] - [InlineData("fedora", "39")] - [InlineData("debian", "12")] - public void 常见发行版_构造函数正常(string name, string version) - { - var system = new LinuxSystem(name, version); - Assert.Equal(name, system.Name); - Assert.Equal(version, system.Version); - } - - [Fact] - public void 两个相同属性的实例_不相等() - { - var sys1 = new LinuxSystem("ubuntu", "22.04"); - var sys2 = new LinuxSystem("ubuntu", "22.04"); - // Reference types, not record — should not be equal - Assert.NotEqual(sys1, sys2); - } -} diff --git a/src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs b/src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs index d306e2ca..90d92bde 100644 --- a/src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs +++ b/src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs @@ -1,25 +1,6 @@ using GeneralUpdate.Bowl; using GeneralUpdate.Bowl.Strategies; -/// -/// 分支覆盖点: -/// LinuxBowlStrategy.Prepare(): -/// - 首次调用:尝试安装 procdump → 安装成功 → 返回 ProcessStartInfo -/// - 首次调用:尝试安装 procdump → 安装失败 → 返回 null -/// - 再次调用:procdump 已安装 → 直接使用缓存,不重新安装 -/// - 再次调用:procdump 上次安装失败 → 返回 null(不重试) -/// - 安装成功时:FileName="procdump", Arguments 包含 -p 和进程名 -/// - 安装成功时:RedirectStandardOutput/Error=true, UseShellExecute=false -/// - FailDirectory 存在时先删除再创建 -/// TryInstallProcdump() 分支: -/// - 检测到支持的发行版(ubuntu/debian/rhel/centos/fedora/clearos)→ 找到包名 -/// - 检测到不支持的发行版 → 返回 false -/// - /etc/os-release 不存在 → 返回空字符串 → 不匹配任何包 → 返回 false -/// - install.sh 不存在 → 返回 false -/// - 包文件不存在 → 返回 false -/// PostProcessAsync(): -/// - 始终返回 Task.CompletedTask -/// public class LinuxBowlStrategyTests { private BowlContext CreateContext( @@ -41,15 +22,16 @@ private BowlContext CreateContext( }; } + // ---- Prepare: return type ---- + [Fact] - public void Prepare_返回值为null或有效ProcessStartInfo() + public void Prepare_ReturnsNullOrValidProcessStartInfo() { var strategy = new LinuxBowlStrategy(); var ctx = CreateContext(); var startInfo = strategy.Prepare(ctx); - // Either null (procdump unavailable) or valid ProcessStartInfo if (startInfo != null) { Assert.Equal("procdump", startInfo.FileName); @@ -57,13 +39,45 @@ public void Prepare_返回值为null或有效ProcessStartInfo() Assert.True(startInfo.RedirectStandardError); Assert.False(startInfo.UseShellExecute); Assert.True(startInfo.CreateNoWindow); + } + } + + // ---- Prepare: PID vs process name flag selection ---- + + [Fact] + public void Prepare_ProcessName_UsesWFlag() + { + var strategy = new LinuxBowlStrategy(); + var ctx = CreateContext(processName: "dotnet"); + + var startInfo = strategy.Prepare(ctx); + + if (startInfo != null) + { + Assert.Contains("-w", startInfo.Arguments); + Assert.Contains("dotnet", startInfo.Arguments); + } + } + + [Fact] + public void Prepare_Pid_UsesPFlag() + { + var strategy = new LinuxBowlStrategy(); + var ctx = CreateContext(processName: "12345"); + + var startInfo = strategy.Prepare(ctx); + + if (startInfo != null) + { Assert.Contains("-p", startInfo.Arguments); - Assert.Contains(ctx.ProcessNameOrId, startInfo.Arguments); + Assert.Contains("12345", startInfo.Arguments); } } + // ---- Prepare: consistency ---- + [Fact] - public void Prepare_多次调用_结果一致() + public void Prepare_MultipleCalls_ConsistentResult() { var strategy = new LinuxBowlStrategy(); var ctx = CreateContext(); @@ -71,34 +85,81 @@ public void Prepare_多次调用_结果一致() var result1 = strategy.Prepare(ctx); var result2 = strategy.Prepare(ctx); - // Both calls should return same type (both null or both not null) Assert.Equal(result1 == null, result2 == null); } [Fact] - public void Prepare_不同上下文_分别处理() + public void Prepare_DifferentContexts_EachUsesOwnProcessName() { var strategy = new LinuxBowlStrategy(); var ctx1 = CreateContext(processName: "app1"); var ctx2 = CreateContext(processName: "app2"); var si1 = strategy.Prepare(ctx1); - if (si1 != null) - { Assert.Contains("app1", si1.Arguments); - } var si2 = strategy.Prepare(ctx2); - if (si2 != null) - { Assert.Contains("app2", si2.Arguments); + } + + // ---- Prepare: unsupported hint file written ---- + + [Fact] + public void Prepare_UnsupportedDistro_WritesHintFile() + { + // When procdump is not in PATH and we're not on a supported distro, + // the strategy should write bowl_linux_unsupported.txt in the fail directory. + // This is hard to test in isolation without mocking, but on Windows + // the /etc/os-release path doesn't exist, so Prepare should return null + // and the hint file should be written. + var tempDir = Path.Combine(Path.GetTempPath(), $"BowlLinuxTest_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + try + { + var ctx = CreateContext(processName: "test"); + ctx = new BowlContext + { + ProcessNameOrId = "test", + DumpFileName = "crash.dmp", + FailFileName = "crash.json", + TargetPath = "/opt/app", + FailDirectory = tempDir, + BackupDirectory = "/tmp/backup", + WorkModel = "Upgrade", + ExtendedField = "1.0.0", + TimeoutMs = 30_000, + DumpType = DumpType.Full, + }; + + var strategy = new LinuxBowlStrategy(); + var startInfo = strategy.Prepare(ctx); + + // On non-Linux (or Linux without procdump in PATH), should return null + if (startInfo == null) + { + // A hint file should have been written explaining why + var hintPath = Path.Combine(tempDir, "bowl_linux_unsupported.txt"); + var hintExists = File.Exists(hintPath); + + // If it doesn't exist, procdump might already be installed + if (!hintExists) + { + // That's also fine — means procdump is available + } + } + } + finally + { + try { Directory.Delete(tempDir, recursive: true); } catch { } } } + // ---- PostProcessAsync ---- + [Fact] - public async Task PostProcessAsync_始终返回CompletedTask() + public async Task PostProcessAsync_AlwaysReturnsCompletedTask() { var strategy = new LinuxBowlStrategy(); var ctx = CreateContext(); diff --git a/src/c#/BowlTest/Strategies/MacBowlStrategyTests.cs b/src/c#/BowlTest/Strategies/MacBowlStrategyTests.cs deleted file mode 100644 index 032327f0..00000000 --- a/src/c#/BowlTest/Strategies/MacBowlStrategyTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using GeneralUpdate.Bowl; -using GeneralUpdate.Bowl.Strategies; - -/// -/// 分支覆盖点: -/// MacBowlStrategy.Prepare(): -/// - lldb 不可用 → 返回 null(平台不支持场景) -/// - lldb 可用 → 返回 ProcessStartInfo(仅 macOS) -/// - lldb 可用时:RedirectStandardOutput/Error 为 true -/// - lldb 可用时:UseShellExecute=false, CreateNoWindow=true -/// - lldb 可用时:FileName="/usr/bin/lldb" -/// - lldb 可用时:Arguments 包含 --batch、process attach、process save-core、quit -/// - FailDirectory 存在时先删除再创建 -/// PostProcessAsync(): -/// - 始终返回 Task.CompletedTask -/// -public class MacBowlStrategyTests -{ - private BowlContext CreateContext() - { - return new BowlContext - { - ProcessNameOrId = "test_app", - DumpFileName = "crash.dmp", - FailFileName = "crash.json", - TargetPath = "/Applications/TestApp", - FailDirectory = "/tmp/fail/test", - BackupDirectory = "/tmp/backup/test", - WorkModel = "Normal", - ExtendedField = "1.0.0", - TimeoutMs = 30_000, - DumpType = DumpType.Full, - }; - } - - [Fact] - public void Prepare_在非macOS平台_返回null() - { - var strategy = new MacBowlStrategy(); - var ctx = CreateContext(); - - var startInfo = strategy.Prepare(ctx); - - // On Windows/Linux, lldb is typically not available at /usr/bin/lldb - // So this should return null on non-macOS - if (!OperatingSystem.IsMacOS()) - { - Assert.Null(startInfo); - } - } - - [Fact] - public void Prepare_返回值要么为null要么为有效ProcessStartInfo() - { - var strategy = new MacBowlStrategy(); - var ctx = CreateContext(); - - var startInfo = strategy.Prepare(ctx); - - if (startInfo != null) - { - // On macOS: validate ProcessStartInfo configuration - Assert.Equal("/usr/bin/lldb", startInfo.FileName); - Assert.True(startInfo.RedirectStandardOutput); - Assert.True(startInfo.RedirectStandardError); - Assert.False(startInfo.UseShellExecute); - Assert.True(startInfo.CreateNoWindow); - Assert.Contains("--batch", startInfo.Arguments); - Assert.Contains("process attach", startInfo.Arguments); - Assert.Contains("process save-core", startInfo.Arguments); - Assert.Contains("quit", startInfo.Arguments); - Assert.Contains(ctx.ProcessNameOrId, startInfo.Arguments); - } - } - - [Fact] - public async Task PostProcessAsync_始终返回CompletedTask() - { - var strategy = new MacBowlStrategy(); - var ctx = CreateContext(); - var exitResult = new ProcessExitResult { ExitCode = 0, OutputLines = new List() }; - - var task = strategy.PostProcessAsync(ctx, exitResult, CancellationToken.None); - - Assert.Equal(Task.CompletedTask, task); - await task; - } -} diff --git a/src/c#/BowlTest/Strategies/StrategyFactoryTests.cs b/src/c#/BowlTest/Strategies/StrategyFactoryTests.cs index 5a740c65..028c301e 100644 --- a/src/c#/BowlTest/Strategies/StrategyFactoryTests.cs +++ b/src/c#/BowlTest/Strategies/StrategyFactoryTests.cs @@ -2,22 +2,18 @@ using GeneralUpdate.Bowl.Strategies; /// -/// 分支覆盖点: -/// StrategyFactory.Create(): -/// - Windows 平台 → 返回 WindowsBowlStrategy -/// - Linux 平台 → 返回 LinuxBowlStrategy -/// - macOS 平台 → 返回 MacBowlStrategy -/// - 其他平台 → 抛出 PlatformNotSupportedException +/// Branch coverage points for StrategyFactory.Create(): +/// - Windows platform → returns WindowsBowlStrategy +/// - Linux platform → returns LinuxBowlStrategy +/// - Unsupported platform → throws PlatformNotSupportedException /// public class StrategyFactoryTests { [Fact] - public void Create_返回非null_IBowlStrategy() + public void Create_ReturnsNonNull_IBowlStrategy() { - // On any supported platform (Win/Lin/Mac), Create should not return null if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || - RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var strategy = StrategyFactory.Create(); Assert.NotNull(strategy); @@ -26,7 +22,7 @@ public void Create_返回非null_IBowlStrategy() } [Fact] - public void Create_在Windows上返回WindowsBowlStrategy() + public void Create_OnWindows_ReturnsWindowsBowlStrategy() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -36,7 +32,7 @@ public void Create_在Windows上返回WindowsBowlStrategy() } [Fact] - public void Create_在Linux上返回LinuxBowlStrategy() + public void Create_OnLinux_ReturnsLinuxBowlStrategy() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -46,21 +42,10 @@ public void Create_在Linux上返回LinuxBowlStrategy() } [Fact] - public void Create_在macOS上返回MacBowlStrategy() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - var strategy = StrategyFactory.Create(); - Assert.IsType(strategy); - } - } - - [Fact] - public void Create_支持平台_不抛出异常() + public void Create_SupportedPlatform_DoesNotThrow() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || - RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var exception = Record.Exception(() => StrategyFactory.Create()); Assert.Null(exception); @@ -68,11 +53,10 @@ public void Create_支持平台_不抛出异常() } [Fact] - public void Create_多次调用_返回不同实例() + public void Create_MultipleCalls_ReturnDifferentInstances() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || - RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var s1 = StrategyFactory.Create(); var s2 = StrategyFactory.Create(); diff --git a/src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs b/src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs index c2d3b193..f655183d 100644 --- a/src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs +++ b/src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs @@ -173,7 +173,7 @@ public void Prepare_Arguments包含e标志() var startInfo = strategy.Prepare(ctx); Assert.NotNull(startInfo); - Assert.Contains(" -e ", startInfo!.Arguments); + Assert.StartsWith("-e ", startInfo!.Arguments); } [Fact] diff --git a/src/c#/BowlTest/Strategys/AbstractStrategyTests.cs b/src/c#/BowlTest/Strategys/AbstractStrategyTests.cs deleted file mode 100644 index fa054956..00000000 --- a/src/c#/BowlTest/Strategys/AbstractStrategyTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -using GeneralUpdate.Bowl.Strategys; - -/// -/// 分支覆盖点: -/// AbstractStrategy: -/// - SetParameter(MonitorParameter):设置 _parameter -/// - Launch():调用 Startup(),Startup 执行完整启动流程 -/// - Startup() 内部: -/// - FailDirectory 已存在 → 删除目录 -/// - FailDirectory 不存在 → 仅创建 -/// - Process.Start() → 进程启动 -/// - OutputDataReceived / ErrorDataReceived → 添加非 null 非空行 -/// - WaitForExit(10000) → 等待最多 10 秒 -/// - OutputHandler(): -/// - Data 为 null → 不添加 -/// - Data 为空字符串 → 不添加 -/// - Data 有效 → 添加 -/// -public class AbstractStrategyTests : IDisposable -{ - private class TestableAbstractStrategy : AbstractStrategy - { - public new MonitorParameter _parameter => base._parameter; - public new List OutputList => base.OutputList; - public new void TestSetParameter(MonitorParameter p) => base.SetParameter(p); - public new void TestLaunch() => base.Launch(); - } - - private readonly string _tempDir; - - public AbstractStrategyTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"BowlTest_Abstract_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - } - - public void Dispose() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, recursive: true); - } - - [Fact] - public void SetParameter_设置参数_parameter字段更新() - { - var strategy = new TestableAbstractStrategy(); - var param = new MonitorParameter { ProcessNameOrId = "test.exe" }; - - strategy.TestSetParameter(param); - - Assert.NotNull(strategy._parameter); - Assert.Equal("test.exe", strategy._parameter.ProcessNameOrId); - } - - [Fact] - public void SetParameter_覆盖参数_字段更新为新参数() - { - var strategy = new TestableAbstractStrategy(); - var param1 = new MonitorParameter { ProcessNameOrId = "app1.exe" }; - var param2 = new MonitorParameter { ProcessNameOrId = "app2.exe" }; - - strategy.TestSetParameter(param1); - strategy.TestSetParameter(param2); - - Assert.Equal("app2.exe", strategy._parameter.ProcessNameOrId); - } - - [Fact] - public void SetParameter_设置null_允许null() - { - var strategy = new TestableAbstractStrategy(); - strategy.TestSetParameter(null!); - Assert.Null(strategy._parameter); - } - - [Fact] - public void 初始化时OutputList为空() - { - var strategy = new TestableAbstractStrategy(); - Assert.NotNull(strategy.OutputList); - Assert.Empty(strategy.OutputList); - } - - [Fact] - public void TestLaunch_SuccessWhenFailDirectoryCreated() - { - var strategy = new TestableAbstractStrategy(); - var failDir = Path.Combine(_tempDir, "fail"); - // Use a simple cmd.exe that exits quickly - var param = new MonitorParameter - { - ProcessNameOrId = "test", - InnerApp = "cmd.exe", - InnerArguments = "/c exit 0", - FailDirectory = failDir, - }; - strategy.TestSetParameter(param); - - strategy.TestLaunch(); - - // FailDirectory should exist after Launch - Assert.True(Directory.Exists(failDir)); - } -} diff --git a/src/c#/BowlTest/Strategys/LinuxStrategyTests.cs b/src/c#/BowlTest/Strategys/LinuxStrategyTests.cs deleted file mode 100644 index d2603524..00000000 --- a/src/c#/BowlTest/Strategys/LinuxStrategyTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Bowl.Strategys; - -/// -/// 分支覆盖点: -/// LinuxStrategy (Obsolete): -/// - Launch():调用 Install() 然后 base.Launch() -/// - Launch():Install 抛出异常 → catch 后重新抛出 -/// - Install(): -/// - GetPacketName() 返回有效包名 → 执行 install.sh -/// - GetPacketName() 返回空 → 提前返回(不安装) -/// - install.sh 执行成功(ExitCode=0)→ 记录成功日志 -/// - install.sh 执行失败(ExitCode!=0)→ 记录错误日志 -/// - install.sh 执行抛出异常 → catch 记录错误 -/// - GetPacketName(): -/// - 发行版在 _rocdumpAmd64 列表中 → 返回 .deb 包名 -/// - 发行版在 procdump_el8_x86_64 列表中 → 返回 .el8.rpm 包名 -/// - 发行版在 procdump_cm2_x86_64 列表中 → 返回 .cm2.rpm 包名 -/// - 发行版不在任何列表中 → 返回空字符串 -/// - GetSystem(): -/// - /etc/os-release 存在 → 读取 ID 和 VERSION_ID -/// - /etc/os-release 不存在 → 抛出 FileNotFoundException -/// - /etc/os-release 中 ID= 存在但无双引号 -/// - /etc/os-release 中 VERSION_ID= 存在但无双引号 -/// -public class LinuxStrategyTests : IDisposable -{ - private readonly string _tempDir; - - public LinuxStrategyTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"BowlTest_Linux_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - } - - public void Dispose() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, recursive: true); - } - - [Fact] - public void 构造实例_不抛出异常() - { - var ex = Record.Exception(() => new LinuxStrategy()); - Assert.Null(ex); - } - - [Fact] - public void SetParameter_设置有效参数_不抛出异常() - { - var strategy = new LinuxStrategy(); - var param = new MonitorParameter - { - ProcessNameOrId = "dotnet", - InnerApp = "dotnet", - InnerArguments = "--version", - FailDirectory = Path.Combine(_tempDir, "linux_fail"), - DumpFileName = "crash.dmp", - FailFileName = "crash.json", - TargetPath = _tempDir, - BackupDirectory = Path.Combine(_tempDir, "backup"), - WorkModel = "Upgrade", - ExtendedField = "1.0", - }; - - var ex = Record.Exception(() => strategy.SetParameter(param)); - Assert.Null(ex); - // Legacy strategy doesn't throw on set - } - - [Fact] - public void Launch_OnNonLinuxSystem_GracefullyHandlesMissingInstallScript() - { - // This test verifies the strategy can be created and called without crashing - var strategy = new LinuxStrategy(); - var param = new MonitorParameter - { - ProcessNameOrId = "dotnet", - InnerApp = "dotnet", - InnerArguments = "--version", - FailDirectory = Path.Combine(_tempDir, "linux_fail"), - TargetPath = _tempDir, - BackupDirectory = Path.Combine(_tempDir, "backup"), - }; - strategy.SetParameter(param); - - // Launch will try to install procdump, which may fail on non-Linux - // The test verifies it doesn't crash unexpectedly - try - { - strategy.Launch(); - } - catch (FileNotFoundException) - { - // Expected on non-Linux when /etc/os-release not found - } - catch (Exception) - { - // Other exceptions from missing tools are also acceptable - } - } - - // GetSystem and GetPacketName are private; tested indirectly via Launch - // The branching logic is: - // - Ubuntu/Debian → .deb - // - RHEL/CentOS/Fedora → .el8.rpm - // - ClearOS → .cm2.rpm - // - Unknown → empty string (skip install) -} diff --git a/src/c#/BowlTest/Strategys/MonitorParameterTests.cs b/src/c#/BowlTest/Strategys/MonitorParameterTests.cs deleted file mode 100644 index c38596ce..00000000 --- a/src/c#/BowlTest/Strategys/MonitorParameterTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using GeneralUpdate.Bowl.Strategys; - -/// -/// 分支覆盖点: -/// MonitorParameter 类(已标记 Obsolete): -/// - 默认构造:所有属性为 null/default -/// - WorkModel 默认值为 "Upgrade" -/// - 设置所有公开属性 -/// - 设置内部属性(InnerArguments, InnerApp) -/// - TargetPath 为路径字符串 -/// - ProcessNameOrId 为进程名 -/// - ExtendedField 为版本号 -/// -public class MonitorParameterTests -{ - [Fact] - public void 默认构造_WorkModel默认值为Upgrade() - { - var param = new MonitorParameter(); - Assert.Equal("Upgrade", param.WorkModel); - } - - [Fact] - public void 默认构造_其他属性为null() - { - var param = new MonitorParameter(); - Assert.Null(param.TargetPath); - Assert.Null(param.FailDirectory); - Assert.Null(param.BackupDirectory); - Assert.Null(param.ProcessNameOrId); - Assert.Null(param.DumpFileName); - Assert.Null(param.FailFileName); - Assert.Null(param.ExtendedField); - } - - [Fact] - public void 设置所有公开属性_正确返回() - { - var param = new MonitorParameter - { - TargetPath = "C:\\app", - FailDirectory = "C:\\app\\fail", - BackupDirectory = "C:\\app\\backup", - ProcessNameOrId = "test.exe", - DumpFileName = "v1_fail.dmp", - FailFileName = "v1_fail.json", - WorkModel = "Normal", - ExtendedField = "1.0.0", - }; - - Assert.Equal("C:\\app", param.TargetPath); - Assert.Equal("C:\\app\\fail", param.FailDirectory); - Assert.Equal("C:\\app\\backup", param.BackupDirectory); - Assert.Equal("test.exe", param.ProcessNameOrId); - Assert.Equal("v1_fail.dmp", param.DumpFileName); - Assert.Equal("v1_fail.json", param.FailFileName); - Assert.Equal("Normal", param.WorkModel); - Assert.Equal("1.0.0", param.ExtendedField); - } - - [Fact] - public void WorkModel为Upgrade_保留Upgrade() - { - var param = new MonitorParameter { WorkModel = "Upgrade" }; - Assert.Equal("Upgrade", param.WorkModel); - } - - [Fact] - public void WorkModel为null_允许null() - { - var param = new MonitorParameter { WorkModel = null! }; - Assert.Null(param.WorkModel); - } - - [Fact] - public void WorkModel为空字符串_允许空字符串() - { - var param = new MonitorParameter { WorkModel = string.Empty }; - Assert.Equal(string.Empty, param.WorkModel); - } - - [Fact] - public void ExtendedField为版本号_正确返回() - { - var param = new MonitorParameter { ExtendedField = "10.0.0-preview.1" }; - Assert.Equal("10.0.0-preview.1", param.ExtendedField); - } -} diff --git a/src/c#/BowlTest/Strategys/WindowStrategyTests.cs b/src/c#/BowlTest/Strategys/WindowStrategyTests.cs deleted file mode 100644 index 588c91ff..00000000 --- a/src/c#/BowlTest/Strategys/WindowStrategyTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using GeneralUpdate.Bowl.Strategys; - -/// -/// 分支覆盖点: -/// WindowStrategy (Obsolete): -/// - Launch(): -/// - 初始化 Actions 管道:CreateCrash, Export, Restore, SetEnvironment -/// - 根据 OS 架构选择 procdump 可执行文件: -/// - X86 → "procdump.exe" -/// - X64 → "procdump64.exe" -/// - 其他 → "procdump64a.exe" -/// - InnerArguments 格式:-e -ma {ProcessNameOrId} {dmpFullName} -/// - 调用 base.Launch()(启动进程) -/// - ExecuteFinalTreatment(): -/// - dump 文件存在 → 执行所有 actions -/// - dump 文件不存在 → 跳过 actions -/// - GetAppName(): -/// - X86 → "procdump.exe" -/// - X64 → "procdump64.exe" -/// - 其他 → "procdump64a.exe" -/// - CreateCrash():序列化 Crash 到 JSON -/// - Export(): -/// - export.bat 存在 → 启动 -/// - export.bat 不存在 → 抛出 FileNotFoundException -/// - Restore(): -/// - WorkModel="Upgrade" → 执行恢复 -/// - WorkModel!="Upgrade" → 跳过 -/// - SetEnvironment(): -/// - WorkModel="Upgrade" → 设置环境变量 -/// - WorkModel!="Upgrade" → 跳过 -/// -public class WindowStrategyTests : IDisposable -{ - private readonly string _tempDir; - - public WindowStrategyTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"BowlTest_Win_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - } - - public void Dispose() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, recursive: true); - } - - [Fact] - public void 构造实例_不抛出异常() - { - var ex = Record.Exception(() => new WindowStrategy()); - Assert.Null(ex); - } - - [Fact] - public void SetParameter_设置有效参数_不抛出异常() - { - var strategy = new WindowStrategy(); - var param = new MonitorParameter - { - ProcessNameOrId = "test.exe", - DumpFileName = "crash.dmp", - FailFileName = "crash.json", - TargetPath = _tempDir, - FailDirectory = Path.Combine(_tempDir, "fail"), - BackupDirectory = Path.Combine(_tempDir, "backup"), - WorkModel = "Upgrade", - ExtendedField = "1.0.0", - }; - - var ex = Record.Exception(() => strategy.SetParameter(param)); - Assert.Null(ex); - } - - [Fact] - public void Launch_使用简单命令_不抛出未处理异常() - { - var strategy = new WindowStrategy(); - var failDir = Path.Combine(_tempDir, "win_fail"); - var param = new MonitorParameter - { - ProcessNameOrId = "test_process", - InnerApp = "cmd.exe", - InnerArguments = "/c exit 0", - DumpFileName = "crash.dmp", - FailFileName = "crash.json", - TargetPath = _tempDir, - FailDirectory = failDir, - BackupDirectory = Path.Combine(_tempDir, "backup"), - WorkModel = "Normal", - ExtendedField = "1.0.0", - }; - strategy.SetParameter(param); - - // WindowStrategy.Launch will try to run procdump which may not exist - // The test verifies graceful handling - try - { - strategy.Launch(); - } - catch (Exception) - { - // Acceptable on test machines without procdump - } - } -} diff --git a/src/c#/BowlTest/Utilities/TestFakes.cs b/src/c#/BowlTest/Utilities/TestFakes.cs index 0441877c..9a3ad302 100644 --- a/src/c#/BowlTest/Utilities/TestFakes.cs +++ b/src/c#/BowlTest/Utilities/TestFakes.cs @@ -61,29 +61,3 @@ public Task ExportAsync(string outputDirectory, CancellationToken ct) return Task.CompletedTask; } } - -internal class FakeEnvironmentProvider : IEnvironmentProvider -{ - private readonly Dictionary _variables = new(); - public bool SetVariableCalled { get; private set; } - public string? LastSetName { get; private set; } - public string? LastSetValue { get; private set; } - public Exception? SetVariableException { get; set; } - - public string? GetVariable(string name) - => _variables.TryGetValue(name, out var val) ? val : null; - - public void SetVariable(string name, string value) - { - SetVariableCalled = true; - LastSetName = name; - LastSetValue = value; - if (SetVariableException != null) throw SetVariableException; - _variables[name] = value; - } - - public void PreSetVariable(string name, string? value) - { - _variables[name] = value; - } -} diff --git a/src/c#/GeneralUpdate.Bowl/Bowl.cs b/src/c#/GeneralUpdate.Bowl/Bowl.cs index 7e601498..ad511f27 100644 --- a/src/c#/GeneralUpdate.Bowl/Bowl.cs +++ b/src/c#/GeneralUpdate.Bowl/Bowl.cs @@ -1,14 +1,10 @@ using System; using System.IO; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Bowl.Internal; using GeneralUpdate.Bowl.Strategies; -using GeneralUpdate.Bowl.Strategys; using GeneralUpdate.Bowl.FileSystem; -using GeneralUpdate.Bowl.Configuration; -using GeneralUpdate.Bowl.Ipc; namespace GeneralUpdate.Bowl; @@ -23,7 +19,6 @@ public sealed class Bowl private readonly IBowlStrategy _strategy; private readonly ICrashReporter _crashReporter; private readonly ISystemInfoProvider _systemInfoProvider; - private readonly IEnvironmentProvider _env; // ---- Constructors ---- @@ -34,8 +29,7 @@ public Bowl() : this( StrategyFactory.Create(), new CrashReporter(), - SystemInfoProviderFactory.Create(), - new EnvironmentProvider()) + SystemInfoProviderFactory.Create()) { } /// @@ -44,13 +38,11 @@ public Bowl() internal Bowl( IBowlStrategy strategy, ICrashReporter crashReporter, - ISystemInfoProvider systemInfoProvider, - IEnvironmentProvider env) + ISystemInfoProvider systemInfoProvider) { _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); _crashReporter = crashReporter ?? throw new ArgumentNullException(nameof(crashReporter)); _systemInfoProvider = systemInfoProvider ?? throw new ArgumentNullException(nameof(systemInfoProvider)); - _env = env ?? throw new ArgumentNullException(nameof(env)); } // ---- Public Async API ---- @@ -175,25 +167,7 @@ private async Task HandleCrashAsync( } } - // 4. Mark failed version to prevent re-upgrading to it - if (context.WorkModel == "Upgrade") - { - try - { - _env.SetVariable("UpgradeFail", context.ExtendedField); - GeneralTracer.Warn($"Bowl.HandleCrashAsync: UpgradeFail set to '{context.ExtendedField}'."); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - GeneralTracer.Error("Bowl.HandleCrashAsync: failed to set UpgradeFail env var.", ex); - } - } - - // 5. Platform-specific post-processing + // 4. Platform-specific post-processing try { await _strategy.PostProcessAsync(context, exitResult, ct); @@ -207,30 +181,15 @@ private async Task HandleCrashAsync( GeneralTracer.Error("Bowl.HandleCrashAsync: post-process failed.", ex); } - // 6. Invoke crash callback + // 5. Invoke crash callback if (context.OnCrash != null) { try { - // Only invoke callback if we have something to report - if (crashReportPath == null) - { - GeneralTracer.Warn("Bowl.HandleCrashAsync: skipping OnCrash callback — no crash report generated."); - return new BowlResult - { - Success = false, - ExitCode = exitResult.ExitCode, - DumpCaptured = true, - DumpFilePath = dumpPath, - CrashReportPath = null, - Restored = restored, - }; - } - var crashInfo = new CrashInfo { DumpFilePath = dumpPath, - CrashReportPath = crashReportPath!, + CrashReportPath = crashReportPath ?? string.Empty, Version = context.ExtendedField, ExitCode = exitResult.ExitCode, }; @@ -257,68 +216,6 @@ private async Task HandleCrashAsync( }; } - // ---- Backward Compatibility ---- - - /// - /// Converts legacy to the new . - /// - public static BowlContext MapToContext(MonitorParameter p) - { - return new BowlContext - { - ProcessNameOrId = p.ProcessNameOrId, - DumpFileName = p.DumpFileName, - FailFileName = p.FailFileName, - TargetPath = p.TargetPath, - FailDirectory = p.FailDirectory, - BackupDirectory = p.BackupDirectory, - WorkModel = p.WorkModel, - ExtendedField = p.ExtendedField, - TimeoutMs = 30_000, - DumpType = DumpType.Full, - AutoRestore = true, - }; - } - - /// - /// Reads ProcessInfo environment variable and builds a legacy . - /// Shared with the legacy static API for backward compatibility. - /// - internal static MonitorParameter CreateParameter() - { - GeneralTracer.Info("Bowl.CreateParameter: reading ProcessInfo from environment variable."); - - var json = Environments.GetEnvironmentVariable("ProcessInfo"); - if (string.IsNullOrWhiteSpace(json)) - { - GeneralTracer.Fatal("Bowl.CreateParameter: ProcessInfo environment variable is not set."); - throw new ArgumentNullException( - "ProcessInfo environment variable not set."); - } - - var processInfo = JsonSerializer.Deserialize(json); - if (processInfo == null) - { - GeneralTracer.Fatal("Bowl.CreateParameter: failed to deserialize ProcessInfo JSON."); - throw new ArgumentNullException( - "ProcessInfo JSON deserialization failed."); - } - - GeneralTracer.Info( - $"Bowl.CreateParameter: AppName={processInfo.AppName}, Version={processInfo.LastVersion}"); - - return new MonitorParameter - { - ProcessNameOrId = processInfo.AppName, - DumpFileName = $"{processInfo.LastVersion}_fail.dmp", - FailFileName = $"{processInfo.LastVersion}_fail.json", - TargetPath = processInfo.InstallPath, - FailDirectory = Path.Combine(processInfo.InstallPath, "fail", processInfo.LastVersion), - BackupDirectory = Path.Combine(processInfo.InstallPath, processInfo.LastVersion), - ExtendedField = processInfo.LastVersion, - }; - } - // ---- Helpers ---- private static string? FindDumpFile(BowlContext context) diff --git a/src/c#/GeneralUpdate.Bowl/BowlContext.cs b/src/c#/GeneralUpdate.Bowl/BowlContext.cs index 41377b71..0eae0ede 100644 --- a/src/c#/GeneralUpdate.Bowl/BowlContext.cs +++ b/src/c#/GeneralUpdate.Bowl/BowlContext.cs @@ -6,7 +6,6 @@ namespace GeneralUpdate.Bowl; /// /// Immutable execution context for Bowl surveillance. -/// Replaces the mutable . /// public readonly record struct BowlContext { diff --git a/src/c#/GeneralUpdate.Bowl/Configuration/ProcessContract.cs b/src/c#/GeneralUpdate.Bowl/Configuration/ProcessContract.cs deleted file mode 100644 index ec83805f..00000000 --- a/src/c#/GeneralUpdate.Bowl/Configuration/ProcessContract.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GeneralUpdate.Bowl.Configuration; - -/// -/// Minimal ProcessContract for Bowl — only the fields needed for crash monitoring and rollback. -/// -public class ProcessContract -{ - [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 index 3e028d86..3a270fca 100644 --- a/src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs +++ b/src/c#/GeneralUpdate.Bowl/FileSystem/StorageHelper.cs @@ -7,7 +7,7 @@ namespace GeneralUpdate.Bowl.FileSystem; /// /// Minimal file system utilities for Bowl (backup restore, directory cleanup, JSON serialization). /// -public static class StorageHelper +internal static class StorageHelper { public static void Restore(string backupPath, string sourcePath) { diff --git a/src/c#/GeneralUpdate.Bowl/Internal/Crash.cs b/src/c#/GeneralUpdate.Bowl/Internal/Crash.cs index 2b783c11..2d036521 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/Crash.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/Crash.cs @@ -1,11 +1,37 @@ -using System.Collections.Generic; -using GeneralUpdate.Bowl.Strategys; +using System.Collections.Generic; namespace GeneralUpdate.Bowl.Internal; +/// +/// Crash report data transfer object. +/// Serialized to JSON as the fail report when a crash is detected. +/// internal class Crash { - public MonitorParameter Parameter { get; set; } - - public List ProcdumpOutPutLines { get; set; } -} \ No newline at end of file + /// Application install root path. + public string? TargetPath { get; set; } + + /// Directory for failure artifacts. + public string? FailDirectory { get; set; } + + /// Backup directory path. + public string? BackupDirectory { get; set; } + + /// The name or PID of the monitored process. + public string? ProcessNameOrId { get; set; } + + /// Dump file name. + public string? DumpFileName { get; set; } + + /// Crash report file name. + public string? FailFileName { get; set; } + + /// Work mode: "Upgrade" or "Normal". + public string? WorkModel { get; set; } + + /// Extended field, typically the version number. + public string? ExtendedField { get; set; } + + /// Captured stdout/stderr lines from the procdump child process. + public List ProcdumpOutPutLines { get; set; } = new(); +} diff --git a/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs b/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs index ce258c8a..f64c2735 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/CrashReporter.cs @@ -4,13 +4,11 @@ using System.Threading; using System.Threading.Tasks; using GeneralUpdate.Bowl.FileSystem; -using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Internal; /// /// Default crash reporter that serializes a record to JSON. -/// Replaces the inline CreateCrash() method in the old WindowStrategy. /// internal sealed class CrashReporter : ICrashReporter { @@ -23,17 +21,14 @@ public Task GenerateReportAsync( var crash = new Crash { - Parameter = new Strategys.MonitorParameter - { - TargetPath = context.TargetPath, - FailDirectory = context.FailDirectory, - BackupDirectory = context.BackupDirectory, - ProcessNameOrId = context.ProcessNameOrId, - DumpFileName = context.DumpFileName, - FailFileName = context.FailFileName, - WorkModel = context.WorkModel, - ExtendedField = context.ExtendedField, - }, + TargetPath = context.TargetPath, + FailDirectory = context.FailDirectory, + BackupDirectory = context.BackupDirectory, + ProcessNameOrId = context.ProcessNameOrId, + DumpFileName = context.DumpFileName, + FailFileName = context.FailFileName, + WorkModel = context.WorkModel, + ExtendedField = context.ExtendedField, ProcdumpOutPutLines = new List(outputLines), }; diff --git a/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs b/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs deleted file mode 100644 index 2b4ff58b..00000000 --- a/src/c#/GeneralUpdate.Bowl/Internal/EnvironmentProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Bowl.Configuration; -using GeneralUpdate.Bowl.Ipc; - -namespace GeneralUpdate.Bowl.Internal; - -/// -/// Environment variable accessor. Abstracts away static environment APIs for testability. -/// -internal interface IEnvironmentProvider -{ - /// Gets an environment variable value. Returns null if not set. - string? GetVariable(string name); - - /// Sets an environment variable. - void SetVariable(string name, string value); -} - -/// -/// Default environment provider backed by . -/// -internal sealed class EnvironmentProvider : IEnvironmentProvider -{ - public string? GetVariable(string name) - => Environments.GetEnvironmentVariable(name); - - public void SetVariable(string name, string value) - => Environments.SetEnvironmentVariable(name, value); -} diff --git a/src/c#/GeneralUpdate.Bowl/Internal/LinuxSystem.cs b/src/c#/GeneralUpdate.Bowl/Internal/LinuxSystem.cs deleted file mode 100644 index 66903efb..00000000 --- a/src/c#/GeneralUpdate.Bowl/Internal/LinuxSystem.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace GeneralUpdate.Bowl.Internal; - -internal class LinuxSystem -{ - internal string Name { get; set; } - - internal string Version { get; set; } - - internal LinuxSystem(string name, string version) - { - Name = name; - Version = version; - } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Internal/SystemInfoProviderFactory.cs b/src/c#/GeneralUpdate.Bowl/Internal/SystemInfoProviderFactory.cs index 37926833..8fa5a77f 100644 --- a/src/c#/GeneralUpdate.Bowl/Internal/SystemInfoProviderFactory.cs +++ b/src/c#/GeneralUpdate.Bowl/Internal/SystemInfoProviderFactory.cs @@ -12,7 +12,7 @@ public static ISystemInfoProvider Create() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new WindowsSystemInfoProvider(); - // Linux and macOS: no-op for now (could export journalctl / syslog in future) + // Linux: no-op for now (could export journalctl / syslog in future) return new NoOpSystemInfoProvider(); } diff --git a/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs b/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs deleted file mode 100644 index 0f1a82b6..00000000 --- a/src/c#/GeneralUpdate.Bowl/Ipc/Environments.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index f5509c67..00000000 --- a/src/c#/GeneralUpdate.Bowl/Ipc/IpcEncryption.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 e7b55322..811d356a 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs @@ -4,15 +4,12 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using GeneralUpdate.Bowl.Internal; using GeneralUpdate.Bowl; namespace GeneralUpdate.Bowl.Strategies; /// /// Linux crash surveillance strategy using procdump. -/// Fixes the dead-code issue in the legacy LinuxStrategy — the strategy factory -/// now correctly creates this strategy on Linux platforms. /// internal sealed class LinuxBowlStrategy : IBowlStrategy { @@ -27,39 +24,37 @@ internal sealed class LinuxBowlStrategy : IBowlStrategy ["clearos"] = "procdump-3.3.0-0.cm2.x86_64.rpm", }; - private bool _procdumpInstalled; + private bool _probed; + private bool _procdumpAvailable; + private string? _lastFailReason; public ProcessStartInfo? Prepare(in BowlContext context) { - // Lazy install procdump if not already done - if (!_procdumpInstalled) + if (!_probed) { - var installed = TryInstallProcdump(); - if (!installed) - { - GeneralTracer.Warn( - "LinuxBowlStrategy.Prepare: procdump installation failed on this system."); - _procdumpInstalled = false; - // Don't throw; return null signals "tool unavailable" to Bowl (graceful degradation) - } - else - { - _procdumpInstalled = true; - } + _procdumpAvailable = ProbeProcdump(context.FailDirectory); + _probed = true; } - if (!_procdumpInstalled) + if (!_procdumpAvailable) return null; var dumpFullPath = Path.Combine(context.FailDirectory, context.DumpFileName); EnsureDirectory(context.FailDirectory); - GeneralTracer.Info($"LinuxBowlStrategy.Prepare: target={context.ProcessNameOrId}, dump={dumpFullPath}"); + // Detect whether the target is a PID (all digits) or a process name. + // Linux procdump uses -p for PID and -w for process name. + var isPid = long.TryParse(context.ProcessNameOrId, out _); + var flag = isPid ? "-p" : "-w"; + + GeneralTracer.Info( + $"LinuxBowlStrategy.Prepare: target='{context.ProcessNameOrId}' ({(isPid ? "PID" : "name")}), " + + $"flag={flag}, dump={dumpFullPath}"); return new ProcessStartInfo { FileName = "procdump", - Arguments = $"-p {context.ProcessNameOrId} -o \"{dumpFullPath}\"", + Arguments = $"{flag} {context.ProcessNameOrId} -o \"{dumpFullPath}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -70,19 +65,46 @@ internal sealed class LinuxBowlStrategy : IBowlStrategy public Task PostProcessAsync(in BowlContext context, ProcessExitResult exitResult, CancellationToken ct) { - // No additional Linux-specific post-processing at this time. return Task.CompletedTask; } - private static bool TryInstallProcdump() + // ---- Probe: check if procdump is available, install if not ---- + + /// + /// Returns true if procdump can be used. + /// Writes a diagnostic file when the environment is unsupported. + /// + private bool ProbeProcdump(string failDirectory) { + // 1. Already in PATH? + if (IsProcdumpInPath()) + { + GeneralTracer.Info("LinuxBowlStrategy: procdump found in PATH, skipping install."); + return true; + } + + // 2. Detect distro var distro = DetectDistro(); + if (string.IsNullOrEmpty(distro)) + { + _lastFailReason = "Cannot detect Linux distribution: /etc/os-release not found."; + WriteUnsupportedHint(failDirectory, _lastFailReason); + GeneralTracer.Warn($"LinuxBowlStrategy: {_lastFailReason}"); + return false; + } + + // 3. Check if we have a matching package if (!DistroPackageMap.TryGetValue(distro, out var package)) { - GeneralTracer.Warn($"LinuxBowlStrategy: unsupported distro '{distro}', cannot install procdump."); + _lastFailReason = + $"Unsupported Linux distribution: '{distro}'. " + + $"Supported distributions: {string.Join(", ", DistroPackageMap.Keys)}."; + WriteUnsupportedHint(failDirectory, _lastFailReason); + GeneralTracer.Warn($"LinuxBowlStrategy: {_lastFailReason}"); return false; } + // 4. Locate bundled package and install script var appDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Applications", "Linux"); var scriptPath = Path.Combine(appDir, "install.sh"); @@ -90,18 +112,94 @@ private static bool TryInstallProcdump() if (!File.Exists(scriptPath)) { - GeneralTracer.Error($"LinuxBowlStrategy: install.sh not found at {scriptPath}."); + _lastFailReason = $"install.sh not found at {scriptPath}."; + WriteUnsupportedHint(failDirectory, _lastFailReason); + GeneralTracer.Error($"LinuxBowlStrategy: {_lastFailReason}"); return false; } if (!File.Exists(packagePath)) { - GeneralTracer.Error($"LinuxBowlStrategy: package not found at {packagePath}."); + _lastFailReason = $"procdump package not found at {packagePath}."; + WriteUnsupportedHint(failDirectory, _lastFailReason); + GeneralTracer.Error($"LinuxBowlStrategy: {_lastFailReason}"); return false; } - GeneralTracer.Info($"LinuxBowlStrategy: installing {package} via install.sh."); - return RunInstallScript(scriptPath, packagePath); + // 5. Run install script + GeneralTracer.Info($"LinuxBowlStrategy: installing {package} via install.sh for distro '{distro}'."); + var installed = RunInstallScript(scriptPath, packagePath); + if (!installed) + { + _lastFailReason = + $"Failed to install procdump package '{package}' for distribution '{distro}'. " + + "Check that sudo is available without an interactive password prompt."; + WriteUnsupportedHint(failDirectory, _lastFailReason); + GeneralTracer.Error($"LinuxBowlStrategy: {_lastFailReason}"); + return false; + } + + GeneralTracer.Info("LinuxBowlStrategy: procdump installed successfully."); + return true; + } + + // ---- Helpers ---- + + private static bool IsProcdumpInPath() + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = "procdump", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + process.Start(); + var exited = process.WaitForExit(5000); + if (!exited) + { + process.Kill(); + return false; + } + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static void WriteUnsupportedHint(string failDirectory, string reason) + { + try + { + Directory.CreateDirectory(failDirectory); + var hintPath = Path.Combine(failDirectory, "bowl_linux_unsupported.txt"); + var content = + $"Bowl Linux Strategy — Unsupported Environment\n" + + $"================================================\n" + + $"Reason: {reason}\n" + + $"Timestamp: {DateTime.UtcNow:O}\n" + + $"\n" + + $"Supported distributions: {string.Join(", ", DistroPackageMap.Keys)}\n" + + $"\n" + + $"To use Bowl on this system, install procdump manually and ensure it is\n" + + $"available in PATH. Bowl will skip the automatic install if procdump\n" + + $"is already present.\n"; + File.WriteAllText(hintPath, content); + GeneralTracer.Info($"LinuxBowlStrategy: unsupported hint written to {hintPath}."); + } + catch (Exception ex) + { + GeneralTracer.Error("LinuxBowlStrategy: failed to write unsupported hint.", ex); + } } private static bool RunInstallScript(string script, string package) diff --git a/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs deleted file mode 100644 index ac3f7b28..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategies/MacBowlStrategy.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Bowl; - -namespace GeneralUpdate.Bowl.Strategies; - -/// -/// macOS crash surveillance strategy. -/// Uses the built-in lldb debugger for crash capture (procdump does not support macOS). -/// -/// -/// Note: lldb requires the process being debugged to allow debugging -/// (SIP / task_for_pid restrictions). For production macOS crash capture, consider -/// using Crashpad (https://chromium.googlesource.com/crashpad/crashpad/) instead. -/// This implementation provides a basic stub that can be extended. -/// -internal sealed class MacBowlStrategy : IBowlStrategy -{ - public ProcessStartInfo? Prepare(in BowlContext context) - { - if (!IsLldbAvailable()) - { - GeneralTracer.Warn( - "MacBowlStrategy.Prepare: lldb not available. macOS crash monitoring is unavailable."); - return null; - } - - var dumpFullPath = Path.Combine(context.FailDirectory, context.DumpFileName); - EnsureDirectory(context.FailDirectory); - - // lldb batch mode: attach to process by name, save core dump, quit. - // Use separate -o arguments per command to avoid nested quoting issues. - GeneralTracer.Info($"MacBowlStrategy.Prepare: target={context.ProcessNameOrId}"); - - return new ProcessStartInfo - { - FileName = "/usr/bin/lldb", - Arguments = string.Concat( - "--batch", - $" -o \"process attach --name {context.ProcessNameOrId} --waitfor\"", - $" -o \"process save-core {dumpFullPath}\"", - " -o quit"), - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - } - - public Task PostProcessAsync(in BowlContext context, - ProcessExitResult exitResult, CancellationToken ct) - { - return Task.CompletedTask; - } - - private static bool IsLldbAvailable() - { - try - { - using var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = "/usr/bin/which", - Arguments = "lldb", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - }, - }; - process.Start(); - var exited = process.WaitForExit(3000); - if (!exited) - { - process.Kill(); - return false; - } - return process.ExitCode == 0; - } - catch - { - return false; - } - } - - private static void EnsureDirectory(string path) - { - if (Directory.Exists(path)) - Directory.Delete(path, recursive: true); - Directory.CreateDirectory(path); - } -} diff --git a/src/c#/GeneralUpdate.Bowl/Strategies/StrategyFactory.cs b/src/c#/GeneralUpdate.Bowl/Strategies/StrategyFactory.cs index 0e863c3c..a38ef0f6 100644 --- a/src/c#/GeneralUpdate.Bowl/Strategies/StrategyFactory.cs +++ b/src/c#/GeneralUpdate.Bowl/Strategies/StrategyFactory.cs @@ -21,9 +21,6 @@ public static IBowlStrategy Create() if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return new LinuxBowlStrategy(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return new MacBowlStrategy(); - throw new PlatformNotSupportedException( $"Bowl does not support the current platform: {RuntimeInformation.OSDescription}"); } diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs deleted file mode 100644 index 696e3560..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategys/AbstractStrategy.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using GeneralUpdate.Bowl.FileSystem; -using GeneralUpdate.Bowl; - -namespace GeneralUpdate.Bowl.Strategys; - -[System.Obsolete("Use Strategies.IBowlStrategy instead. Will be removed in v10.")] -internal abstract class AbstractStrategy : IStrategy -{ - protected MonitorParameter _parameter; - protected List OutputList = new (); - - public void SetParameter(MonitorParameter parameter) => _parameter = parameter; - - public virtual void Launch() - { - GeneralTracer.Info($"AbstractStrategy.Launch: starting inner application. App={_parameter.InnerApp}, Args={_parameter.InnerArguments}"); - Startup(_parameter.InnerApp, _parameter.InnerArguments); - GeneralTracer.Info("AbstractStrategy.Launch: inner application process finished."); - } - - private void Startup(string appName, string arguments) - { - GeneralTracer.Info($"AbstractStrategy.Startup: preparing process. FileName={appName}"); - if (Directory.Exists(_parameter.FailDirectory)) - { - GeneralTracer.Info($"AbstractStrategy.Startup: removing existing fail directory: {_parameter.FailDirectory}"); - StorageHelper.DeleteDirectory(_parameter.FailDirectory); - } - Directory.CreateDirectory(_parameter.FailDirectory); - GeneralTracer.Info($"AbstractStrategy.Startup: fail directory created: {_parameter.FailDirectory}"); - - var startInfo = new ProcessStartInfo - { - FileName = appName, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - var process = new Process { StartInfo = startInfo }; - process.OutputDataReceived += OutputHandler; - process.ErrorDataReceived += OutputHandler; - process.Start(); - GeneralTracer.Info($"AbstractStrategy.Startup: process started. PID={process.Id}"); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(1000 * 10); - GeneralTracer.Info($"AbstractStrategy.Startup: process exited. ExitCode={process.ExitCode}"); - } - - private void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) - { - var data = outLine.Data; - if (!string.IsNullOrEmpty(data)) - { - GeneralTracer.Debug($"AbstractStrategy.OutputHandler: {data}"); - OutputList.Add(data); - } - } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/IStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/IStrategy.cs deleted file mode 100644 index 694f9c81..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategys/IStrategy.cs +++ /dev/null @@ -1,11 +0,0 @@ -using GeneralUpdate.Bowl.Internal; - -namespace GeneralUpdate.Bowl.Strategys; - -[System.Obsolete("Use Strategies.IBowlStrategy instead. Will be removed in v10.")] -internal interface IStrategy -{ - void Launch(); - - void SetParameter(MonitorParameter parameter); -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs deleted file mode 100644 index 5244276c..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategys/LinuxStrategy.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Bowl; - -namespace GeneralUpdate.Bowl.Strategys; - -[System.Obsolete("Use Strategies.LinuxBowlStrategy instead. Will be removed in v10.")] -internal class LinuxStrategy : AbstractStrategy -{ - /*procdump-3.3.0-0.cm2.x86_64.rpm: - Compatible Systems: This RPM package may be suitable for certain CentOS or RHEL-based derivatives, specifically the CM2 version. CM2 typically refers to ClearOS 7.x or similar community-maintained versions. - - procdump-3.3.0-0.el8.x86_64.rpm: - Compatible Systems: This RPM package is suitable for Red Hat Enterprise Linux 8 (RHEL 8), CentOS 8, and other RHEL 8-based distributions. - - procdump_3.3.0_amd64.deb: - Compatible Systems: This DEB package is suitable for Debian and its derivatives, such as Ubuntu, for 64-bit systems (amd64 architecture).*/ - - private IReadOnlyList _rocdumpAmd64 = new List { "Ubuntu", "Debian" }; - private IReadOnlyList procdump_el8_x86_64 = new List { "Red Hat", "CentOS", "Fedora" }; - private IReadOnlyList procdump_cm2_x86_64 = new List { "ClearOS" }; - - public override void Launch() - { - GeneralTracer.Info("LinuxStrategy.Launch: starting Linux surveillance launch."); - try - { - Install(); - GeneralTracer.Info("LinuxStrategy.Launch: procdump installation completed, invoking base launch."); - base.Launch(); - GeneralTracer.Info("LinuxStrategy.Launch: launch lifecycle completed."); - } - catch (Exception ex) - { - GeneralTracer.Error("LinuxStrategy.Launch: exception occurred during Linux surveillance launch.", ex); - throw; - } - } - - private void Install() - { - GeneralTracer.Info("LinuxStrategy.Install: determining procdump package and running install script."); - string scriptPath = "./install.sh"; - string packageFile = GetPacketName(); - - if (string.IsNullOrEmpty(packageFile)) - { - GeneralTracer.Warn("LinuxStrategy.Install: no matching procdump package found for the current Linux distribution."); - return; - } - - GeneralTracer.Info($"LinuxStrategy.Install: executing install.sh with package={packageFile}."); - ProcessStartInfo processStartInfo = new ProcessStartInfo() - { - FileName = "/bin/bash", - Arguments = $"{scriptPath} {packageFile}", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - try - { - using Process process = Process.Start(processStartInfo); - string output = process.StandardOutput.ReadToEnd(); - string error = process.StandardError.ReadToEnd(); - process.WaitForExit(); - - if (!string.IsNullOrEmpty(output)) - GeneralTracer.Info($"LinuxStrategy.Install output: {output}"); - - if (!string.IsNullOrEmpty(error)) - GeneralTracer.Warn($"LinuxStrategy.Install error output: {error}"); - - if (process.ExitCode != 0) - { - GeneralTracer.Error($"LinuxStrategy.Install: install script exited with code {process.ExitCode}."); - } - else - { - GeneralTracer.Info("LinuxStrategy.Install: procdump installation succeeded."); - } - } - catch (Exception e) - { - GeneralTracer.Error("LinuxStrategy.Install: exception occurred while running install script.", e); - } - } - - private string GetPacketName() - { - GeneralTracer.Info("LinuxStrategy.GetPacketName: detecting Linux distribution."); - var packageFileName = string.Empty; - var system = GetSystem(); - GeneralTracer.Info($"LinuxStrategy.GetPacketName: detected distribution={system.Name}, version={system.Version}."); - if (_rocdumpAmd64.Contains(system.Name)) - { - packageFileName = $"procdump_3.3.0_amd64.deb"; - } - else if (procdump_el8_x86_64.Contains(system.Name)) - { - packageFileName = $"procdump-3.3.0-0.el8.x86_64.rpm"; - } - else if (procdump_cm2_x86_64.Contains(system.Name)) - { - packageFileName = $"procdump-3.3.0-0.cm2.x86_64.rpm"; - } - - if (string.IsNullOrEmpty(packageFileName)) - GeneralTracer.Warn($"LinuxStrategy.GetPacketName: no matching package for distribution={system.Name}."); - else - GeneralTracer.Info($"LinuxStrategy.GetPacketName: resolved package={packageFileName}."); - - return packageFileName; - } - - private LinuxSystem GetSystem() - { - GeneralTracer.Info("LinuxStrategy.GetSystem: reading /etc/os-release."); - string osReleaseFile = "/etc/os-release"; - if (File.Exists(osReleaseFile)) - { - var lines = File.ReadAllLines(osReleaseFile); - string distro = string.Empty; - string version = string.Empty; - - foreach (var line in lines) - { - if (line.StartsWith("ID=")) - { - distro = line.Substring(3).Trim('\"'); - } - else if (line.StartsWith("VERSION_ID=")) - { - version = line.Substring(11).Trim('\"'); - } - } - - GeneralTracer.Info($"LinuxStrategy.GetSystem: distro={distro}, version={version}."); - return new LinuxSystem(distro, version); - } - - GeneralTracer.Fatal("LinuxStrategy.GetSystem: /etc/os-release not found, cannot determine Linux distribution."); - throw new FileNotFoundException("Cannot determine the Linux distribution. The /etc/os-release file does not exist."); - } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/MonitorParameter.cs b/src/c#/GeneralUpdate.Bowl/Strategys/MonitorParameter.cs deleted file mode 100644 index b90e7675..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategys/MonitorParameter.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace GeneralUpdate.Bowl.Strategys; - -[System.Obsolete("Use BowlContext instead. Will be removed in v10.")] -public class MonitorParameter -{ - public MonitorParameter() { } - - public string TargetPath { get; set; } - - public string FailDirectory { get; set; } - - public string BackupDirectory { get; set; } - - public string ProcessNameOrId { get; set; } - - public string DumpFileName { get; set; } - - public string FailFileName { get; set; } - - internal string InnerArguments { get; set; } - - internal string InnerApp { get; set; } - - /// - /// Upgrade: upgrade mode. This mode is primarily used in conjunction with GeneralUpdate for internal use. Please do not modify it arbitrarily when the default mode is activated. - /// Normal: Normal mode,This mode can be used independently to monitor a single program. If the program crashes, it will export the crash information. - /// - public string WorkModel { get; set; } = "Upgrade"; - - public string ExtendedField { get; set; } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs b/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs deleted file mode 100644 index f57a3da4..00000000 --- a/src/c#/GeneralUpdate.Bowl/Strategys/WindowStrategy.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using GeneralUpdate.Bowl.Internal; -using GeneralUpdate.Bowl.FileSystem; -using GeneralUpdate.Bowl.Configuration; -using GeneralUpdate.Bowl.Ipc; -using GeneralUpdate.Bowl; - -namespace GeneralUpdate.Bowl.Strategys; - -[System.Obsolete("Use Strategies.WindowsBowlStrategy instead. Will be removed in v10.")] -internal class WindowStrategy : AbstractStrategy -{ - private const string WorkModel = "Upgrade"; - private string? _applicationsDirectory; - private List _actions = new(); - - public override void Launch() - { - GeneralTracer.Info("WindowStrategy.Launch: initializing actions pipeline."); - InitializeActions(); - _applicationsDirectory = Path.Combine(_parameter.TargetPath, "Applications", "Windows"); - _parameter.InnerApp = Path.Combine(_applicationsDirectory, GetAppName()); - var dmpFullName = Path.Combine(_parameter.FailDirectory, _parameter.DumpFileName); - _parameter.InnerArguments = $"-e -ma {_parameter.ProcessNameOrId} {dmpFullName}"; - GeneralTracer.Info($"WindowStrategy.Launch: launching inner app={_parameter.InnerApp}, dumpFile={dmpFullName}."); - //This method is used to launch scripts in applications. - base.Launch(); - GeneralTracer.Info("WindowStrategy.Launch: base launch completed, executing final treatment."); - ExecuteFinalTreatment(); - GeneralTracer.Info("WindowStrategy.Launch: launch lifecycle completed."); - } - - private string GetAppName() - { - var name = RuntimeInformation.OSArchitecture switch - { - Architecture.X86 => "procdump.exe", - Architecture.X64 => "procdump64.exe", - _ => "procdump64a.exe" - }; - GeneralTracer.Info($"WindowStrategy.GetAppName: resolved procdump executable={name} for arch={RuntimeInformation.OSArchitecture}."); - return name; - } - - private void ExecuteFinalTreatment() - { - var dumpFile = Path.Combine(_parameter.FailDirectory, _parameter.DumpFileName); - GeneralTracer.Info($"WindowStrategy.ExecuteFinalTreatment: checking for dump file at {dumpFile}."); - if (File.Exists(dumpFile)) - { - GeneralTracer.Info($"WindowStrategy.ExecuteFinalTreatment: dump file found, executing {_actions.Count} remediation action(s)."); - foreach (var action in _actions) - { - action.Invoke(); - } - GeneralTracer.Info("WindowStrategy.ExecuteFinalTreatment: all remediation actions completed."); - } - else - { - GeneralTracer.Info("WindowStrategy.ExecuteFinalTreatment: no dump file found, monitored process exited normally."); - } - } - - private void InitializeActions() - { - _actions.Add(CreateCrash); - _actions.Add(Export); - _actions.Add(Restore); - _actions.Add(SetEnvironment); - GeneralTracer.Debug("WindowStrategy.InitializeActions: registered actions: CreateCrash, Export, Restore, SetEnvironment."); - } - - /// - /// Export the crash output information from procdump.exe and the monitoring parameters of Bowl. - /// - private void CreateCrash() - { - GeneralTracer.Info("WindowStrategy.CreateCrash: serializing crash report."); - var crash = new Crash - { - Parameter = _parameter, - ProcdumpOutPutLines = OutputList - }; - var failJsonPath = Path.Combine(_parameter.FailDirectory, _parameter.FailFileName); - StorageHelper.CreateJson(failJsonPath, crash); - GeneralTracer.Info($"WindowStrategy.CreateCrash: crash report written to {failJsonPath}."); - } - - /// - /// Export operating system information, system logs, and system driver information. - /// - private void Export() - { - GeneralTracer.Info("WindowStrategy.Export: exporting OS and system diagnostic information."); - var batPath = Path.Combine(_applicationsDirectory, "export.bat"); - if (!File.Exists(batPath)) - { - GeneralTracer.Error($"WindowStrategy.Export: export.bat not found at {batPath}."); - throw new FileNotFoundException("export.bat not found!"); - } - - Process.Start(batPath, _parameter.FailDirectory); - GeneralTracer.Info($"WindowStrategy.Export: export.bat started targeting {_parameter.FailDirectory}."); - } - - /// - /// Within the GeneralUpdate upgrade system, restore the specified backup version files to the current working directory. - /// - private void Restore() - { - GeneralTracer.Info($"WindowStrategy.Restore: checking work model. CurrentModel={_parameter.WorkModel}, ExpectedModel={WorkModel}."); - if (string.Equals(_parameter.WorkModel, WorkModel)) - { - GeneralTracer.Info($"WindowStrategy.Restore: restoring backup from {_parameter.BackupDirectory} to {_parameter.TargetPath}."); - StorageHelper.Restore(_parameter.BackupDirectory, _parameter.TargetPath); - GeneralTracer.Info("WindowStrategy.Restore: restore completed successfully."); - } - else - { - GeneralTracer.Info("WindowStrategy.Restore: restore skipped, work model is not Upgrade."); - } - } - - /// - /// Write the failed update version number to the local environment variable. - /// - private void SetEnvironment() - { - if (!string.Equals(_parameter.WorkModel, WorkModel)) - { - GeneralTracer.Info("WindowStrategy.SetEnvironment: skipped, work model is not Upgrade."); - return; - } - - /* - * The `UpgradeFail` environment variable is used to mark an exception version number during updates. - * If the latest version number obtained via an HTTP request is less than or equal to the exception version number, the update is skipped. - * Once this version number is set, it will not be removed, and updates will not proceed until a version greater than the exception version number is obtained through the HTTP request. - */ - Environments.SetEnvironmentVariable("UpgradeFail", _parameter.ExtendedField); - GeneralTracer.Warn($"WindowStrategy.SetEnvironment: UpgradeFail environment variable set to version={_parameter.ExtendedField}."); - } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs b/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs index bd513e72..ddf4a46e 100644 --- a/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs +++ b/src/c#/GeneralUpdate.Bowl/Tracer/TextTraceListener.cs @@ -4,7 +4,9 @@ using System.Collections.Concurrent; using System.Threading; -public class TextTraceListener : TraceListener, IDisposable +namespace GeneralUpdate.Bowl; + +internal class TextTraceListener : TraceListener, IDisposable { private readonly string _filePath; private readonly BlockingCollection _messageQueue; diff --git a/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs b/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs index 0dc55624..fe4c397b 100644 --- a/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs +++ b/src/c#/GeneralUpdate.Bowl/Tracer/WindowsOutputDebugListener.cs @@ -4,7 +4,7 @@ namespace GeneralUpdate.Bowl; -public class WindowsOutputDebugListener : TraceListener +internal class WindowsOutputDebugListener : TraceListener { /// /// Does not affect .NET AOT compilation and runtime on the Windows platform, provided that the following conditions are met: diff --git a/tests/BowlTest/Integration/BowlCrashPipelineTests.cs b/tests/BowlTest/Integration/BowlCrashPipelineTests.cs index bd5d169b..4520be91 100644 --- a/tests/BowlTest/Integration/BowlCrashPipelineTests.cs +++ b/tests/BowlTest/Integration/BowlCrashPipelineTests.cs @@ -38,7 +38,7 @@ public void Dispose() } /// - /// Simulates a full crash ?dump ?report ?callback pipeline. + /// Simulates a full crash �?dump �?report �?callback pipeline. /// Produces real files that can be inspected on disk. /// [Fact] @@ -206,7 +206,7 @@ public async Task SimulatedCrash_NormalMode_DoesNotRestore() var bowl = CreateBowl(mockStrategy); var result = await bowl.LaunchAsync(context); - _output.WriteLine($"Normal mode ?Restored: {result.Restored}, DumpCaptured: {result.DumpCaptured}"); + _output.WriteLine($"Normal mode �?Restored: {result.Restored}, DumpCaptured: {result.DumpCaptured}"); Assert.True(result.DumpCaptured); Assert.False(result.Restored, "Normal mode should NOT restore backup"); @@ -242,7 +242,7 @@ public async Task NoCrash_NoDumpFile_ReturnsSuccess() var bowl = CreateBowl(mockStrategy); var result = await bowl.LaunchAsync(context); - _output.WriteLine($"Healthy ?Success: {result.Success}, DumpCaptured: {result.DumpCaptured}"); + _output.WriteLine($"Healthy �?Success: {result.Success}, DumpCaptured: {result.DumpCaptured}"); Assert.False(result.DumpCaptured, "No dump should be captured for healthy process"); Assert.Equal(0, result.ExitCode); @@ -253,7 +253,7 @@ public async Task NoCrash_NoDumpFile_ReturnsSuccess() private static Bowl CreateBowl(IBowlStrategy strategy) { return new Bowl(strategy, new CrashReporter(), - new NoOpInfoProvider(), new MockEnvironmentProvider()); + new NoOpInfoProvider()); } /// @@ -325,15 +325,5 @@ public Task ExportAsync(string outputDirectory, CancellationToken ct) => Task.CompletedTask; } - private sealed class MockEnvironmentProvider : IEnvironmentProvider - { - private readonly Dictionary _vars = new(); - - public string? GetVariable(string name) - => _vars.TryGetValue(name, out var v) ? v : null; - - public void SetVariable(string name, string value) - => _vars[name] = value; - } } } diff --git a/tests/BowlTest/Integration/BowlIntegrationTests.cs b/tests/BowlTest/Integration/BowlIntegrationTests.cs index 4f0d1af8..caccd8e8 100644 --- a/tests/BowlTest/Integration/BowlIntegrationTests.cs +++ b/tests/BowlTest/Integration/BowlIntegrationTests.cs @@ -1,9 +1,6 @@ using System; using System.IO; -using System.Runtime.InteropServices; -using System.Text.Json; using GeneralUpdate.Bowl; -using GeneralUpdate.Bowl.Strategys; namespace BowlTest.Integration { @@ -37,118 +34,95 @@ public void Dispose() } /// - /// Tests Normal mode vs Upgrade mode behavior difference. + /// Tests Normal mode vs Upgrade mode behavior difference via BowlContext. /// [Fact] public void WorkModel_DifferentiatesBetweenNormalAndUpgradeMode() { - // Arrange - var normalParameter = new MonitorParameter - { - WorkModel = "Normal" - }; - - var upgradeParameter = new MonitorParameter - { - WorkModel = "Upgrade" - }; + var normalCtx = new BowlContext { WorkModel = "Normal" }; + var upgradeCtx = new BowlContext { WorkModel = "Upgrade" }; - // Assert - Assert.Equal("Normal", normalParameter.WorkModel); - Assert.Equal("Upgrade", upgradeParameter.WorkModel); - Assert.NotEqual(normalParameter.WorkModel, upgradeParameter.WorkModel); + Assert.Equal("Normal", normalCtx.WorkModel); + Assert.Equal("Upgrade", upgradeCtx.WorkModel); + Assert.NotEqual(normalCtx.WorkModel, upgradeCtx.WorkModel); } /// - /// Tests that parameter paths are correctly constructed from ProcessContract. + /// Tests that fail and backup directory paths are constructed correctly from install path and version. /// [Fact] - public void ParameterConstruction_FromProcessContract_CreatesCorrectPaths() + public void BowlContext_PathConstruction_CreatesCorrectPaths() { - // Arrange var installPath = "/path/to/install"; var version = "3.2.1"; - // Expected paths based on CreateParameter logic in Bowl.cs var expectedFailDir = Path.Combine(installPath, "fail", version); var expectedBackupDir = Path.Combine(installPath, version); var expectedDumpFile = $"{version}_fail.dmp"; var expectedFailFile = $"{version}_fail.json"; - // Act - Create parameter manually with same logic - var parameter = new MonitorParameter + var ctx = new BowlContext { TargetPath = installPath, FailDirectory = expectedFailDir, BackupDirectory = expectedBackupDir, DumpFileName = expectedDumpFile, FailFileName = expectedFailFile, - ExtendedField = version + ExtendedField = version, }; - // Assert - Assert.Equal(expectedFailDir, parameter.FailDirectory); - Assert.Equal(expectedBackupDir, parameter.BackupDirectory); - Assert.Equal(expectedDumpFile, parameter.DumpFileName); - Assert.Equal(expectedFailFile, parameter.FailFileName); - Assert.Equal(version, parameter.ExtendedField); - Assert.Contains(version, parameter.FailDirectory); - Assert.Contains(version, parameter.BackupDirectory); + Assert.Equal(expectedFailDir, ctx.FailDirectory); + Assert.Equal(expectedBackupDir, ctx.BackupDirectory); + Assert.Equal(expectedDumpFile, ctx.DumpFileName); + Assert.Equal(expectedFailFile, ctx.FailFileName); + Assert.Equal(version, ctx.ExtendedField); + Assert.Contains(version, ctx.FailDirectory); + Assert.Contains(version, ctx.BackupDirectory); } /// - /// Tests that extended field can store version information. + /// Tests that ExtendedField can store version information. /// [Fact] - public void ExtendedField_StoresVersionEntryrmation() + public void ExtendedField_StoresVersionInformation() { - // Arrange var versions = new[] { "1.0.0", "2.1.3", "10.5.2-beta" }; foreach (var version in versions) { - // Act - var parameter = new MonitorParameter - { - ExtendedField = version - }; - - // Assert - Assert.Equal(version, parameter.ExtendedField); + var ctx = new BowlContext { ExtendedField = version }; + Assert.Equal(version, ctx.ExtendedField); } } /// - /// Tests that ProcessContract JSON with all required fields parses correctly. + /// Tests Normalize applies expected defaults. /// [Fact] - public void ProcessContractJson_WithAllFields_ParsesCorrectly() + public void Normalize_AppliesDefaults_WorkModelTimeoutAndDumpType() { - // Arrange - var json = @"{ - ""AppName"": ""MyApp.exe"", - ""InstallPath"": ""/path/to/app"", - ""LastVersion"": ""1.2.3"" - }"; - - // Act - var processInfo = JsonSerializer.Deserialize(json); + var ctx = new BowlContext { ProcessNameOrId = "test.exe" }; + var normalized = ctx.Normalize(); - // Assert - Assert.NotNull(processInfo); - Assert.Equal("MyApp.exe", processInfo.AppName); - Assert.Equal("/path/to/app", processInfo.InstallPath); - Assert.Equal("1.2.3", processInfo.LastVersion); + Assert.Equal("Upgrade", normalized.WorkModel); + Assert.Equal(30_000, normalized.TimeoutMs); + Assert.Equal(DumpType.Full, normalized.DumpType); } /// - /// Helper class for ProcessContract JSON deserialization testing. + /// Tests that explicit WorkModel "Normal" is preserved after Normalize. /// - private class ProcessContractDto + [Fact] + public void Normalize_PreservesExplicitNormalWorkModel() { - public string? AppName { get; set; } - public string? InstallPath { get; set; } - public string? LastVersion { get; set; } + var ctx = new BowlContext + { + ProcessNameOrId = "test.exe", + WorkModel = "Normal", + }; + var normalized = ctx.Normalize(); + + Assert.Equal("Normal", normalized.WorkModel); } } } diff --git a/tests/BowlTest/Strategies/BowlAsyncTests.cs b/tests/BowlTest/Strategies/BowlAsyncTests.cs index fd42fa70..849bc7c7 100644 --- a/tests/BowlTest/Strategies/BowlAsyncTests.cs +++ b/tests/BowlTest/Strategies/BowlAsyncTests.cs @@ -20,7 +20,7 @@ public void Bowl_Constructor_DoesNotThrow() } /// - /// LaunchAsync with non-existent procdump path ?either throws Win32Exception + /// LaunchAsync with non-existent procdump path �?either throws Win32Exception /// (file not found) or returns a result with no dump captured. /// [Fact] @@ -53,37 +53,5 @@ public async Task LaunchAsync_WithInvalidAppPath_Throws() } } - /// - /// MapToContext correctly translates old MonitorParameter to new BowlContext. - /// - [Fact] - public void MapToContext_TranslatesAllFields() - { - var old = new GeneralUpdate.Bowl.Strategys.MonitorParameter - { - ProcessNameOrId = "app.exe", - DumpFileName = "crash.dmp", - FailFileName = "crash.json", - TargetPath = "/install", - FailDirectory = "/install/fail/1.0", - BackupDirectory = "/install/1.0", - WorkModel = "Normal", - ExtendedField = "2.0.0", - }; - - var ctx = Bowl.MapToContext(old); - - Assert.Equal("app.exe", ctx.ProcessNameOrId); - Assert.Equal("crash.dmp", ctx.DumpFileName); - Assert.Equal("crash.json", ctx.FailFileName); - Assert.Equal("/install", ctx.TargetPath); - Assert.Equal("/install/fail/1.0", ctx.FailDirectory); - Assert.Equal("/install/1.0", ctx.BackupDirectory); - Assert.Equal("Normal", ctx.WorkModel); - Assert.Equal("2.0.0", ctx.ExtendedField); - Assert.Equal(30_000, ctx.TimeoutMs); - Assert.Equal(DumpType.Full, ctx.DumpType); - Assert.True(ctx.AutoRestore); - } } } diff --git a/tests/BowlTest/Strategys/AbstractStrategyTests.cs b/tests/BowlTest/Strategys/AbstractStrategyTests.cs deleted file mode 100644 index 44781715..00000000 --- a/tests/BowlTest/Strategys/AbstractStrategyTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using GeneralUpdate.Bowl.Strategys; - -namespace BowlTest.Strategys -{ - /// - /// Contains test cases for the AbstractStrategy class behavior. - /// Tests process launching, output handling, and directory management. - /// Note: AbstractStrategy is internal, so we test through WindowStrategy. - /// - public class AbstractStrategyTests - { - /// - /// Tests that ProcessNameOrId can accept process name. - /// - [Fact] - public void ProcessNameOrId_AcceptsProcessName() - { - // Arrange & Act - var parameter = new MonitorParameter - { - ProcessNameOrId = "myapp.exe" - }; - - // Assert - Assert.Equal("myapp.exe", parameter.ProcessNameOrId); - } - - /// - /// Tests that ProcessNameOrId can accept process ID. - /// - [Fact] - public void ProcessNameOrId_AcceptsProcessId() - { - // Arrange & Act - var parameter = new MonitorParameter - { - ProcessNameOrId = "12345" - }; - - // Assert - Assert.Equal("12345", parameter.ProcessNameOrId); - } - - /// - /// Tests that InnerArguments are constructed correctly for procdump. - /// We can verify the expected format through the parameter values. - /// - [Fact] - public void InnerArguments_ShouldContainProcdumpParameters() - { - // The InnerArguments should be in format: "-e -ma {ProcessNameOrId} {dumpFullPath}" - // This is set by WindowStrategy before launching - - // Arrange - var processName = "test.exe"; - var dumpFileName = "crash.dmp"; - var failDirectory = "/path/to/fail"; - var expectedDumpPath = Path.Combine(failDirectory, dumpFileName); - - // The format should be: -e -ma test.exe /path/to/fail/crash.dmp - var expectedFormat = $"-e -ma {processName} {expectedDumpPath}"; - - // Assert - Verify the expected format structure - Assert.Contains("-e", expectedFormat); - Assert.Contains("-ma", expectedFormat); - Assert.Contains(processName, expectedFormat); - Assert.Contains(dumpFileName, expectedFormat); - } - - /// - /// Tests that Applications directory path is constructed correctly for Windows. - /// - [Fact] - public void ApplicationsDirectory_ConstructedCorrectly_ForWindows() - { - // Arrange - var targetPath = "/path/to/target"; - var expectedAppDir = Path.Combine(targetPath, "Applications", "Windows"); - - // Assert - Assert.Equal($"{targetPath}{Path.DirectorySeparatorChar}Applications{Path.DirectorySeparatorChar}Windows", expectedAppDir); - } - - /// - /// Tests that InnerApp path is constructed correctly with architecture-specific procdump. - /// - [Fact] - public void InnerApp_PathConstructedCorrectly_WithArchitecture() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; - } - - // Arrange - var targetPath = "/path/to/target"; - var applicationsDir = Path.Combine(targetPath, "Applications", "Windows"); - - var currentArch = RuntimeInformation.OSArchitecture; - var expectedExe = currentArch switch - { - Architecture.X86 => "procdump.exe", - Architecture.X64 => "procdump64.exe", - _ => "procdump64a.exe" - }; - - var expectedPath = Path.Combine(applicationsDir, expectedExe); - - // Assert - Assert.Contains("Applications", expectedPath); - Assert.Contains("Windows", expectedPath); - Assert.Contains(".exe", expectedPath); - Assert.EndsWith(expectedExe, expectedPath); - } - - /// - /// Tests that strategy parameters are properly initialized. - /// - [Fact] - public void Strategy_ParametersInitialized_Correctly() - { - // Arrange - var parameter = new MonitorParameter - { - TargetPath = "/target", - FailDirectory = "/fail", - BackupDirectory = "/backup", - ProcessNameOrId = "app.exe", - DumpFileName = "dump.dmp", - FailFileName = "fail.json", - WorkModel = "Upgrade", - ExtendedField = "1.0.0" - }; - - // Assert - All parameters should be set - Assert.NotNull(parameter.TargetPath); - Assert.NotNull(parameter.FailDirectory); - Assert.NotNull(parameter.BackupDirectory); - Assert.NotNull(parameter.ProcessNameOrId); - Assert.NotNull(parameter.DumpFileName); - Assert.NotNull(parameter.FailFileName); - Assert.NotNull(parameter.WorkModel); - Assert.NotNull(parameter.ExtendedField); - } - } -} diff --git a/tests/BowlTest/Strategys/MonitorParameterTests.cs b/tests/BowlTest/Strategys/MonitorParameterTests.cs deleted file mode 100644 index 8a86dc02..00000000 --- a/tests/BowlTest/Strategys/MonitorParameterTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -using GeneralUpdate.Bowl.Strategys; - -namespace BowlTest.Strategys -{ - /// - /// Contains test cases for the MonitorParameter class. - /// Tests parameter initialization and property assignments. - /// - public class MonitorParameterTests - { - /// - /// Tests that constructor creates a new instance with default values. - /// - [Fact] - public void Constructor_CreatesInstance_WithDefaultValues() - { - // Act - var parameter = new MonitorParameter(); - - // Assert - Assert.NotNull(parameter); - Assert.Equal("Upgrade", parameter.WorkModel); - } - - /// - /// Tests that WorkModel property has default value of "Upgrade". - /// - [Fact] - public void WorkModel_HasDefaultValue_Upgrade() - { - // Arrange & Act - var parameter = new MonitorParameter(); - - // Assert - Assert.Equal("Upgrade", parameter.WorkModel); - } - - /// - /// Tests that TargetPath property can be set and retrieved. - /// - [Fact] - public void TargetPath_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedPath = "/path/to/target"; - - // Act - parameter.TargetPath = expectedPath; - - // Assert - Assert.Equal(expectedPath, parameter.TargetPath); - } - - /// - /// Tests that FailDirectory property can be set and retrieved. - /// - [Fact] - public void FailDirectory_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedPath = "/path/to/fail"; - - // Act - parameter.FailDirectory = expectedPath; - - // Assert - Assert.Equal(expectedPath, parameter.FailDirectory); - } - - /// - /// Tests that BackupDirectory property can be set and retrieved. - /// - [Fact] - public void BackupDirectory_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedPath = "/path/to/backup"; - - // Act - parameter.BackupDirectory = expectedPath; - - // Assert - Assert.Equal(expectedPath, parameter.BackupDirectory); - } - - /// - /// Tests that ProcessNameOrId property can be set and retrieved. - /// - [Fact] - public void ProcessNameOrId_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedValue = "myapp.exe"; - - // Act - parameter.ProcessNameOrId = expectedValue; - - // Assert - Assert.Equal(expectedValue, parameter.ProcessNameOrId); - } - - /// - /// Tests that DumpFileName property can be set and retrieved. - /// - [Fact] - public void DumpFileName_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedValue = "crash.dmp"; - - // Act - parameter.DumpFileName = expectedValue; - - // Assert - Assert.Equal(expectedValue, parameter.DumpFileName); - } - - /// - /// Tests that FailFileName property can be set and retrieved. - /// - [Fact] - public void FailFileName_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedValue = "fail.json"; - - // Act - parameter.FailFileName = expectedValue; - - // Assert - Assert.Equal(expectedValue, parameter.FailFileName); - } - - /// - /// Tests that WorkModel property can be changed from default value. - /// - [Fact] - public void WorkModel_CanBeChanged() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedValue = "Normal"; - - // Act - parameter.WorkModel = expectedValue; - - // Assert - Assert.Equal(expectedValue, parameter.WorkModel); - } - - /// - /// Tests that ExtendedField property can be set and retrieved. - /// - [Fact] - public void ExtendedField_CanBeSetAndRetrieved() - { - // Arrange - var parameter = new MonitorParameter(); - var expectedValue = "1.0.0"; - - // Act - parameter.ExtendedField = expectedValue; - - // Assert - Assert.Equal(expectedValue, parameter.ExtendedField); - } - - /// - /// Tests that all properties can be set together. - /// - [Fact] - public void AllProperties_CanBeSetTogether() - { - // Arrange & Act - var parameter = new MonitorParameter - { - TargetPath = "/target", - FailDirectory = "/fail", - BackupDirectory = "/backup", - ProcessNameOrId = "app.exe", - DumpFileName = "dump.dmp", - FailFileName = "fail.json", - WorkModel = "Normal", - ExtendedField = "2.0.0" - }; - - // Assert - Assert.Equal("/target", parameter.TargetPath); - Assert.Equal("/fail", parameter.FailDirectory); - Assert.Equal("/backup", parameter.BackupDirectory); - Assert.Equal("app.exe", parameter.ProcessNameOrId); - Assert.Equal("dump.dmp", parameter.DumpFileName); - Assert.Equal("fail.json", parameter.FailFileName); - Assert.Equal("Normal", parameter.WorkModel); - Assert.Equal("2.0.0", parameter.ExtendedField); - } - } -} diff --git a/tests/BowlTest/Strategys/WindowStrategyTests.cs b/tests/BowlTest/Strategys/WindowStrategyTests.cs deleted file mode 100644 index 14156d71..00000000 --- a/tests/BowlTest/Strategys/WindowStrategyTests.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using GeneralUpdate.Bowl.Strategys; - -namespace BowlTest.Strategys -{ - /// - /// Contains test cases for the WindowStrategy class. - /// Tests strategy initialization, execution flow, and platform-specific behavior. - /// Note: WindowStrategy is internal, so we test through public API and reflection. - /// - public class WindowStrategyTests - { - /// - /// Tests that GetAppName returns correct procdump executable for X86 architecture. - /// - [Fact] - public void GetAppName_ForX86Architecture_ReturnsProcdumpExe() - { - // Only run this test on Windows - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; - } - - // This test validates the architecture-based selection logic - // For X86: procdump.exe - // For X64: procdump64.exe - // For others: procdump64a.exe (ARM64) - - var currentArch = RuntimeInformation.OSArchitecture; - string expectedExe = currentArch switch - { - Architecture.X86 => "procdump.exe", - Architecture.X64 => "procdump64.exe", - _ => "procdump64a.exe" - }; - - // We can't test GetAppName directly as it's private, but we can verify - // the logic through the behavior of Launch method - Assert.NotNull(expectedExe); - Assert.EndsWith(".exe", expectedExe); - } - - /// - /// Tests that SetParameter sets the parameter correctly. - /// - [Fact] - public void SetParameter_SetsParameterCorrectly() - { - // Only run this test on Windows - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; - } - - // Arrange - var tempPath = Path.GetTempPath(); - var testPath = Path.Combine(tempPath, $"BowlTest_{Guid.NewGuid()}"); - Directory.CreateDirectory(testPath); - - try - { - var parameter = new MonitorParameter - { - TargetPath = testPath, - FailDirectory = Path.Combine(testPath, "fail"), - BackupDirectory = Path.Combine(testPath, "backup"), - ProcessNameOrId = "test.exe", - DumpFileName = "test.dmp", - FailFileName = "test.json", - WorkModel = "Normal" - }; - - // Get WindowStrategy type using reflection - var bowlAssembly = typeof(MonitorParameter).Assembly; - var strategyType = bowlAssembly.GetType("GeneralUpdate.Bowl.Strategys.WindowStrategy"); - - if (strategyType != null) - { - // Act - var strategy = Activator.CreateInstance(strategyType); - var setParameterMethod = strategyType.GetMethod("SetParameter"); - - // Assert - Should not throw - var exception = Record.Exception(() => setParameterMethod?.Invoke(strategy, new[] { parameter })); - Assert.Null(exception); - } - } - finally - { - // Cleanup - if (Directory.Exists(testPath)) - { - Directory.Delete(testPath, true); - } - } - } - - /// - /// Tests that WorkModel property correctly defaults to "Upgrade". - /// - [Fact] - public void MonitorParameter_WorkModel_DefaultsToUpgrade() - { - // Arrange & Act - var parameter = new MonitorParameter(); - - // Assert - Assert.Equal("Upgrade", parameter.WorkModel); - } - - /// - /// Tests that MonitorParameter can be configured for Normal mode. - /// - [Fact] - public void MonitorParameter_CanBeConfiguredForNormalMode() - { - // Arrange & Act - var parameter = new MonitorParameter - { - WorkModel = "Normal" - }; - - // Assert - Assert.Equal("Normal", parameter.WorkModel); - } - - /// - /// Tests that dump file name is constructed correctly with version. - /// - [Fact] - public void DumpFileName_ConstructedCorrectly_WithVersion() - { - // Arrange - var version = "1.2.3"; - var expectedDumpFileName = $"{version}_fail.dmp"; - var expectedFailFileName = $"{version}_fail.json"; - - var parameter = new MonitorParameter - { - DumpFileName = expectedDumpFileName, - FailFileName = expectedFailFileName - }; - - // Assert - Assert.Equal(expectedDumpFileName, parameter.DumpFileName); - Assert.Equal(expectedFailFileName, parameter.FailFileName); - } - - /// - /// Tests that backup directory path is constructed correctly. - /// - [Fact] - public void BackupDirectory_ConstructedCorrectly_WithVersionPath() - { - // Arrange - var installPath = "/path/to/install"; - var version = "1.2.3"; - var expectedBackupDir = Path.Combine(installPath, version); - - var parameter = new MonitorParameter - { - BackupDirectory = expectedBackupDir - }; - - // Assert - Assert.Equal(expectedBackupDir, parameter.BackupDirectory); - Assert.Contains(version, parameter.BackupDirectory); - } - - /// - /// Tests that fail directory path is constructed correctly with version. - /// - [Fact] - public void FailDirectory_ConstructedCorrectly_WithVersionPath() - { - // Arrange - var installPath = "/path/to/install"; - var version = "1.2.3"; - var expectedFailDir = Path.Combine(installPath, "fail", version); - - var parameter = new MonitorParameter - { - FailDirectory = expectedFailDir - }; - - // Assert - Assert.Equal(expectedFailDir, parameter.FailDirectory); - Assert.Contains("fail", parameter.FailDirectory); - Assert.Contains(version, parameter.FailDirectory); - } - } -}