-
-
Save jborean93/d8e84c4ab744d0237a1ff650acec1086 to your computer and use it in GitHub Desktop.
<# | |
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(); } | |
} | |
'@ |
@zett42 thanks for the link, the reason why I never went with the .NET Process class is because it didn't support ProcessStartInfo.WindowStyle
unless you used ShellExecute = true
which then has its own set of problems. .NET did change this in .NET 8 (with a PR I sent through) but as this is being compiled with .NET Framework it won't pick up the new changes. The CreateNoWindow
argument corresponds to CREATE_NO_WINDOW
in the process creation args and while it won't spawn the console window it might have some unexpected consequences if the console application then tries to use anything in the console and expects it to just exist. Plus using CreateProcess
directly gives me finer control over how I can spawn the process and in the future I could more easily set the stdio handles rather than rely on .NET's limited subset of control over that.
This can certainly be expanded to support waiting for the child process and returning the rc, it's the reason why I added --
to separate future options for NoGui.cs
vs arguments to pass through to the new process I just never got around to it. Error handling is another thing that would also be nice but I didn't want to try and figure out how to get that data back in a nice way vs just popping up a msg box but people can certainly change it up if they wish.
I'll try and update it so it will wait and passthrough the rc by default and have an option to just spawn and end. Keep in mind if you did Start-Process NoGui.exe -ArgumentList '-- ...' -Wait
it would wait for any child process spawned anyway, you just wouldn't be able to get the RC.
Edit: It has been expanded to wait and set the RC as the child process' RC by default. You can do NoGui.exe -nowait -- ...
to go back to the old behaviour. It also inherits the stdio handles if present but can also be overridden with the -stdin
, -stdout
, and -stderr
arguments if you cannot set the stdio when calling NoGui.exe
.
Ahh, I wasn't aware of the WindowStyle
/ CreateNoWindow
problematic. I've added a related note to my SO answer and renamed to NoConsole.exe, to avoid appearing like a fork of your Gist. Appreciate the enhancements to NoGui.
Sounds great, using the message boxes in your version is a nice way to display the errors in the exe.
Here is an extended version of this utility that waits for the child process to exit and returns its exit code. It also includes improved error handing.
https://stackoverflow.com/a/79566616/7571258