external/NoGui.ps1
<#
NOTE: Must be run in Windows PowerShell (5.1), PowerShell (7+) cannot create standalone exes. This is designed to create a simple exe that can be used to spawn any console application with a hidden Window. As NoGui.exe is a GUI executable it won't spawn with an associated console window and can be used to then create a new process with a hidden console window with the arguments it was created with. By default, NoGui will spawn the child process with same stdio handles as itself. This allows the caller to interact with the stdio of the child process if they spawn NoGui with the proper handles. Please note that most tools which spawn a GUI application will not setup the stdio handles, in those cases the '-stdin', '-stdout', and '-stderr' arguments can be used instead. NoGui will also spawn the process and wait for it to end before it ends itself. This allows it to set the exit code of its process to the one it spawned. Use the '-nowait' argument to exit as soon as the process has been created. The command line arguments for NoGui comes in two parts: NoGui.exe [-nowait] [-logpath PATH] [-stdin PATH] [-stdout PATH] [-stderr PATH] -- child process command line The options before '--' are options to control the behaviour of NoGui.exe: -nowait Will spawn the process and not wait for it to finish before exiting. By default NoGui will spawn the process and wait for it to end to pass along the return code from the child process. If set the return code will be 0 if the process was successfully created. -log PATH The path to log the operations of NoGui for debugging purposes. The directory specified in the path must exist. -stdin PATH The path to a file to set as the stdin of the process to spawn. -stdout PATH The path to a file to set as the stdout of the process to spawn. -stdin PATH The path to a file to set as the stderr of the process to spawn. The arguments after -- will be used as the new process. This must be set otherwise NoGui won't know what process to create. Examples: Spawn pwsh.exe and wait for it to exit NoGui.exe -- pwsh.exe Run pwsh.exe from an absolute path that needs to be quoted. The exit code for NoGui.exe will based on the exit code from pwsh.exe. NoGui.exe -- "C:\Program Files\PowerShell\7\pwsh.exe" -Command "'abc' > C:\Windows\TEMP\test.txt; exit 2" Run pwsh.exe but will not wait for it to complete. NoGui.exe -nowait -- pwsh.exe -Command "'abc'" Runs NoGui with a path to log its operations for debugging NoGui.exe -log C:\temp\NoGui.log -- pwsh.exe -Command "'abc'" Runs NoGui with a custom stdout and stderr path from the current directory NoGui.exe -stdout stdout.txt -stderr stderr.exe -- pwsh.exe -Command "$host.UI.WriteLine('stdout'); $host.UI.WriteErrorLine('stderr')" Will create a new process `pwsh.exe` but will not wait for it to finish before exiting. #> Add-Type -OutputType WindowsApplication -OutputAssembly NoGui.exe -TypeDefinition @' using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Text; namespace NoGui { class Program { public const int INFINITE = -1; public const int STARTF_USESHOWWINDOW = 0x00000001; public const int STARTF_USESTDHANDLES = 0x00000100; public const int SW_HIDE = 0; [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFOW { public Int32 cb; public IntPtr lpReserved; public IntPtr lpDesktop; public IntPtr lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwYSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [DllImport("Kernel32.dll", SetLastError = true)] public static extern bool CloseHandle( IntPtr hObject); [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessW( [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory, ref STARTUPINFOW lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("Kernel32.dll", SetLastError = true)] public static extern bool GetExitCodeProcess( IntPtr hProcess, out int lpExitCode); [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)] public static extern void GetStartupInfoW( out STARTUPINFOW lpStartupInfo); [DllImport("Kernel32.dll", SetLastError = true)] public static extern int WaitForSingleObject( IntPtr hHandle, int dwMilliseconds); static int Main(string[] args) { CommandLineArgs parsedArgs = CommandLineArgs.Parse(args); NoGuiLogger logger = new NoGuiLogger(parsedArgs.LogPath); FileStream stdinStream = null; FileStream stdoutStream = null; FileStream stderrStream = null; try { logger.Log("Starting with options {0}", parsedArgs); STARTUPINFOW startupInfo; GetStartupInfoW(out startupInfo); IntPtr stdinHandle = startupInfo.hStdInput; IntPtr stdoutHandle = startupInfo.hStdOutput; IntPtr stderrHandle = startupInfo.hStdError; if (string.IsNullOrEmpty(parsedArgs.ProcessCommandLine)) { logger.Log("No command line was found"); return -1; } if (!string.IsNullOrEmpty(parsedArgs.StdinPath)) { if (!File.Exists(parsedArgs.StdinPath)) { logger.Log("Path for stdin '{0}' was not found", parsedArgs.StdinPath); return -1; } stdinStream = File.Open(parsedArgs.StdinPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Inheritable); stdinHandle = stdinStream.SafeFileHandle.DangerousGetHandle(); } if (!string.IsNullOrEmpty(parsedArgs.StdoutPath)) { if (!Directory.Exists(Path.GetDirectoryName(parsedArgs.StdoutPath))) { logger.Log("Parent directory for stdout '{0}' was not found", parsedArgs.StdoutPath); return -1; } stdoutStream = File.Open(parsedArgs.StdoutPath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Inheritable); stdoutHandle = stdoutStream.SafeFileHandle.DangerousGetHandle(); } if (!string.IsNullOrEmpty(parsedArgs.StderrPath)) { if (!Directory.Exists(Path.GetDirectoryName(parsedArgs.StderrPath))) { logger.Log("Parent directory for stderr '{0}' was not found", parsedArgs.StderrPath); return -1; } if (parsedArgs.StderrPath == parsedArgs.StdoutPath) { stderrHandle = stdoutHandle; } else { stderrStream = File.Open(parsedArgs.StderrPath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Inheritable); stderrHandle = stderrStream.SafeFileHandle.DangerousGetHandle(); } } return StartProcess( parsedArgs.ProcessCommandLine, parsedArgs.NoWait, stdinHandle, stdoutHandle, stderrHandle, logger); } catch (Exception e) { logger.Log("Uncaught exception {0}", e.ToString()); return -1; } finally { if (stdinStream != null) stdinStream.Dispose(); if (stdoutStream != null) stdoutStream.Dispose(); if (stderrStream != null) stderrStream.Dispose(); logger.Dispose(); } } private static int StartProcess( string commandLine, bool noWait, IntPtr stdin, IntPtr stdout, IntPtr stderr, NoGuiLogger logger) { STARTUPINFOW si = new STARTUPINFOW() { cb = Marshal.SizeOf<STARTUPINFOW>(), dwFlags = STARTF_USESHOWWINDOW, wShowWindow = SW_HIDE, hStdInput = stdin, hStdOutput = stdout, hStdError = stderr, }; if (stdin != IntPtr.Zero || stdout != IntPtr.Zero || stderr != IntPtr.Zero) { logger.Log( "Adding STARTF_USESTDHANDLES flag - stdin 0x{0:X8} - stdout 0x{1:X8} - stderr 0x{2:X8}", stdin.ToInt64(), stdout.ToInt64(), stderr.ToInt64()); si.dwFlags |= STARTF_USESTDHANDLES; } PROCESS_INFORMATION pi; StringBuilder commandLineBuffer = new StringBuilder(commandLine); logger.Log("Starting new process with command line - {0}", commandLine); bool res = CreateProcessW( null, commandLineBuffer, IntPtr.Zero, IntPtr.Zero, true, 0x00000410, // CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT IntPtr.Zero, null, ref si, out pi ); if (res) { logger.Log("Child process successfully starts with PID {0}", pi.dwProcessId); try { if (noWait) { logger.Log("-nowait has been set, exiting early"); return 0; } logger.Log("Waiting for process to end"); int waitRes = WaitForSingleObject(pi.hProcess, INFINITE); if (waitRes == 0) { logger.Log("Process ended, getting exit code"); int exitCode; if (GetExitCodeProcess(pi.hProcess, out exitCode)) { logger.Log("Process exit code {0}", exitCode); return exitCode; } else { Win32Exception exp = new Win32Exception(); logger.Log("GetExitCodeProcess failed 0x{0:X8} - {1}", exp.NativeErrorCode, exp.Message); } } else { Win32Exception exp = new Win32Exception(); logger.Log("WaitForSingleObject failed 0x{0:X8} - {1}", exp.NativeErrorCode, exp.Message); } } finally { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } } else { Win32Exception exp = new Win32Exception(); logger.Log("CreateProcess failed 0x{0:X8} - {1}", exp.NativeErrorCode, exp.Message); } return -1; } } } internal class CommandLineArgs { [DllImport("Kernel32.dll")] private static extern IntPtr GetCommandLineW(); private CommandLineArgs( string processCommandLine, bool noWait, string stdinPath, string stdoutPath, string stderrPath, string logPath) { ProcessCommandLine = processCommandLine; NoWait = noWait; StdinPath = stdinPath; StdoutPath = stdoutPath; StderrPath = stderrPath; LogPath = logPath; } public string ProcessCommandLine { get; private set; } public bool NoWait { get; private set; } public string StdinPath { get; private set; } public string StdoutPath { get; private set; } public string StderrPath { get; private set; } public string LogPath { get; private set; } public static CommandLineArgs Parse(string[] args) { string processCommandLine = null; bool noWait = false; string stdinPath = null; string stdoutPath = null; string stderrPath = null; string logPath = null; // Do not free this, it's a pointer to an address in the PEB. // We get the raw command line rather than use the args so we // don't need to worry about escaping. IntPtr cmdLinePtr = GetCommandLineW(); string cmdLine = Marshal.PtrToStringUni(cmdLinePtr); int cmdLineArgsIdx = cmdLine.IndexOf(" -- "); if (cmdLineArgsIdx != -1) { processCommandLine = cmdLine.Substring(cmdLineArgsIdx + 4); } for (int i = 0; i < args.Length; i++) { string arg = args[i]; string nextArg = null; if ((i + 1) < args.Length) { nextArg = args[i + 1]; } if (arg == "--") { break; } else if (arg.Equals("-nowait", StringComparison.OrdinalIgnoreCase)) { noWait = true; } else if (nextArg != null && nextArg != "--") { if (arg.Equals("-stdin", StringComparison.OrdinalIgnoreCase)) { stdinPath = Path.GetFullPath(nextArg); i++; } else if (arg.Equals("-stdout", StringComparison.OrdinalIgnoreCase)) { stdoutPath = Path.GetFullPath(nextArg); i++; } else if (arg.Equals("-stderr", StringComparison.OrdinalIgnoreCase)) { stderrPath = Path.GetFullPath(nextArg); i++; } else if (arg.Equals("-log", StringComparison.OrdinalIgnoreCase)) { logPath = Path.GetFullPath(nextArg); i++; } } } return new CommandLineArgs( processCommandLine, noWait, stdinPath, stdoutPath, stderrPath, logPath); } public override string ToString() { return string.Format( "CommandLineArgs(ProcessCommandLine={0}, NoWait={1}, StdinPath={2}, StdoutPath={3}, StderrPath={4}, LogPath={5})", ProcessCommandLine, NoWait, StdinPath, StdoutPath, StderrPath, LogPath); } } internal class NoGuiLogger : IDisposable { private FileStream _fs; private StreamWriter _sw; public NoGuiLogger(string path) { if (string.IsNullOrEmpty(path) || !Directory.Exists(Path.GetDirectoryName(path))) { return; } _fs = File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read); _sw = new StreamWriter(_fs, new UTF8Encoding(false)); } public void Log(string msg, params object[] args) { if (_sw == null) { return; } string timeStamp = string.Format("{0} - ", DateTime.Now.ToString("hh:mm:ss.f")); _sw.WriteLine(timeStamp + string.Format(msg, args)); _sw.Flush(); } public void Dispose() { if (_sw != null) _sw.Dispose(); if (_fs != null) _fs.Dispose(); GC.SuppressFinalize(this); } ~NoGuiLogger() { Dispose(); } } '@ |