diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce869d4..b5f89939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ ship. ### Changed +- **Local process enumeration.** Ignore nonpositive operating-system process IDs, including the Windows idle + pseudo-process, before filtering and truncation. Unfiltered listings now return attachable process identities + instead of failing when the local catalog contains PID 0. + ### Security ### Deployment diff --git a/libs/CheatEngine.Client.Core/Domains/LocalProcessCatalog.cs b/libs/CheatEngine.Client.Core/Domains/LocalProcessCatalog.cs index 4b27a51c..5edbb47b 100644 --- a/libs/CheatEngine.Client.Core/Domains/LocalProcessCatalog.cs +++ b/libs/CheatEngine.Client.Core/Domains/LocalProcessCatalog.cs @@ -48,6 +48,13 @@ internal static bool TryEnumerate( return Cancel(out result, out failure); } + // Windows reports the Idle pseudo-process as PID 0. LocalProcessId deliberately models only usable, + // positive process identifiers, so omit system and malformed host rows before they affect the bound. + if (process.Id <= 0) + { + continue; + } + if (Matches(request, process)) { matches.Add(process); diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessHostTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessHostTests.cs index 6bdfaa71..99962d45 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessHostTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessHostTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using CheatEngine.Client.Core.Domains; +using CheatEngine.Client.Processes; +using CheatEngine.Client.Results; namespace CheatEngine.Client.Core.Tests.Domains; @@ -41,4 +43,21 @@ public void GetLocalProcessesContainsCopiedMetadataForTheCurrentManagedProcess() Assert.Equal(current.ProcessName, currentSnapshot.Name); } + + [Fact] + public void LocalCatalogSkipsSystemPseudoProcessesWhileRetainingTheCurrentProcess() + { + using Process current = Process.GetCurrentProcess(); + LocalProcessHost host = new(); + + bool succeeded = LocalProcessCatalog.TryEnumerate(host, + new LocalProcessEnumerationRequest(int.MaxValue), out LocalProcessEnumerationResult result, + out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.True(succeeded); + Assert.Equal(default, failure); + Assert.Contains(result.Processes, process => + process.Id.Value == current.Id && process.Name == current.ProcessName); + Assert.All(result.Processes, process => Assert.True(process.Id.Value > 0)); + } } diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientLocalProcessesTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientLocalProcessesTests.cs index 8be8d5c2..881a3f5f 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientLocalProcessesTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientLocalProcessesTests.cs @@ -37,6 +37,46 @@ public void GetLocalProcessesReturnsBoundedOrderedCopiedLocalMetadata() Assert.Empty(port.TargetCalls); } + [Fact] + public void GetLocalProcessesSkipsSystemAndMalformedRowsBeforeApplyingTheBound() + { + (ProcessClient client, FakeLocalProcessHost host, FakeRuntimeObservationPort port) = Create( + [ + new LocalProcessInfo(0, "System Idle Process", null), + new LocalProcessInfo(-1, "malformed", null), + new LocalProcessInfo(52, "alpha-worker", "C:\\fixtures\\alpha-worker.exe"), + new LocalProcessInfo(43, "alpha-server", "C:\\fixtures\\alpha-server.exe") + ]); + + LocalProcessEnumerationResult result = client.GetLocalProcesses( + new LocalProcessEnumerationRequest(1), TestContext.Current.CancellationToken); + + Assert.True(result.IsTruncated); + LocalProcessSnapshot snapshot = Assert.Single(result.Processes); + Assert.Equal(new LocalProcessId(43), snapshot.Id); + Assert.Equal("alpha-server", snapshot.Name); + Assert.Equal(1, host.GetLocalProcessesCalls); + Assert.Empty(port.Calls); + Assert.Empty(port.TargetCalls); + } + + [Fact] + public void GetLocalProcessesReturnsAnEmptyPageWhenOnlyNonpositiveHostRowsMatch() + { + (ProcessClient client, FakeLocalProcessHost host, _) = Create( + [ + new LocalProcessInfo(0, "System Idle Process", null), + new LocalProcessInfo(-4, "malformed", null) + ]); + + LocalProcessEnumerationResult result = client.GetLocalProcesses( + new LocalProcessEnumerationRequest(1), TestContext.Current.CancellationToken); + + Assert.Empty(result.Processes); + Assert.False(result.IsTruncated); + Assert.Equal(1, host.GetLocalProcessesCalls); + } + [Fact] public void TryGetLocalProcessesReportsACancellationAsNotStartedBeforeReadingTheLocalCatalog() {