mirror of
https://github.com/iOfficeAI/OfficeCLI
synced 2026-04-21 13:37:23 +00:00
fix(resident): drop stdout redirect to unblock parent on Windows
Redirecting the resident child's stdout caused Windows CreateProcess to set bInheritHandles=TRUE, leaking the outer shell's pipe handle into the resident. The shell then blocked for ~60s waiting for EOF on a pipe the resident still owned, even though officecli main had already exited. - Remove RedirectStandardOutput from the resident ProcessStartInfo (stderr redirect is kept for startup failure diagnostics) - Dispose the Process on all exit paths (success/exited/timeout) to release the remaining pipe handle promptly - Wrap resident-side background Console.Error.WriteLine calls in a LogStderr helper that swallows IOException, so the resident does not crash when writing diagnostics to a stderr pipe whose read-end was closed by the parent exiting
This commit is contained in:
parent
96fee3399b
commit
ae51cbce5e
2 changed files with 29 additions and 11 deletions
|
|
@ -166,7 +166,11 @@ static partial class CommandBuilder
|
|||
Arguments = $"__resident-serve__ \"{filePath}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
// Do NOT redirect stdout: on Windows, RedirectStandardOutput
|
||||
// causes bInheritHandles=TRUE which leaks the outer shell's
|
||||
// pipe handle into the resident, blocking the caller for ~60s
|
||||
// until the resident's idle timeout. Stderr is still redirected
|
||||
// to capture diagnostics if the resident fails during startup.
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
|
|
@ -185,16 +189,21 @@ static partial class CommandBuilder
|
|||
{
|
||||
Thread.Sleep(100);
|
||||
if (ResidentClient.TryConnect(filePath, out _))
|
||||
{
|
||||
process.Dispose();
|
||||
return true;
|
||||
}
|
||||
if (process.HasExited)
|
||||
{
|
||||
var stderr = process.StandardError.ReadToEnd();
|
||||
error = $"Resident process exited. {stderr}";
|
||||
process.Dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
error = "Resident process started but not responding.";
|
||||
process.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ public class ResidentServer : IDisposable
|
|||
private CancellationTokenSource _idleCts = new();
|
||||
private bool _disposed;
|
||||
|
||||
// Safe stderr logging: the parent process may have redirected our stderr
|
||||
// to a pipe whose read-end closes when the parent exits, so any
|
||||
// Console.Error.WriteLine after that point throws IOException. Swallow
|
||||
// it silently — these are best-effort diagnostics, not critical output.
|
||||
private static void LogStderr(string message)
|
||||
{
|
||||
try { Console.Error.WriteLine(message); } catch (IOException) { }
|
||||
}
|
||||
|
||||
// Valid idle-timeout range: 1s .. 24h. Anything outside falls back to
|
||||
// the 12min default. A value of "0" is rejected (would be an infinite-
|
||||
// busy spin on the watchdog task). Shared between the startup env-var
|
||||
|
|
@ -203,7 +212,7 @@ public class ResidentServer : IDisposable
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Resident error: {ex.Message}");
|
||||
LogStderr($"Resident error: {ex.Message}");
|
||||
// currentMain is still the pre-created replacement; it is
|
||||
// still valid for the next iteration's WaitForConnectionAsync.
|
||||
}
|
||||
|
|
@ -258,7 +267,7 @@ public class ResidentServer : IDisposable
|
|||
// cancelling _mainCts / _pingCts, so the "ping liveness ⇔
|
||||
// file locked" invariant is preserved end-to-end: the
|
||||
// ping pipe stays alive until handler.Dispose() completes.
|
||||
Console.Error.WriteLine($"Resident idle for {currentTimeout.TotalMinutes} minutes, closing.");
|
||||
LogStderr($"Resident idle for {currentTimeout.TotalMinutes} minutes, closing.");
|
||||
_ = ShutdownAsync();
|
||||
break;
|
||||
}
|
||||
|
|
@ -317,7 +326,7 @@ public class ResidentServer : IDisposable
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Ping responder error: {ex.Message}");
|
||||
LogStderr($"Ping responder error: {ex.Message}");
|
||||
// currentMain/current is already the replacement;
|
||||
// loop continues.
|
||||
}
|
||||
|
|
@ -383,7 +392,7 @@ public class ResidentServer : IDisposable
|
|||
try { await ShutdownAsync(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Shutdown error during __close__: {ex.Message}");
|
||||
LogStderr($"Shutdown error during __close__: {ex.Message}");
|
||||
}
|
||||
|
||||
var response = MakeResponse(0, "Closing resident.", "");
|
||||
|
|
@ -397,7 +406,7 @@ public class ResidentServer : IDisposable
|
|||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Ping handler error: {ex.Message}");
|
||||
LogStderr($"Ping handler error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -424,7 +433,7 @@ public class ResidentServer : IDisposable
|
|||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Resident error: {ex.Message}");
|
||||
LogStderr($"Resident error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1091,13 +1100,13 @@ public class ResidentServer : IDisposable
|
|||
{
|
||||
if (!ShutdownAsync().Wait(TimeSpan.FromMinutes(10)))
|
||||
{
|
||||
Console.Error.WriteLine("Warning: shutdown timed out after 10 minutes, forcing exit.");
|
||||
LogStderr("Warning: shutdown timed out after 10 minutes, forcing exit.");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Warning: shutdown error: {ex.Message}");
|
||||
LogStderr($"Warning: shutdown error: {ex.Message}");
|
||||
}
|
||||
|
||||
try { _commandLock.Dispose(); } catch { }
|
||||
|
|
@ -1160,7 +1169,7 @@ public class ResidentServer : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("Warning: timeout waiting for in-flight command to drain.");
|
||||
LogStderr("Warning: timeout waiting for in-flight command to drain.");
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException) { /* _commandLock already disposed */ }
|
||||
|
|
@ -1172,7 +1181,7 @@ public class ResidentServer : IDisposable
|
|||
try { _handler.Dispose(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Warning: handler dispose error: {ex.Message}");
|
||||
LogStderr($"Warning: handler dispose error: {ex.Message}");
|
||||
}
|
||||
|
||||
// 5. NOW cancel ping + idle. Clients observing the ping pipe from
|
||||
|
|
|
|||
Loading…
Reference in a new issue