如何在没有控制台的情况下通过 cmd.exe 启动分离进程?

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

我想从 C# 启动一个完全分离的外部程序。 我通过 pinvoke 使用 CreateProcess,因为 Process.Start 不允许我使用 DETACHED_PROCESS。我还希望此应用程序将其输出重定向到某个文件。

示例代码如下:

            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 是这样的:“/c Foo.bat > Foo.log 2>&1” 一切正常,Foo.log 由 Foo.bat 填充。没有其他控制台 window 可见。完美。

  2. CommandLineArguments 是这样的:“/c Foo.exe > Foo.log 2>&1” Foo.exe 是 .NET 控制台应用程序。 Foo.log 未填充,Foo.exe 在可见控制台 window 中启动。奇怪的。为什么行为与 1. 不同?

  3. 仅供参考。 CommandLineArguments 是这样的:“/c Foo.exe > Foo.log 2>&1” Foo.exe 是 .NET Windows 应用程序。 一切正常,但是当我只是从命令提示符启动这个应用程序时,我看不到任何输出,因为没有分配控制台。

我想要 2. 和 1 一样工作。为什么会有区别?

更新:我不想为自己编写 Foo.log 因为启动应用程序会被杀死。

更新:好的,我写了一些代码来指定只有一个句柄被继承但是 CreateProcess 在调用 EXTENDED_STARTUPINFO_PRESENT 时给我错误 87(即使它存在并且是空的)。

你能帮我看看为什么吗?

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);
            }
        }
    }
}

检查这个答案以隐藏控制台:

How to Run a C# console application with the console hidden

这个启动分离进程的答案:

Process Tree

在情况 1 中,您要启动的 cmd.exe 实例可以 运行 批处理文件本身。没有创建子进程。

在情况 2 中,您要启动的 cmd.exe 实例必须 运行 控制台应用程序作为子进程。它无法知道您希望应用程序不被赋予控制台 window,因此当它调用 CreateProcess 时它不使用 DETACHED_PROCESS 标志,并且 Windows 创建一个新的控制台正常。

在情况 3 中,子进程不是控制台应用程序,因此即使未指定 DETACHED_PROCESS,Windows 也不会为其创建控制台。

通常的解决方案是自己打开foo.log文件,直接启动控制台应用程序(而不是通过cmd.exe)并使用STARTUP_INFO结构传递日志文件句柄作为新过程的标准输出和标准错误。一旦 CreateProcess 返回,您就可以关闭文件句柄。当您的进程关闭时,子进程中重复的句柄不会受到影响。

但是,我不确定您将如何在 .NET 中正确地处理它。在最好的情况下这有点棘手,因为您必须让子进程继承日志文件句柄而不让它不恰当地继承其他句柄 - 这可能是 Process.Start 导致您出现问题的原因。推荐的做法是使用带有 PROC_THREAD_ATTRIBUTE_HANDLE_LIST 条目的 process/thread 属性列表 (InitializeProcThreadAttributeList)。 (但日志句柄仍然需要可继承。)

创建一个 vb 脚本,NoWindow.vbs,也许是动态编程,如下所示

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

在您的主应用程序中,只需使用 Process.Start

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

vbs 脚本本身将分离进程。没有 window 可见。

我的测试仅限于使用 Procexp 来确认进程 Foo.exe 已分离 - Win7。