fix exit behavior

old behavior was whenever the user types "exit" to stop the entire terminal, which is not correct (e.g. does not work correctly for nested cmd.exe sessions). Now we wait for the top-level process to exit, which I think is more correct.
Also contains a minor rename, Process -> ProcessFactory, ProcessResources -> Process.
This commit is contained in:
Will Fuqua 2018-09-21 21:50:18 +07:00
parent bf32b8d48f
commit 3a1ee61476
No known key found for this signature in database
GPG key ID: 67B6F489167C16A3
3 changed files with 86 additions and 89 deletions

View file

@ -1,7 +1,6 @@
using System;
using System.Runtime.InteropServices;
using static MiniTerm.Native.ProcessApi;
using static MiniTerm.Native.PseudoConsoleApi;
namespace MiniTerm
{
@ -11,16 +10,16 @@ namespace MiniTerm
/// <remarks>
/// Possible to replace with managed code? The key is being able to provide the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute
/// </remarks>
static class Process
static class ProcessFactory
{
/// <summary>
/// Start and configure a process. The return value should be considered opaque, and eventually disposed.
/// Start and configure a process. The return value represents the process and should be disposed.
/// </summary>
internal static ProcessResources Start(string command, IntPtr attributes, IntPtr hPC)
internal static Process Start(string command, IntPtr attributes, IntPtr hPC)
{
var startupInfo = ConfigureProcessThread(hPC, attributes);
var processInfo = RunProcess(ref startupInfo, "cmd.exe");
return new ProcessResources(startupInfo, processInfo);
return new Process(startupInfo, processInfo);
}
private static STARTUPINFOEX ConfigureProcessThread(IntPtr hPC, IntPtr attributes)
@ -93,70 +92,73 @@ namespace MiniTerm
return pInfo;
}
}
internal sealed class ProcessResources : IDisposable
/// <summary>
/// Represents an instance of a process
/// </summary>
internal sealed class Process : IDisposable
{
public Process(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo)
{
public ProcessResources(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo)
{
StartupInfo = startupInfo;
ProcessInfo = processInfo;
}
STARTUPINFOEX StartupInfo { get; }
PROCESS_INFORMATION ProcessInfo { get; }
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// dispose managed state (managed objects).
}
// dispose unmanaged state
// Free the attribute list
if (StartupInfo.lpAttributeList != IntPtr.Zero)
{
DeleteProcThreadAttributeList(StartupInfo.lpAttributeList);
Marshal.FreeHGlobal(StartupInfo.lpAttributeList);
}
// Close process and thread handles
if (ProcessInfo.hProcess != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hProcess);
}
if (ProcessInfo.hThread != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hThread);
}
disposedValue = true;
}
}
~ProcessResources()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// use the following line if the finalizer is overridden above.
GC.SuppressFinalize(this);
}
#endregion
StartupInfo = startupInfo;
ProcessInfo = processInfo;
}
public STARTUPINFOEX StartupInfo { get; }
public PROCESS_INFORMATION ProcessInfo { get; }
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// dispose managed state (managed objects).
}
// dispose unmanaged state
// Free the attribute list
if (StartupInfo.lpAttributeList != IntPtr.Zero)
{
DeleteProcThreadAttributeList(StartupInfo.lpAttributeList);
Marshal.FreeHGlobal(StartupInfo.lpAttributeList);
}
// Close process and thread handles
if (ProcessInfo.hProcess != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hProcess);
}
if (ProcessInfo.hThread != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hThread);
}
disposedValue = true;
}
}
~Process()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// use the following line if the finalizer is overridden above.
GC.SuppressFinalize(this);
}
#endregion
}
}

View file

@ -13,8 +13,8 @@ namespace MiniTerm
/// </remarks>
internal sealed class PseudoConsolePipe : IDisposable
{
public SafeFileHandle ReadSide;
public SafeFileHandle WriteSide;
public readonly SafeFileHandle ReadSide;
public readonly SafeFileHandle WriteSide;
public PseudoConsolePipe()
{

View file

@ -2,6 +2,7 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static MiniTerm.Native.ConsoleApi;
@ -49,17 +50,16 @@ namespace MiniTerm
using (var inputPipe = new PseudoConsolePipe())
using (var outputPipe = new PseudoConsolePipe())
using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, (short)Console.WindowWidth, (short)Console.WindowHeight))
using (var process = Process.Start(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle))
using (var process = ProcessFactory.Start(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle))
{
// set up a background task to copy all pseudoconsole output to stdout
// copy all pseudoconsole output to stdout
Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide));
// prompt for stdin input and send the result to the pseudoconsole
Task.Run(() => CopyInputToPipe(inputPipe.WriteSide));
// free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar)
OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe));
// prompt for stdin input and send the result to the pipe.
// blocks until the user types "exit"
CopyInputToPipe(inputPipe.WriteSide);
WaitForExit(process).WaitOne(Timeout.Infinite);
}
}
@ -75,25 +75,11 @@ namespace MiniTerm
writer.AutoFlush = true;
writer.WriteLine(@"cd \");
StringBuilder buffer = new StringBuilder();
while (true)
{
// send input character-by-character to the pipe
char key = Console.ReadKey(intercept: true).KeyChar;
writer.Write(key);
// stop the input loop if 'exit' was sent
// TODO: if we have nested cmd.exe process, this will kill all of them which is wrong.
// could we somehow detect when the top-level cmd.exe process has ended and remove this logic?
buffer.Append(key);
if (key == '\r')
{
if (buffer.ToString() == ExitCommand)
{
break;
}
buffer.Clear();
}
}
}
}
@ -123,6 +109,15 @@ namespace MiniTerm
}
}
/// <summary>
/// Get an AutoResetEvent that signals when the process exits
/// </summary>
private static AutoResetEvent WaitForExit(ProcessFactory.Process process) =>
new AutoResetEvent(false)
{
SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, false)
};
/// <summary>
/// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button).
/// Intended for resource cleanup logic.