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
7 changes: 7 additions & 0 deletions src/SwfocTrainer.App/Infrastructure/AsyncCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ public event EventHandler? CanExecuteChanged

private static void TryShowError(string message)
{
// No interactive WPF application (headless test runner / app shutdown): skip the
// modal dialog so MessageBox.Show cannot block with no message pump to dismiss it.
if (Application.Current is null)
{
return;
}

try
{
MessageBox.Show(message, "Operation Failed", MessageBoxButton.OK, MessageBoxImage.Error);
Expand Down
42 changes: 23 additions & 19 deletions src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#pragma warning disable S4136
#pragma warning disable S4136
using System.Globalization;
using System.Text.RegularExpressions;
using SwfocTrainer.Core.Contracts;
Expand Down Expand Up @@ -307,7 +307,8 @@ private async Task<SeedScaffoldResult> BuildSeedScaffoldAsync(
p.Warnings.Add("No local path hints were inferred from launch samples.");
}

var metadata = BuildSeedMetadata(p.Seed, p.SourceRunId, baseProfile.Id, workshopIds, aliases, pathHints, requiredCapabilities);
var metadataInput = new SeedMetadataInput(p.Seed, p.SourceRunId, baseProfile.Id, workshopIds, aliases, pathHints, requiredCapabilities);
var metadata = BuildSeedMetadata(metadataInput);

var draftProfile = new TrainerProfile(
Id: p.ProfileId,
Expand Down Expand Up @@ -342,26 +343,29 @@ private async Task<SeedScaffoldResult> BuildSeedScaffoldAsync(
return new SeedScaffoldResult(workshopIds, pathHints, aliases, outputPath);
}

private static Dictionary<string, string> BuildSeedMetadata(
GeneratedProfileSeed seed,
string sourceRunId,
string baseProfileId,
IReadOnlyList<string> workshopIds,
IReadOnlyList<string> aliases,
IReadOnlyList<string> pathHints,
IReadOnlyList<string> requiredCapabilities)
private sealed record SeedMetadataInput(
GeneratedProfileSeed Seed,
string SourceRunId,
string BaseProfileId,
IReadOnlyList<string> WorkshopIds,
IReadOnlyList<string> Aliases,
IReadOnlyList<string> PathHints,
IReadOnlyList<string> RequiredCapabilities);

private static Dictionary<string, string> BuildSeedMetadata(SeedMetadataInput input)
{
var seed = input.Seed;
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["origin"] = "auto_discovery",
["sourceRunId"] = sourceRunId.Trim(),
["sourceRunId"] = input.SourceRunId.Trim(),
["confidence"] = seed.Confidence.ToString("0.00", CultureInfo.InvariantCulture),
["parentProfile"] = ResolveParentProfile(seed, baseProfileId)
["parentProfile"] = ResolveParentProfile(seed, input.BaseProfileId)
};

AddMetadataIfNotEmpty(metadata, "requiredWorkshopIds", workshopIds);
AddMetadataIfNotEmpty(metadata, "profileAliases", aliases);
AddMetadataIfNotEmpty(metadata, "localPathHints", pathHints);
AddMetadataIfNotEmpty(metadata, "requiredWorkshopIds", input.WorkshopIds);
AddMetadataIfNotEmpty(metadata, "profileAliases", input.Aliases);
AddMetadataIfNotEmpty(metadata, "localPathHints", input.PathHints);
AddTrimmedMetadata(metadata, "onboardingNotes", seed.Notes);
AddTrimmedMetadata(metadata, "seedTitle", seed.Title);
AddTrimmedMetadata(metadata, "workshopId", seed.WorkshopId);
Expand All @@ -375,7 +379,7 @@ private static Dictionary<string, string> BuildSeedMetadata(
AddFilteredListMetadata(metadata, "parentDependencies", seed.ParentDependencies);
AddFilteredListMetadata(metadata, "launchHints", seed.LaunchHints);
AddFilteredListMetadata(metadata, "anchorHints", seed.AnchorHints);
AddMetadataIfNotEmpty(metadata, "requiredCapabilities", requiredCapabilities);
AddMetadataIfNotEmpty(metadata, "requiredCapabilities", input.RequiredCapabilities);

return metadata;
}
Expand Down Expand Up @@ -583,7 +587,7 @@ private static IReadOnlyList<string> MergePathHints(
}
}

foreach (var hint in inferred.Where(x => !string.IsNullOrWhiteSpace(x) && IsPathHintCandidate(x))) // NOSONAR
foreach (var hint in inferred.Where(x => !string.IsNullOrWhiteSpace(x) && IsPathHintCandidate(x)))
{
merged.Add(hint);
}
Expand Down Expand Up @@ -637,7 +641,7 @@ private static IReadOnlyList<string> InferWorkshopIds(IReadOnlyList<ModLaunchSam
return ids.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
}

private static IReadOnlyList<string> InferPathHints(IReadOnlyList<ModLaunchSample> samples) // NOSONAR
private static IReadOnlyList<string> InferPathHints(IReadOnlyList<ModLaunchSample> samples)
{
var hints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

Expand Down Expand Up @@ -713,7 +717,7 @@ private static IEnumerable<string> TokenizeHintInput(string input)
.Where(normalized => !string.IsNullOrWhiteSpace(normalized));
}

private static IReadOnlyList<string> InferAliases(string profileId, string displayName, IReadOnlyList<string>? userAliases) // NOSONAR
private static IReadOnlyList<string> InferAliases(string profileId, string displayName, IReadOnlyList<string>? userAliases)
{
var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Expand Down
82 changes: 34 additions & 48 deletions src/SwfocTrainer.Runtime/Scanning/ProcessMemoryScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
{
private readonly record struct FloatScanCriteria(float Value, float Tolerance);

private readonly record struct RegionScanContext(
nint Handle,
nint RegionBase,
long RegionSize,
List<nint> Results,
int MaxResults,
CancellationToken CancellationToken);

private readonly record struct ChunkReadContext(
nint Handle,
nint RegionBase,
long Offset,
int ToRead);

internal readonly record struct FloatApproxScanRequest(
int ProcessId,
float Value,
Expand Down Expand Up @@ -39,7 +53,7 @@
maxResults,
cancellationToken,
(handle, regionBase, regionSize, results, max, token) =>
ScanRegion(handle, regionBase, regionSize, value, results, max, token));
ScanRegion(new RegionScanContext(handle, regionBase, regionSize, results, max, token), value));
}

public static IReadOnlyList<nint> ScanFloatApprox(
Expand Down Expand Up @@ -67,56 +81,36 @@
maxResults,
cancellationToken,
(handle, regionBase, regionSize, results, max, token) =>
ScanRegionFloatApprox(handle, regionBase, regionSize, criteria, results, max, token));
ScanRegionFloatApprox(new RegionScanContext(handle, regionBase, regionSize, results, max, token), criteria));
}

private static void ScanRegion(
nint handle,
nint regionBase,
long regionSize,
int value,
List<nint> results,
int maxResults,
CancellationToken cancellationToken)
RegionScanContext ctx,
int value)
{
ScanRegionChunks(
handle,
regionBase,
regionSize,
results,
maxResults,
cancellationToken,
ctx,
(buffer, read, chunkBase) =>
{
for (var i = 0; i <= read - 4 && results.Count < maxResults; i++)
for (var i = 0; i <= read - 4 && ctx.Results.Count < ctx.MaxResults; i++)
{
if (BitConverter.ToInt32(buffer, i) == value)
{
results.Add(chunkBase + i);
ctx.Results.Add(chunkBase + i);
}
}
});
}

private static void ScanRegionFloatApprox(
nint handle,
nint regionBase,
long regionSize,
FloatScanCriteria criteria,
List<nint> results,
int maxResults,
CancellationToken cancellationToken)
RegionScanContext ctx,
FloatScanCriteria criteria)
{
ScanRegionChunks(
handle,
regionBase,
regionSize,
results,
maxResults,
cancellationToken,
ctx,
(buffer, read, chunkBase) =>
{
for (var i = 0; i <= read - 4 && results.Count < maxResults; i += 4)
for (var i = 0; i <= read - 4 && ctx.Results.Count < ctx.MaxResults; i += 4)
{
var candidate = BitConverter.ToSingle(buffer, i);
if (!float.IsFinite(candidate))
Expand All @@ -126,40 +120,35 @@

if (MathF.Abs(candidate - criteria.Value) <= criteria.Tolerance)
{
results.Add(chunkBase + i);
ctx.Results.Add(chunkBase + i);
}
}
});
}

private static void ScanRegionChunks(
nint handle,
nint regionBase,
long regionSize,
List<nint> results,
int maxResults,
CancellationToken cancellationToken,
RegionScanContext ctx,
Action<byte[], int, nint> chunkScanner)
{
const int chunkSize = 64 * 1024;
var buffer = new byte[chunkSize];

for (long offset = 0; offset < regionSize && results.Count < maxResults; offset += chunkSize)
for (long offset = 0; offset < ctx.RegionSize && ctx.Results.Count < ctx.MaxResults; offset += chunkSize)
{
cancellationToken.ThrowIfCancellationRequested();
var toRead = (int)Math.Min(chunkSize, regionSize - offset);
ctx.CancellationToken.ThrowIfCancellationRequested();
var toRead = (int)Math.Min(chunkSize, ctx.RegionSize - offset);
if (toRead <= 0)
{
break;
}

buffer = EnsureBufferSize(buffer, toRead);
if (!TryReadChunk(handle, regionBase, offset, buffer, toRead, out var read))
if (!TryReadChunk(new ChunkReadContext(ctx.Handle, ctx.RegionBase, offset, toRead), buffer, out var read))
{
continue;
}

chunkScanner(buffer, read, regionBase + (nint)offset);
chunkScanner(buffer, read, ctx.RegionBase + (nint)offset);
}
}

Expand All @@ -169,15 +158,12 @@
}

private static bool TryReadChunk(
nint handle,
nint regionBase,
long offset,
ChunkReadContext ctx,
byte[] buffer,
int toRead,
out int read)
{
read = 0;
if (!NativeMethods.ReadProcessMemory(handle, regionBase + (nint)offset, buffer, toRead, out var readRaw))
if (!NativeMethods.ReadProcessMemory(ctx.Handle, ctx.RegionBase + (nint)ctx.Offset, buffer, ctx.ToRead, out var readRaw))

Check notice

Code scanning / CodeQL

Calls to unmanaged code Note

Replace this call with a call to managed code if possible.
Comment thread
Prekzursil marked this conversation as resolved.
Dismissed
{
return false;
}
Expand Down
69 changes: 30 additions & 39 deletions src/SwfocTrainer.Runtime/Services/CapabilityMapResolver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using SwfocTrainer.Core.Contracts;
using SwfocTrainer.Core.Models;
Expand Down Expand Up @@ -233,28 +233,19 @@ private static CapabilityResolutionResult ResolveOperationAnchors(AnchorResoluti
.ToArray();
if (missingRequired.Length > 0)
{
return BuildRequiredAnchorsMissingResult(
input.RequestedProfileId,
input.OperationId,
input.Operation,
input.Fingerprint,
matched,
missingRequired,
input.CapabilityHint);
var requiredCtx = new AnchorValidationContext(
input.RequestedProfileId, input.OperationId, matched, missingRequired, input.CapabilityHint);
return BuildRequiredAnchorsMissingResult(requiredCtx, input.Operation, input.Fingerprint);
}

var missingOptional = input.Operation.OptionalAnchors
.Where(anchor => !ContainsAnchor(input.ResolvedAnchors, anchor, comparer))
.ToArray();
if (missingOptional.Length > 0)
{
return BuildOptionalAnchorsMissingResult(
input.RequestedProfileId,
input.OperationId,
input.Fingerprint.FingerprintId,
matched,
missingOptional,
input.CapabilityHint);
var optionalCtx = new AnchorValidationContext(
input.RequestedProfileId, input.OperationId, matched, missingOptional, input.CapabilityHint);
return BuildOptionalAnchorsMissingResult(optionalCtx, input.Fingerprint.FingerprintId);
}

return BuildAllRequiredAnchorsPresentResult(
Expand All @@ -266,44 +257,36 @@ private static CapabilityResolutionResult ResolveOperationAnchors(AnchorResoluti
}

private static CapabilityResolutionResult BuildRequiredAnchorsMissingResult(
string requestedProfileId,
string operationId,
AnchorValidationContext ctx,
CapabilityOperationMap operation,
BinaryFingerprint fingerprint,
IReadOnlyList<string> matchedAnchors,
IReadOnlyList<string> missingAnchors,
CapabilityAvailabilityHint? capabilityHint)
BinaryFingerprint fingerprint)
{
return new CapabilityResolutionResult(
requestedProfileId,
operationId,
ctx.RequestedProfileId,
ctx.OperationId,
SdkCapabilityStatus.Degraded,
CapabilityReasonCode.RequiredAnchorsMissing,
BuildConfidence(matchedAnchors.Count, operation.RequiredAnchors.Count),
BuildConfidence(ctx.MatchedAnchors.Count, operation.RequiredAnchors.Count),
fingerprint.FingerprintId,
matchedAnchors,
missingAnchors,
ResolveCapabilityMetadata(capabilityHint));
ctx.MatchedAnchors,
ctx.MissingAnchors,
ResolveCapabilityMetadata(ctx.CapabilityHint));
}

private static CapabilityResolutionResult BuildOptionalAnchorsMissingResult(
string requestedProfileId,
string operationId,
string fingerprintId,
IReadOnlyList<string> matchedAnchors,
IReadOnlyList<string> missingAnchors,
CapabilityAvailabilityHint? capabilityHint)
AnchorValidationContext ctx,
string fingerprintId)
{
return new CapabilityResolutionResult(
requestedProfileId,
operationId,
ctx.RequestedProfileId,
ctx.OperationId,
SdkCapabilityStatus.Degraded,
CapabilityReasonCode.OptionalAnchorsMissing,
0.85d,
fingerprintId,
matchedAnchors,
missingAnchors,
ResolveCapabilityMetadata(capabilityHint));
ctx.MatchedAnchors,
ctx.MissingAnchors,
ResolveCapabilityMetadata(ctx.CapabilityHint));
}

private static CapabilityResolutionResult BuildAllRequiredAnchorsPresentResult(
Expand Down Expand Up @@ -507,6 +490,14 @@ private sealed class CapabilityOperationDto
public string[]? OptionalAnchors { get; set; } = Array.Empty<string>();
}

private sealed record AnchorValidationContext(
string RequestedProfileId,
string OperationId,
IReadOnlyList<string> MatchedAnchors,
IReadOnlyList<string> MissingAnchors,
CapabilityAvailabilityHint? CapabilityHint);


private sealed class CapabilityEntryDto
{
public string? FeatureId { get; set; } = string.Empty;
Expand Down
Loading
Loading