Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to launch detached process through cmd.exe with no console?

I want to launch an external program from C# to be completely detached. I use CreateProcess through pinvoke because Process.Start doesn't allow me to use DETACHED_PROCESS. Also I want this application to redirect it's output to some file.

Here's the sample code:

            var processInformation = new ProcessUtility.PROCESS_INFORMATION();
            var securityInfo = new ProcessUtility.STARTUPINFO();
            var sa = new ProcessUtility.SECURITY_ATTRIBUTES();
            sa.Length = Marshal.SizeOf(sa); 

            // Create process with no window and totally detached
            var result = ProcessUtility.CreateProcess(Path.Combine(Environment.SystemDirectory, "cmd.exe"), commandLineArguments, ref sa, ref sa, false,
                ProcessUtility.DETACHED_PROCESS, IntPtr.Zero, null, ref securityInfo, out processInformation);
  1. CommandLineArguments are something like this : "/c Foo.bat > Foo.log 2>&1" Everything works fine and Foo.log is populated by Foo.bat. No additional console window is visible. PERFECT.

  2. CommandLineArguments are something like this : "/c Foo.exe > Foo.log 2>&1" Foo.exe is .NET Console Application. Foo.log is not populated and Foo.exe is launched in visible console window. STRANGE. Why behavior is different from 1. ?

  3. Just for your information. CommandLineArguments are something like this : "/c Foo.exe > Foo.log 2>&1" Foo.exe is .NET Windows Application. Everything works fine but when I launch this application simply from command prompt I see no output because no console is allocated.

I want 2. work the same as 1. Why there's a difference ?

UPDATE: I don't want to write the Foo.log for myself because launching application will be killed.

UPDATE: Ok, I wrote some code to specify that only one handle is inherited but CreateProcess gives me error 87 when calling with EXTENDED_STARTUPINFO_PRESENT ( even if it's present and empty ).

Can you please help me why?

public class ProcessUtility
{
    // Process creation flags
    const uint ZERO_FLAG = 0x00000000;
    const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
    const uint CREATE_DEFAULT_ERROR_MODE = 0x04000000;
    const uint CREATE_NEW_CONSOLE = 0x00000010;
    const uint CREATE_NEW_PROCESS_GROUP = 0x00000200;
    const uint CREATE_NO_WINDOW = 0x08000000;
    const uint CREATE_PROTECTED_PROCESS = 0x00040000;
    const uint CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000;
    const uint CREATE_SEPARATE_WOW_VDM = 0x00001000;
    const uint CREATE_SHARED_WOW_VDM = 0x00001000;
    const uint CREATE_SUSPENDED = 0x00000004;
    const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
    const uint DEBUG_ONLY_THIS_PROCESS = 0x00000002;
    const uint DEBUG_PROCESS = 0x00000001;
    const uint DETACHED_PROCESS = 0x00000008;
    const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
    const uint INHERIT_PARENT_AFFINITY = 0x00010000;

    // Thread attributes flags
    const uint PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002;
    const uint PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000;

    // File creation flags
    const uint FILE_ACCESS_WRITE = 0x40000000;

    // StartupInfo flags
    const int STARTF_USESTDHANDLES = 0x00000100;

    [StructLayout(LayoutKind.Sequential)]
    struct STARTUPINFO
    {
        public Int32 cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public Int32 dwX;
        public Int32 dwY;
        public Int32 dwXSize;
        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;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct STARTUPINFOEX
    {
        public STARTUPINFO StartupInfo;
        public IntPtr lpAttributeList;
    };

    [StructLayout(LayoutKind.Sequential)]
    struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public Int32 dwProcessID;
        public Int32 dwThreadID;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct SECURITY_ATTRIBUTES
    {
        public Int32 Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern bool CreateProcess(
        string lpApplicationName,
        string lpCommandLine,
        ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        string lpCurrentDirectory,
        [In] ref STARTUPINFO lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern bool CreateProcess(
        string lpApplicationName,
        string lpCommandLine,
        ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        string lpCurrentDirectory,
        [In] ref STARTUPINFOEX lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [DllImport("kernel32.dll")]
    public static extern uint GetLastError();

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UpdateProcThreadAttribute(
        IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue,
        IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool InitializeProcThreadAttributeList(
        IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool CloseHandle(IntPtr hObject);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern SafeFileHandle CreateFile(
        string lpFileName,
        uint fileAccess,
        [MarshalAs(UnmanagedType.U4)] FileShare fileShare,
        SECURITY_ATTRIBUTES securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        uint dwFlagsAndAttributes,
        IntPtr hTemplateFile);

    public static bool CreateProcessWithStdHandlesRedirect(string lpApplicationName, string lpCommandLine, string logFilename)
    {
        var startupInfo = new STARTUPINFOEX();
        startupInfo.StartupInfo.cb = Marshal.SizeOf(startupInfo);

        try
        {
            var lpSize = IntPtr.Zero;
            if (InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref lpSize) || lpSize == IntPtr.Zero)
                return false;
            startupInfo.lpAttributeList = Marshal.AllocHGlobal(lpSize);

            // Here startupInfo.lpAttributeList is initialized to hold 1 value
            if (!InitializeProcThreadAttributeList(startupInfo.lpAttributeList, 1, 0, ref lpSize))
                return false;

            var fileSecurityAttributes = new SECURITY_ATTRIBUTES();
            fileSecurityAttributes.Length = Marshal.SizeOf(fileSecurityAttributes);
            // Create inheritable file handle
            fileSecurityAttributes.bInheritHandle = true;

            // Open log file for writing
            using (var handle = CreateFile(logFilename, FILE_ACCESS_WRITE, FileShare.ReadWrite,
                fileSecurityAttributes, FileMode.Create, 0, IntPtr.Zero))
            {
                var fileHandle = handle.DangerousGetHandle();

                // Add filehandle to proc thread attribute list
                if (!UpdateProcThreadAttribute(startupInfo.lpAttributeList, 0, (IntPtr)PROC_THREAD_ATTRIBUTE_HANDLE_LIST, fileHandle,
                    (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero))
                    return false;

                startupInfo.StartupInfo.hStdError = fileHandle;
                startupInfo.StartupInfo.hStdOutput = fileHandle;
                // startupInfo.StartupInfo.hStdInput = ?;
                startupInfo.StartupInfo.dwFlags = STARTF_USESTDHANDLES;

                var processInformation = new PROCESS_INFORMATION();
                var securityAttributes = new SECURITY_ATTRIBUTES();
                securityAttributes.Length = Marshal.SizeOf(securityAttributes);
                securityAttributes.bInheritHandle = true;

                // Create process with no window and totally detached
                return ProcessUtility.CreateProcess(lpApplicationName, lpCommandLine, ref securityAttributes, ref securityAttributes, true,
                    DETACHED_PROCESS | EXTENDED_STARTUPINFO_PRESENT, IntPtr.Zero, null, ref startupInfo, out processInformation);
            }
        }
        finally
        {
            if (startupInfo.lpAttributeList != IntPtr.Zero)
            {
                DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
                Marshal.FreeHGlobal(startupInfo.lpAttributeList);
            }
        }
    }
}
like image 238
norekhov Avatar asked Sep 27 '22 21:09

norekhov


2 Answers

In case 1, the instance of cmd.exe that you're launching can run the batch file itself. No child process is created.

In case 2, the instance of cmd.exe that you're launching has to run the console application as a child process. It has no way of knowing that you wanted the application to not be given a console window, so when it calls CreateProcess it does not use the DETACHED_PROCESS flag, and Windows creates a new console as normal.

In case 3, the child process is not a console application, so Windows does not create a console for it even though DETACHED_PROCESS was not specified.

The usual solution is to open the foo.log file yourself, launch the console application directly (rather than via cmd.exe) and use the STARTUP_INFO structure to pass the log file handle as the standard output and standard error for the new process. Once CreateProcess has returned you can close the file handle. The duplicated handle in the child process will not be affected when your process closes.

However, I'm not sure how you would properly go about that in .NET. It's a bit tricky at the best of times, because you have to make the child process inherit the log file handle without having it inherit other handles inappropriately - that's probably the reason Process.Start is causing you problems. The recommended practice is to use a process/thread attribute list (InitializeProcThreadAttributeList) with a PROC_THREAD_ATTRIBUTE_HANDLE_LIST entry. (But the log handle still needs to be inheritable.)

like image 115
Harry Johnston Avatar answered Oct 03 '22 06:10

Harry Johnston


Create a vb script, NoWindow.vbs, perhaps programmatically on the fly, as follows

CreateObject("Wscript.Shell").Run WScript.Arguments(0), 0, False

From you main app, call cscript simply with Process.Start

Process.Start("cscript", "NoWindow.vbs \"cmd /c Foo.exe > Foo.log 2>&1 \" ");

The vbs script itself will detach the process. No window is visible.

My testing was limited to using Procexp to confirm that the proceses Foo.exe was detached - Win7.

like image 31
Patrick Avatar answered Oct 03 '22 07:10

Patrick