Skip to content
Open
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
13 changes: 10 additions & 3 deletions src/OpenClaw.Shared/Mxc/DirectAppContainerExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ public async Task<SandboxExecutionResult> ExecuteAsync(
if (ct.IsCancellationRequested)
ct.ThrowIfCancellationRequested();

// If our linked token tripped (timeout) and the caller's didn't, surface
// as a TimedOut result rather than throwing.
var timedOut = result.TimedOut || linked.IsCancellationRequested;
// The grace token can trip while stdout is drained after wxc-exec
// has already exited. Only the executor's TimedOut flag means the
// command itself did not finish.
var timedOut = CompletedResultIsTimeout(result, linked.Token);
if (timedOut)
{
return new SandboxExecutionResult(
Expand Down Expand Up @@ -185,6 +186,12 @@ public async Task<SandboxExecutionResult> ExecuteAsync(
}
}

internal static bool CompletedResultIsTimeout(MxcResult result, CancellationToken graceToken)
{
_ = graceToken;
return result.TimedOut;
}

private static string CreateScratchDir()
{
var dir = Path.Combine(Path.GetTempPath(), "openclaw-mxc-" + Guid.NewGuid().ToString("N").Substring(0, 12));
Expand Down
20 changes: 20 additions & 0 deletions tests/OpenClaw.Shared.Tests/Mxc/DirectAppContainerExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ public class DirectAppContainerExecutorTests
TimeoutMs: 30_000),
TimeoutMs: 30_000);

[Fact]
public void CompletedResult_IsNotATimeoutWhenTheGraceTokenIsCancelled()
{
using var grace = new CancellationTokenSource();
grace.Cancel();
var finished = new MxcResult
{
Success = true,
ExitCode = 7,
Output = "done",
TimedOut = false,
};

Assert.False(DirectAppContainerExecutor.CompletedResultIsTimeout(finished, grace.Token));
Assert.True(grace.IsCancellationRequested);

var timedOut = new MxcResult { ExitCode = -1, TimedOut = true };
Assert.True(DirectAppContainerExecutor.CompletedResultIsTimeout(timedOut, grace.Token));
}

[Fact]
public async Task ExecuteAsync_AppContainerUnavailable_Throws()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,67 @@ public async Task SystemRun_CustomEnv_DeniesBeforeMxcUnavailableHostFallback()
Assert.Equal(0, executor.CallCount);
}

[IntegrationFact]
public async Task DirectWxcExec_HostTokenCancelsBeforeSandboxDeadline_ReportsTimeout()
{
var availability = MxcAvailability.Probe(NullLogger.Instance);
if (!availability.CanRunSystemRunSandbox || string.IsNullOrWhiteSpace(availability.WxcExecPath))
{
Console.WriteLine(
"[mxc-integration] SKIPPING host-token: " +
string.Join("; ", availability.SystemRunSandboxUnsupportedReasons));
return;
}

if (!HasSupportedSandboxPath(AppContext.BaseDirectory)
|| !HasSupportedSandboxPath(availability.WxcExecPath))
{
Console.WriteLine("[mxc-integration] SKIPPING host-token: unsupported sandbox path.");
return;
}

var scratch = Path.Combine(Path.GetTempPath(), "openclaw-mxc-host-token-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(scratch);
try
{
var settings = new SettingsData
{
SystemRunSandboxEnabled = true,
SystemRunAllowOutbound = false,
};
var policy = MxcPolicyBuilder.ForSystemRun(settings, scratch);
using var argsDoc = JsonDocument.Parse(
"""
{"command":"for /L %i in (1,1,8000000) do @rem","shell":"cmd","args":[],"timeoutMs":60000}
""");
var request = new SandboxExecutionRequest(
"system.run",
argsDoc.RootElement.Clone(),
policy,
TimeoutMs: 60_000,
Cwd: scratch);
var built = MxcConfigBuilder.Build(request, scratch);
var config = built with
{
Process = built.Process with { TimeoutMs = 60_000 },
};

var executor = new MxcExecutor(availability.WxcExecPath!);
using var hostToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(2_000));
var result = await executor.RunAsync(config, hostToken.Token);
Console.WriteLine(
"[mxc-integration] host-token " +
$"exitCode={result.ExitCode}; timedOut={result.TimedOut}; durationMs={result.DurationMs}; error={result.Error}");
Assert.True(result.TimedOut, result.Error);
Assert.Contains("cancelled", result.Error ?? "", StringComparison.OrdinalIgnoreCase);
Assert.InRange(result.DurationMs, 1_500, 5_000);
}
finally
{
try { Directory.Delete(scratch, recursive: true); } catch { }
}
}

private static bool HasSupportedSandboxPath(string path)
{
try
Expand Down
Loading