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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ OnDisable
resource lease, a Lua reference, a cancellation token, or a target-bound value across disable/re-enable. Constructors,
field initializers, and static initialization must not call Cheat Engine; the SDK binding is valid only after enable.

All Client operations are synchronous. A cancellation token can prevent dispatch or stop Client-managed work between
steps, but it does not claim to interrupt a Lua primitive that has already started. Read
[ADR 0002](docs/adr/0002-plugin-activation-lifecycle.md) before adding a service that touches Cheat Engine.
All Client operations are synchronous. For stateful Client operations, request validation is followed by activation
admission, then caller-cancellation observation, and only then policy checks or Cheat Engine work. A stale activation
therefore throws `CheatEngineActivationExpiredException` even when the requested capability is currently gated. A
cancellation token can prevent dispatch or stop Client-managed work between steps, but it does not claim to interrupt
a Lua primitive that has already started. `ILocalProcessDiagnostics` is the explicit exception: it is an offline BCL
catalog service, never a proof of Cheat Engine target identity, and its copied snapshots remain usable after disable.
Read [ADR 0002](docs/adr/0002-plugin-activation-lifecycle.md) before adding a service that touches Cheat Engine.

## Packages and direct SDK reference

Expand Down
22 changes: 14 additions & 8 deletions docs/engineering/work-items/CLI-021.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ SDK owns CE mappings, native safety, factual outcomes and low-level owners. Clie

### Technical requirements and task checklist

- [ ] Apply stale-client policy consistently before operational work, even for unavailable implementations.
- [ ] Label local process discovery/enrichment as local; no speculative remote backend.
- [ ] Document cancellation ordering and keep pure immutable snapshots usable where intended.
- [x] Apply stale-client policy consistently before operational work, even for unavailable implementations.
- [x] Label local process discovery/enrichment as local; no speculative remote backend.
- [x] Document cancellation ordering and keep pure immutable snapshots usable where intended.

### Acceptance criteria

- [ ] Disable/re-enable does not produce different lifetime behavior by domain accident.
- [ ] Offline diagnostics have a separate explicit contract.
- [ ] Local process metadata is never mistaken for authoritative SDK target identity.
- [x] Disable/re-enable does not produce different lifetime behavior by domain accident.
- [x] Offline diagnostics have a separate explicit contract.
- [x] Local process metadata is never mistaken for authoritative SDK target identity.

### Required validation

- [ ] Stale facade across local, implemented and unavailable domains.
- [ ] Canceled enumeration and disappearing local process fixtures.
- [x] Stale facade across local, implemented and unavailable domains.
- [x] Canceled enumeration and disappearing local process fixtures.

### Scope exclusions

Expand Down Expand Up @@ -93,3 +93,9 @@ Required for the affected capability. Optional profiles may be deferred through
### Maintainer notes

Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.

Implementation: local BCL enumeration moved to `ILocalProcessDiagnostics` with `LocalProcessId`; `IProcessClient`
continues to describe only Cheat Engine selection. Missing BCL metadata now yields optional enrichment, not a target
detach. Stateful and capability-gated services admit the activation before policy or cancellation; offline snapshots
remain pure managed values. Release build passed with zero warnings. Core tests passed (238); the full solution run had
one pre-existing source-generator snapshot newline mismatch, while its other 522 tests passed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using CheatEngine.Client.Results;

namespace CheatEngine.Client.Processes;

/// <summary>Reads bounded, copied metadata from the local operating-system process catalog.</summary>
/// <remarks>
/// This is an offline diagnostic contract. It neither dispatches to Cheat Engine nor observes, selects, or proves
/// a Cheat Engine target. Its values remain ordinary managed snapshots after a plugin activation ends. For each
/// operation, request validation occurs first, then cancellation is observed before catalog access and between
/// Client-managed materialization steps.
/// </remarks>
public interface ILocalProcessDiagnostics
{
/// <summary>Tries to enumerate copied local-process metadata within an explicit materialization bound.</summary>
/// <remarks>Cancellation is observed before catalog access and between Client-managed materialization steps.</remarks>
public bool TryGetProcesses(ProcessEnumerationRequest request, out ProcessEnumerationResult result,
out CheatEngineFailure failure, CancellationToken cancellationToken = default);

/// <summary>Enumerates copied local-process metadata within an explicit materialization bound.</summary>
public ProcessEnumerationResult GetProcesses(ProcessEnumerationRequest request,
CancellationToken cancellationToken = default);
}
30 changes: 13 additions & 17 deletions libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@

namespace CheatEngine.Client.Processes;

/// <summary>Reads the process selected by the active Cheat Engine session.</summary>
/// <summary>Reads and changes the process selected by the active Cheat Engine session.</summary>
public interface IProcessClient
{
/// <summary>Tries to enumerate copied local-process metadata within an explicit materialization bound.</summary>
public bool TryGetProcesses(ProcessEnumerationRequest request, out ProcessEnumerationResult result,
out CheatEngineFailure failure, CancellationToken cancellationToken = default);

/// <summary>Enumerates copied local-process metadata within an explicit materialization bound.</summary>
public ProcessEnumerationResult GetProcesses(ProcessEnumerationRequest request,
CancellationToken cancellationToken = default);

/// <summary>Tries to get a copied snapshot of the currently selected target process.</summary>
/// <remarks>
/// Returns <see cref="CheatEngineFailureKind.TargetNotAttached" /> when Cheat Engine has no selected target or
/// its selected target is no longer available in local process metadata. An inconsistent local metadata result
/// returns <see cref="CheatEngineFailureKind.InvalidHostResult" />. Invalid arguments, lifecycle failures, and
/// unexpected implementation exceptions are not converted into a <c>Try</c> result.
/// Returns <see cref="CheatEngineFailureKind.TargetNotAttached" /> only when Cheat Engine has no selected target.
/// Local operating-system metadata is optional enrichment; its absence leaves the Cheat Engine target snapshot
/// valid with null name and executable path. Invalid arguments, lifecycle failures, and unexpected implementation
/// exceptions are not converted into a <c>Try</c> result.
/// </remarks>
public bool TryGetCurrent(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
CancellationToken cancellationToken = default);
Expand All @@ -32,9 +24,9 @@ public bool TryGetCurrent(out ProcessSnapshot snapshot, out CheatEngineFailure f
/// architecture changed.
/// </summary>
/// <remarks>
/// Returns <see cref="CheatEngineFailureKind.TargetNotAttached" /> and invalidates an observed selection when
/// the selected target is absent or no longer has local process metadata. This is an observation, not an
/// atomic process-lifetime guarantee.
/// Returns <see cref="CheatEngineFailureKind.TargetNotAttached" /> and invalidates an observed selection only
/// when Cheat Engine reports no selected target. Local metadata is optional enrichment and does not establish
/// liveness or target identity. This is an observation, not an atomic process-lifetime guarantee.
/// </remarks>
public bool TryRefresh(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
CancellationToken cancellationToken = default);
Expand All @@ -50,7 +42,11 @@ public bool TryAttach(TargetProcessId processId, out ProcessSnapshot snapshot,
public ProcessSnapshot Attach(TargetProcessId processId,
CancellationToken cancellationToken = default);

/// <summary>Tries to attach to the single local process whose executable name matches exactly.</summary>
/// <summary>Tries to attach to the single locally discovered process whose executable name matches exactly.</summary>
/// <remarks>
/// Activation admission occurs before local discovery; caller cancellation is then observed before catalog access.
/// A local match is only an attach candidate. Cheat Engine's selected target is verified before returning.
/// </remarks>
public bool TryAttachExactName(string processName, out ProcessSnapshot snapshot, out CheatEngineFailure failure,
CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace CheatEngine.Client.Processes;

/// <summary>Identifies a process observed by the local operating-system catalog.</summary>
/// <remarks>This is deliberately not a Cheat Engine target identity. A numeric process identifier can be reused.</remarks>
public readonly record struct LocalProcessId
{
/// <summary>Creates a positive local operating-system process identifier.</summary>
public LocalProcessId(int value)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
Value = value;
}

/// <summary>Gets the locally observed numeric process identifier.</summary>
public int Value { get; }
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using CheatEngine.SDK.Engine.Inspection;

namespace CheatEngine.Client.Processes;

/// <summary>Copied local-process metadata that is independent of Cheat Engine's selected target.</summary>
/// <remarks>This data is local operating-system enrichment only; it neither selects nor identifies a Cheat Engine target.</remarks>
public readonly record struct ProcessInfoSnapshot
{
/// <summary>Creates copied local-process metadata.</summary>
public ProcessInfoSnapshot(TargetProcessId id, string? name, string? executablePath)
public ProcessInfoSnapshot(LocalProcessId id, string? name, string? executablePath)
{
if (name is { Length: 0 })
{
Expand All @@ -23,8 +22,8 @@ public ProcessInfoSnapshot(TargetProcessId id, string? name, string? executableP
ExecutablePath = executablePath;
}

/// <summary>Gets the local process identifier.</summary>
public TargetProcessId Id
/// <summary>Gets the local operating-system process identifier, not a Cheat Engine target identity.</summary>
public LocalProcessId Id
{
get;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace CheatEngine.Client.Processes;

/// <summary>An immutable snapshot of the process currently selected in Cheat Engine.</summary>
/// <remarks>The identifier and architecture are Cheat Engine observations. Name and executable path are optional local BCL enrichment and do not establish liveness or authoritative target provenance.</remarks>
public readonly record struct ProcessSnapshot
{
/// <summary>Creates a selected-process snapshot without a target-architecture observation.</summary>
Expand Down
19 changes: 15 additions & 4 deletions libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
#nullable enable
CheatEngine.Client.Processes.ILocalProcessDiagnostics
CheatEngine.Client.Processes.ILocalProcessDiagnostics.GetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessEnumerationResult
CheatEngine.Client.Processes.ILocalProcessDiagnostics.TryGetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, out CheatEngine.Client.Processes.ProcessEnumerationResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.LocalProcessId
CheatEngine.Client.Processes.LocalProcessId.Equals(CheatEngine.Client.Processes.LocalProcessId other) -> bool
CheatEngine.Client.Processes.LocalProcessId.LocalProcessId() -> void
CheatEngine.Client.Processes.LocalProcessId.LocalProcessId(int value) -> void
CheatEngine.Client.Processes.LocalProcessId.Value.get -> int
~override CheatEngine.Client.Processes.LocalProcessId.Equals(object obj) -> bool
override CheatEngine.Client.Processes.LocalProcessId.GetHashCode() -> int
~override CheatEngine.Client.Processes.LocalProcessId.ToString() -> string
static CheatEngine.Client.Processes.LocalProcessId.operator !=(CheatEngine.Client.Processes.LocalProcessId left, CheatEngine.Client.Processes.LocalProcessId right) -> bool
static CheatEngine.Client.Processes.LocalProcessId.operator ==(CheatEngine.Client.Processes.LocalProcessId left, CheatEngine.Client.Processes.LocalProcessId right) -> bool
CheatEngine.Client.ICheatEngineClient.Allocations.get -> CheatEngine.Client.Allocations.IAllocationClient!
CheatEngine.Client.ICheatEngineClient.Assembly.get -> CheatEngine.Client.Assembly.IAssemblyClient!
CheatEngine.Client.ICheatEngineClient.Dbvm.get -> CheatEngine.Client.Dbvm.IDbvmClient!
Expand All @@ -20,13 +33,11 @@ static CheatEngine.Client.Runtime.ClientCapabilityId.Timers.get -> CheatEngine.C
CheatEngine.Client.Processes.IProcessClient.AttachForeground(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
CheatEngine.Client.Processes.IProcessClient.Create(CheatEngine.Client.Processes.ProcessStartRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
CheatEngine.Client.Processes.IProcessClient.GetPauseState(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessPauseSnapshot
CheatEngine.Client.Processes.IProcessClient.GetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessEnumerationResult
CheatEngine.Client.Processes.IProcessClient.Pause(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
CheatEngine.Client.Processes.IProcessClient.ResumeExecution(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
CheatEngine.Client.Processes.IProcessClient.TryAttachForeground(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.IProcessClient.TryCreate(CheatEngine.Client.Processes.ProcessStartRequest request, out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.IProcessClient.TryGetPauseState(out CheatEngine.Client.Processes.ProcessPauseSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.IProcessClient.TryGetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, out CheatEngine.Client.Processes.ProcessEnumerationResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.IProcessClient.TryPause(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.IProcessClient.TryResumeExecution(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
CheatEngine.Client.Processes.ProcessEnumerationRequest
Expand Down Expand Up @@ -54,10 +65,10 @@ static CheatEngine.Client.Processes.ProcessEnumerationResult.operator ==(CheatEn
CheatEngine.Client.Processes.ProcessInfoSnapshot
CheatEngine.Client.Processes.ProcessInfoSnapshot.Equals(CheatEngine.Client.Processes.ProcessInfoSnapshot other) -> bool
CheatEngine.Client.Processes.ProcessInfoSnapshot.ExecutablePath.get -> string?
CheatEngine.Client.Processes.ProcessInfoSnapshot.Id.get -> CheatEngine.SDK.Engine.Inspection.TargetProcessId
CheatEngine.Client.Processes.ProcessInfoSnapshot.Id.get -> CheatEngine.Client.Processes.LocalProcessId
CheatEngine.Client.Processes.ProcessInfoSnapshot.Name.get -> string?
CheatEngine.Client.Processes.ProcessInfoSnapshot.ProcessInfoSnapshot() -> void
CheatEngine.Client.Processes.ProcessInfoSnapshot.ProcessInfoSnapshot(CheatEngine.SDK.Engine.Inspection.TargetProcessId id, string? name, string? executablePath) -> void
CheatEngine.Client.Processes.ProcessInfoSnapshot.ProcessInfoSnapshot(CheatEngine.Client.Processes.LocalProcessId id, string? name, string? executablePath) -> void
~override CheatEngine.Client.Processes.ProcessInfoSnapshot.Equals(object obj) -> bool
override CheatEngine.Client.Processes.ProcessInfoSnapshot.GetHashCode() -> int
~override CheatEngine.Client.Processes.ProcessInfoSnapshot.ToString() -> string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
using CheatEngine.Client.Allocations;
using CheatEngine.Client.Core.Domains.Events;
using CheatEngine.Client.Results;
using CheatEngine.Client.Core.Infrastructure;

namespace CheatEngine.Client.Core.Domains.Allocations;

/// <summary>Preserves the allocation contract while its SDK ownership factory awaits live-host validation.</summary>
internal sealed class UnavailableAllocationClient : IAllocationClient
{
private readonly CoreLifetime? _lifetime;
internal UnavailableAllocationClient(CoreLifetime? lifetime = null) => _lifetime = lifetime;
public bool TryAllocate(TargetAllocationRequest request, [NotNullWhen(true)] out ITargetMemoryLease? lease,
out CheatEngineFailure failure,
CancellationToken cancellationToken = default)
{
lease = null;
failure = UnavailableCapabilityFailure.Create("Target allocations", "Allocations.Allocate", cancellationToken);
failure = UnavailableCapabilityFailure.Create(_lifetime, "Target allocations", "Allocations.Allocate", cancellationToken);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
using CheatEngine.Client.Assembly;
using CheatEngine.Client.Core.Domains.Events;
using CheatEngine.Client.Results;
using CheatEngine.Client.Core.Infrastructure;
using CheatEngine.SDK.Engine.Values;

namespace CheatEngine.Client.Core.Domains.Assembly;

/// <summary>Preserves the assembly and patch surface until Auto Assembler ownership passes its live-host gate.</summary>
internal sealed class UnavailableAssemblyClient : IAssemblyClient
{
private readonly CoreLifetime? _lifetime;
internal UnavailableAssemblyClient(CoreLifetime? lifetime = null) => _lifetime = lifetime;
public bool TryDisassemble(Address address, out AssemblyInstructionSnapshot instruction,
out CheatEngineFailure failure,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -99,9 +102,9 @@ public IAutoAssemblerPatchLease ApplyPatch(AutoAssemblerScript script,
return UnavailableCapabilityFailure.Throw<IAutoAssemblerPatchLease>(failure);
}

private static CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken)
private CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken)
{
return UnavailableCapabilityFailure.Create("Assembly, disassembly, and Auto Assembler patches", operation,
return UnavailableCapabilityFailure.Create(_lifetime, "Assembly, disassembly, and Auto Assembler patches", operation,
cancellationToken);
}
}
Loading