使用AllocConsole和目标架构x86时没有控制台输出

31

我有一个WinForms项目,如果用户想要一个调试控制台,我会使用AllocConsole()分配一个控制台。

当目标架构设置为“Any CPU”时,所有控制台输出都正常工作,但当我将其更改为“x86”时,不会输出任何内容(Console.Read()仍按预期工作)。 如果我直接打开EXE,则输出有效。 看起来Visual Studio将其重定向到自己的“输出”窗口中。

我还尝试了这个回答,但它没有起作用,我还尝试了Console.SetOut(GetStdHandle(-11)),也没有起作用。

对我来说,将目标架构设置为“Any CPU”不是选择。

因此,这是我的两个问题:

  • 为什么只有在将目标架构设置为x86时才会出现这种情况?
  • 如何在Visual Studio内运行时输出到我的控制台?

1
请在输出窗口查找“First chance”异常。 - Hans Passant
1
@HansPassant:不,没有异常,但是您的评论帮助我找到了(部分)解决方案:当我直接打开.exe文件时,它可以工作,但是当我调试我的解决方案时,所有输出都会显示在VS的“输出”视图中。 但仍有两个问题:为什么只有x86和如何使控制台输出在我调试解决方案时可见?(仍然+1给你:D) - teamalpha5441
5个回答

39

当启用“启用本机代码调试”时,使用AllocConsole创建的控制台输出将重定向到调试输出窗口。

之所以只在x86中发生而不是AnyCPU,是因为您只能在x86应用程序中调试本地代码。

请注意,此行为仅适用于使用AllocConsole创建的控制台。控制台应用程序的输出不会被重定向。

编辑:控制台不输出文本的另一个原因是在调用AllocConsole之前已写入控制台。

无论原因如何,此代码都将恢复重定向的输出,并在控制台无效的情况下重新打开它。 它使用魔术数字7,这通常是stdout句柄的值。

using System;
using System.IO;
using System.Runtime.InteropServices;

public static class ConsoleHelper
{
    public static void CreateConsole()
    {
        AllocConsole();

        // stdout's handle seems to always be equal to 7
        IntPtr defaultStdout = new IntPtr(7);
        IntPtr currentStdout = GetStdHandle(StdOutputHandle);

        if (currentStdout != defaultStdout)
            // reset stdout
            SetStdHandle(StdOutputHandle, defaultStdout);

        // reopen stdout
        TextWriter writer = new StreamWriter(Console.OpenStandardOutput()) 
        { AutoFlush = true };
        Console.SetOut(writer);
    }

    // P/Invoke required:
    private const UInt32 StdOutputHandle = 0xFFFFFFF5;
    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(UInt32 nStdHandle);
    [DllImport("kernel32.dll")]
    private static extern void SetStdHandle(UInt32 nStdHandle, IntPtr handle);
    [DllImport("kernel32")]
    static extern bool AllocConsole();
}

请见如何检测控制台输入(Console.In, stdin)是否被重定向?,另一种检测控制台处理程序是否被重定向的方法。


听起来不错,但奇怪的是,我的项目属性中禁用了本地代码调试... - teamalpha5441
5
非常感谢您的精彩回答。我想补充一点,使用十六进制数值 0x7 作为默认标准输出句柄并不是很安全,因为这不是一个真正的句柄,很可能是一个未记录的特性。最好使用 CreateFile(L"CONOUT$") 来查找实际的控制台屏幕缓冲区,并进行测试。 - ceztko
2
在C++中,完整的指令是CreateFile(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);。将其移植到C#应该不会非常困难。 - ceztko
5
注意:在Windows Server 2012 R2上,默认的标准输出句柄不是7。看起来该值是1228(注意:我使用的服务器上还安装了Citrix)。如果我强制使用7,就什么也不会显示。 - James Wilkins
2
@tunafish24,在调用AllocConsole之前,通过SetStdHandle将感兴趣的句柄重置为NULL。不要在之后调用SetStdHandle。不需要对句柄值做任何假设。在Windows 8+中,它将不是传统控制台伪句柄(即3、7、11、15等),而是ConDrv设备上文件的内核句柄(例如“\Device\ConDrv\Output”),其句柄值是4的倍数(例如28、112、448)的任意句柄值。 - Eryk Sun
显示剩余5条评论

29

早期的答案都对我在VS2017和Windows 10下不太有效(例如在调试模式下启动应用程序时会失败)。

下面是代码的一点改进。想法是相同的,但移除了魔数(Ceztko已经提到了这一点),并且初始化了所有必要的输入/输出流。

如果创建一个新控制台(alwaysCreateNewConsole = true),则此代码适用于我的情况。

附加到父进程控制台(alwaysCreateNewConsole = false)有几个缺点。例如,我无法完全模仿从cmd启动的控制台应用程序的行为。而且我不确定它是否可能。

最重要的是,在审查了Console类之后,我重新考虑了使用手动创建的控制台与Console类的总体想法。它对大多数情况都有效(我希望如此),但未来可能会带来很多问题。

    static class WinConsole
    {
        static public void Initialize(bool alwaysCreateNewConsole = true)
        {
            bool consoleAttached = true;
            if (alwaysCreateNewConsole
                || (AttachConsole(ATTACH_PARRENT) == 0
                && Marshal.GetLastWin32Error() != ERROR_ACCESS_DENIED))
            {
                consoleAttached = AllocConsole() != 0;
            }

            if (consoleAttached)
            {
                InitializeOutStream();
                InitializeInStream();
            }
        }

        private static void InitializeOutStream()
        {
            var fs = CreateFileStream("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, FileAccess.Write);
            if (fs != null)
            {
                var writer = new StreamWriter(fs) { AutoFlush = true };
                Console.SetOut(writer);
                Console.SetError(writer);
            }
        }

        private static void InitializeInStream()
        {
            var fs = CreateFileStream("CONIN$", GENERIC_READ, FILE_SHARE_READ, FileAccess.Read);
            if (fs != null)
            {
                Console.SetIn(new StreamReader(fs));
            }
        }

        private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode,
                                FileAccess dotNetFileAccess)
        {
            var file = new SafeFileHandle(CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero), true);
            if (!file.IsInvalid)
            {
                var fs = new FileStream(file, dotNetFileAccess);
                return fs;
            }
            return null;
        }

        #region Win API Functions and Constants
        [DllImport("kernel32.dll",
            EntryPoint = "AllocConsole",
            SetLastError = true,
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.StdCall)]
        private static extern int AllocConsole();

        [DllImport("kernel32.dll",
            EntryPoint = "AttachConsole",
            SetLastError = true,
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.StdCall)]
        private static extern UInt32 AttachConsole(UInt32 dwProcessId);

        [DllImport("kernel32.dll",
            EntryPoint = "CreateFileW",
            SetLastError = true,
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.StdCall)]
        private static extern IntPtr CreateFileW(
              string lpFileName,
              UInt32 dwDesiredAccess,
              UInt32 dwShareMode,
              IntPtr lpSecurityAttributes,
              UInt32 dwCreationDisposition,
              UInt32 dwFlagsAndAttributes,
              IntPtr hTemplateFile
            );

        private const UInt32 GENERIC_WRITE = 0x40000000;
        private const UInt32 GENERIC_READ = 0x80000000;
        private const UInt32 FILE_SHARE_READ = 0x00000001;
        private const UInt32 FILE_SHARE_WRITE = 0x00000002;
        private const UInt32 OPEN_EXISTING = 0x00000003;
        private const UInt32 FILE_ATTRIBUTE_NORMAL = 0x80;
        private const UInt32 ERROR_ACCESS_DENIED = 5;

        private const UInt32 ATTACH_PARRENT = 0xFFFFFFFF;

        #endregion
    }

在我的VS2017中无法工作(或在禁用vhost的情况下使用VS2015)。 - ZorgoZ
3
在W10的VS2017/VS2019上运行得很好,但在Windows 7上运行时就不行了。 - Kevin Kouketsu
即使控制台方法是WinApi而不是StdCall,大多数EntryPoint和CharSet在这里也是无用的,但它仍然可以在W10 / VS2019上工作!! - Yet Another Code Maker
谢谢,这是唯一对我有用的东西/VS 2019。 - Keytrap
在VS 2019上运行良好。 - mbpakalin
显示剩余2条评论

8

以下方法适用于我在vs 2015中使用,其他答案都不适用:

来源:https://social.msdn.microsoft.com/profile/dmitri567/?ws=usercard-mini

using System;   
using System.Windows.Forms;   
using System.Text;   
using System.IO;   
using System.Runtime.InteropServices;   
using Microsoft.Win32.SafeHandles;   

namespace WindowsApplication   
{   
    static class Program   
    {   
        [DllImport("kernel32.dll",   
            EntryPoint = "GetStdHandle",   
            SetLastError = true,   
            CharSet = CharSet.Auto,   
            CallingConvention = CallingConvention.StdCall)]   
        private static extern IntPtr GetStdHandle(int nStdHandle);   
        [DllImport("kernel32.dll",   
            EntryPoint = "AllocConsole",   
            SetLastError = true,   
            CharSet = CharSet.Auto,   
            CallingConvention = CallingConvention.StdCall)]   
        private static extern int AllocConsole();   
        private const int STD_OUTPUT_HANDLE = -11;   
        private const int MY_CODE_PAGE = 437;   

        static void Main(string[] args)   
        {   
            Console.WriteLine("This text you can see in debug output window.");   

            AllocConsole();   
            IntPtr stdHandle=GetStdHandle(STD_OUTPUT_HANDLE);   
            SafeFileHandle safeFileHandle = new SafeFileHandle(stdHandle, true);   
            FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write);   
            Encoding encoding = System.Text.Encoding.GetEncoding(MY_CODE_PAGE);   
            StreamWriter standardOutput = new StreamWriter(fileStream, encoding);   
            standardOutput.AutoFlush = true;   
            Console.SetOut(standardOutput);   

            Console.WriteLine("This text you can see in console window.");   

            MessageBox.Show("Now I'm happy!");   
        }   
    }   
}  

在VS2017中对我没用- 调试,任何CPU(两个消息都输出到调试窗口) - FastAl

5
我也遇到了这个问题。每次尝试调试我的应用程序时,控制台都是空白的。奇怪的是,不使用调试器启动exe文件可以正常工作。
我发现我必须从项目的“调试”菜单中“启用Visual Studio托管进程”。
Stephen正确指出,“启用本机代码调试”确实将控制台重定向到“输出”窗口。然而,无论本机代码调试设置如何,直到我启用了Visual Studio托管进程,我都没有看到任何输出。
这可能是仅禁用本机代码调试未能解决您的问题的原因。

2
这个选项在Visual Studio 2017中已被移除。 - Demonslay335

2
请看Visual Studio开发者社区的最初回答: https://developercommunity.visualstudio.com/content/problem/12166/console-output-is-gone-in-vs2017-works-fine-when-d.html 请查看Ramkumar Ramesh的回答。 我在VS 2017中测试了这段代码。 我花了一天时间才找到这个答案。希望对你有所帮助。 编辑 - - Mike建议加入一些描述。我想建议一些更正,Zuniar使用VS 2015测试过,但在VS 2017中不起作用。 请使用kernel32.dll中的CreateFile引用,而不是GetStdHandle。
IntPtr stdHandle = CreateFile("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, 0, 
OPEN_EXISTING, 0, 0);

"最初的回答" 变成 "原始答案" 请在添加上述代码之前先声明。
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateFile(string lpFileName, uint 
dwDesiredAccess, uint dwShareMode, uint lpSecurityAttributes, uint 
dwCreationDisposition, uint dwFlagsAndAttributes, uint hTemplateFile);

private const int MY_CODE_PAGE = 437;
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_WRITE = 0x2;        
private const uint OPEN_EXISTING = 0x3;

我已从给定的链接中获取了这段代码。

我已经从给定的链接中获取了这段代码。


1
如果链接内容发生变化,您应该在此处包含答案的描述。 - user11563547

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接