// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
namespace autoShell.Tests;
///
/// Manages an autoShell.exe child process with redirected stdin/stdout
/// for end-to-end testing of the JSON command protocol.
///
internal sealed class AutoShellProcess : IDisposable
{
private static readonly string s_exePath = Path.Combine(AppContext.BaseDirectory, "autoShell.exe");
private readonly Process _process;
private AutoShellProcess(Process process)
{
_process = process;
}
///
/// Starts autoShell.exe in interactive (stdin) mode with redirected I/O.
///
public static AutoShellProcess StartInteractive()
{
var psi = new ProcessStartInfo
{
FileName = s_exePath,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var process = Process.Start(psi)
?? throw new InvalidOperationException("Failed to start autoShell.exe");
return new AutoShellProcess(process);
}
///
/// Starts autoShell.exe with command-line arguments (non-interactive mode).
/// Returns stdout content and exit code after the process completes.
///
public static (string Output, int ExitCode) RunWithArgs(string args, int timeoutMs = 10000)
{
var psi = new ProcessStartInfo
{
FileName = s_exePath,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = Process.Start(psi)!;
string output = process.StandardOutput.ReadToEnd();
bool exited = process.WaitForExit(timeoutMs);
if (!exited)
{
process.Kill();
throw new TimeoutException($"autoShell.exe did not exit within {timeoutMs}ms");
}
return (output, process.ExitCode);
}
///
/// Sends a JSON command string to stdin, terminated with \r\n.
///
public void SendCommand(string json)
{
_process.StandardInput.WriteLine(json);
_process.StandardInput.Flush();
}
///
/// Reads a single line of stdout with a timeout.
/// Returns null if the timeout expires before a line is available.
/// Uses a lock to prevent concurrent reads on the same stream.
///
private readonly SemaphoreSlim _readLock = new(1, 1);
public async Task ReadLineAsync(int timeoutMs = 5000)
{
await _readLock.WaitAsync();
try
{
using var cts = new CancellationTokenSource(timeoutMs);
return await _process.StandardOutput.ReadLineAsync(cts.Token).AsTask();
}
catch (OperationCanceledException)
{
return null;
}
finally
{
_readLock.Release();
}
}
///
/// Sends a quit command and waits for the process to exit.
///
public void SendQuit(int timeoutMs = 5000)
{
SendCommand("""{"actionName":"quit","parameters":{}}""");
_process.WaitForExit(timeoutMs);
}
///
/// Returns true if the process has exited.
///
public bool HasExited => _process.HasExited;
///
/// Closes the stdin stream (sends EOF).
///
public void CloseStdin()
{
_process.StandardInput.Close();
}
///
/// Waits for the process to exit within the given timeout.
///
public bool WaitForExit(int timeoutMs)
{
return _process.WaitForExit(timeoutMs);
}
public void Dispose()
{
try
{
if (!_process.HasExited)
{
_process.Kill();
_process.WaitForExit(3000);
}
}
catch { }
_process.Dispose();
}
}