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
47 changes: 43 additions & 4 deletions src/OpenClaw.Connection/WindowsTcpListenerSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,59 @@ public static WindowsTcpListenerSnapshotResult Capture()
return null;

var readTask = process.StandardOutput.ReadToEndAsync();
if (!process.WaitForExit(5_000))
var output = AwaitRedirectedOutput(process, readTask, timeoutMs: 5_000);
return output?.Trim();
}
catch
{
return null;
}
}

/// <summary>
/// Wait for the child, then drain redirected stdout with the leftover
/// timeout. WaitForExit returns when the child exits, but ReadToEnd
/// completes only after the write end of the pipe closes. A descendant
/// that inherited stdout can keep the pipe open, so unbounded
/// GetResult() would hang past the inspection timeout.
/// </summary>
internal static string? AwaitRedirectedOutput(Process process, Task<string> readTask, int timeoutMs)
{
const int minDrainMs = 250;
var sw = Stopwatch.StartNew();
if (!process.WaitForExit(timeoutMs))
{
try { process.Kill(entireProcessTree: true); } catch { }
ObserveQuietly(readTask);
return null;
}

var elapsedMs = (int)Math.Min(sw.ElapsedMilliseconds, timeoutMs);
var drainBudgetMs = Math.Max(timeoutMs - elapsedMs, minDrainMs);
try
{
if (!readTask.Wait(drainBudgetMs))
{
try { process.Kill(entireProcessTree: true); } catch { }
ObserveQuietly(readTask);
return null;
}

return readTask.GetAwaiter().GetResult().Trim();
}
catch
catch (AggregateException)
{
return null;
}

return readTask.Status == TaskStatus.RanToCompletion ? readTask.Result : null;
}

private static void ObserveQuietly(Task task) =>
_ = task.ContinueWith(
static t => { _ = t.Exception; },
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);

private static bool CaptureIpv4(List<WindowsTcpListenerInfo> destination)
{
return CaptureTable(
Expand Down
29 changes: 1 addition & 28 deletions src/OpenClaw.Tray.WinUI/Services/WslGatewayKeepAliveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,34 +316,7 @@ private static void DeleteKeepAliveMarker(string markerDir, string distroName)
}

private static string? GetProcessCommandLine(int pid)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo("powershell.exe",
$"-NoProfile -Command \"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}').CommandLine\"")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return null;

// Drain stdout asynchronously so a large command line cannot deadlock the fixed-size pipe,
// and bound the whole inspection: WaitForExit(5000) returns before ReadToEnd could block
// forever on a hung CIM/PowerShell. On timeout, kill and report indeterminate (null).
var readTask = p.StandardOutput.ReadToEndAsync();
if (!p.WaitForExit(5000))
{
// slopwatch-ignore: SW003 Best-effort kill of a stuck inspection process; failure cannot improve caller state.
try { p.Kill(entireProcessTree: true); } catch { }
return null;
}

return readTask.GetAwaiter().GetResult()?.Trim();
}
catch { return null; }
}
=> WindowsTcpListenerSnapshot.GetProcessCommandLine(pid);

private static string ResolveWslExePath()
{
Expand Down
35 changes: 35 additions & 0 deletions tests/OpenClaw.Connection.Tests/WindowsTcpListenerSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Diagnostics;

namespace OpenClaw.Connection.Tests;

public sealed class WindowsTcpListenerSnapshotTests
{
[Fact]
public void GetProcessCommandLine_InvalidPid_ReturnsNull()
{
Assert.Null(WindowsTcpListenerSnapshot.GetProcessCommandLine(0));
Assert.Null(WindowsTcpListenerSnapshot.GetProcessCommandLine(-1));
}

[Fact]
public async Task AwaitRedirectedOutput_ReturnsNullWhenStdoutNeverCloses()
{
using var process = Process.Start(new ProcessStartInfo
{
FileName = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh",
Arguments = OperatingSystem.IsWindows() ? "/c exit 0" : "-c \"exit 0\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
});
Assert.NotNull(process);

var never = new TaskCompletionSource<string>().Task;
var helper = Task.Run(() =>
WindowsTcpListenerSnapshot.AwaitRedirectedOutput(process, never, timeoutMs: 400));

var completed = await Task.WhenAny(helper, Task.Delay(TimeSpan.FromSeconds(3)));
Assert.Same(helper, completed);
Assert.Null(await helper);
}
}