【问题标题】:How can a C# Windows Console application tell if it is run interactivelyC# Windows 控制台应用程序如何判断它是否以交互方式运行
【发布时间】:2010-11-14 09:22:44
【问题描述】:

用 C# 编写的 Windows 控制台应用程序如何确定它是在非交互式环境(例如从服务或作为计划任务)中调用,还是从能够与用户交互的环境(例如命令提示符或 PowerShell)中调用?

【问题讨论】:

    标签: c# console-application user-interaction


    【解决方案1】:

    [编辑:2021 年 4 月 - 新答案...]

    由于 Visual Studio 调试器最近发生了变化,我原来的答案在调试时停止正常工作。为了解决这个问题,我提供了一种完全不同的方法。原始答案的文本包含在底部。


    1.请仅提供代码...

    确定 .NET 应用程序是否在 GUI 模式下运行:

    [DllImport("kernel32.dll")] static extern IntPtr GetModuleHandleW(IntPtr _);
    
    public static bool IsGui
    {
        get
        {
            var p = GetModuleHandleW(default);
            return Marshal.ReadInt16(p, Marshal.ReadInt32(p, 0x3C) + 0x5C) == 2;
        }
    }
    

    这会检查 PE 标头中的 Subsystem 值。对于控制台应用程序,该值将是 3 而不是 2


    2.讨论

    related question 中所述,GUI vs. console 最可靠的指标是“Subsystem”字段可执行映像的PE header。以下 C# enum 列出了允许的(记录在案的)值:

    public enum Subsystem : ushort
    {
        Unknown                 /**/ = 0x0000,
        Native                  /**/ = 0x0001,
        WindowsGui              /**/ = 0x0002,
        WindowsCui              /**/ = 0x0003,
        OS2Cui                  /**/ = 0x0005,
        PosixCui                /**/ = 0x0007,
        NativeWindows           /**/ = 0x0008,
        WindowsCEGui            /**/ = 0x0009,
        EfiApplication          /**/ = 0x000A,
        EfiBootServiceDriver    /**/ = 0x000B,
        EfiRuntimeDriver        /**/ = 0x000C,
        EfiRom                  /**/ = 0x000D,
        Xbox                    /**/ = 0x000E,
        WindowsBootApplication  /**/ = 0x0010,
    };
    

    尽管代码很简单,但我们这里的案例可以简化。由于我们只对正在运行的进程感兴趣——它必须被加载,因此不需要打开任何文件或从磁盘读取来获取 subsystem 值。我们的可执行映像保证已经映射到内存中。通过调用以下函数来检索任何加载的文件图像的基地址很简单:

    [DllImport("kernel32.dll")]
    static extern IntPtr GetModuleHandleW(IntPtr lpModuleName);
    

    虽然我们可能会为这个函数提供一个文件名,但事情还是更容易,我们不必这样做。传递null,或者在本例中为default(IntPtr.Zero)(与IntPtr.Zero 相同),返回当前进程的虚拟内存映像的基地址。这消除了必须获取条目程序集及其Location 属性等的额外步骤(前面提到过)。事不宜迟,以下是新的简化代码:

    static Subsystem GetSubsystem()
    {
        var p = GetModuleHandleW(default);          // PE image VM mapped base address
        p += Marshal.ReadInt32(p, 0x3C);                // RVA of COFF/PE within DOS header
        return (Subsystem)Marshal.ReadInt16(p + 0x5C);  // PE offset to 'Subsystem' value
    }
    
    public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;
    
    public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;
    


    [官方回答结束]


    3.奖金讨论

    对于 .NET,Subsystem 可能是 PE 标头 中最有用的信息——或唯一。但是,根据您对细节的容忍度,可能还有其他宝贵的花絮,并且使用刚刚描述的技术来检索其他有趣的数据很容易。

    显然,通过更改之前使用的最终字段偏移量 (0x5C),您可以访问 COFF 或 PE 标头中的其他字段。下一个 sn-p 说明了 Subsystem(如上)加上三个附加字段及其各自的偏移量。

    注意:为了减少混乱,下面使用的 enum 声明可以找到 here

    var p = GetModuleHandleW(default);  // PE image VM mapped base address
    p += Marshal.ReadInt32(p, 0x3C);        // RVA of COFF/PE within DOS header
    
    var subsys = (Subsystem)Marshal.ReadInt16(p + 0x005C);        // (same as before)
    var machine = (ImageFileMachine)Marshal.ReadInt16(p + 0x0004);          // new
    var imgType = (ImageFileCharacteristics)Marshal.ReadInt16(p + 0x0016);  // new
    var dllFlags = (DllCharacteristics)Marshal.ReadInt16(p + 0x005E);       // new
    //                    ... etc.
    

    为了改善访问非托管内存中的多个字段时的情况,必须定义一个覆盖struct。这允许使用 C# 进行直接和自然的托管访问。对于运行示例,我将相邻的 COFF 和 PE 标头合并到以下 C# struct 定义中,并且仅包含我们认为有趣的四个字段:

    [StructLayout(LayoutKind.Explicit)]
    struct COFF_PE
    {
        [FieldOffset(0x04)] public ImageFileMachine MachineType;
        [FieldOffset(0x16)] public ImageFileCharacteristics Characteristics;
        [FieldOffset(0x5C)] public Subsystem Subsystem;
        [FieldOffset(0x5E)] public DllCharacteristics DllCharacteristics;
    };
    

    注意:可以在here

    找到此结构的完整版本,没有省略的字段

    任何诸如此类的互操作struct 都必须在运行时正确设置,并且有很多选项可以这样做。理想情况下,最好将struct 覆盖“in-situ”直接施加在非托管内存上,这样就不需要进行内存复制。然而,为了避免在这里进一步延长讨论,我将展示一个涉及复制的更简单的方法。

    var p = GetModuleHandleW(default);
    var _pe = Marshal.PtrToStructure<COFF_PE>(p + Marshal.ReadInt32(p, 0x3C));
    
    Trace.WriteLine($@"
        MachineType:        {_pe.MachineType}
        Characteristics:    {_pe.Characteristics}
        Subsystem:          {_pe.Subsystem}
        DllCharacteristics: {_pe.DllCharacteristics}");
    


    4.演示代码的输出

    这是控制台程序运行时的输出...

    机器类型:Amd64
    特点:ExecutableImage、LargeAddressAware
    子系统:WindowsCui (3)
    DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware

    ...与 GUI (WPF) 应用程序相比:

    机器类型:Amd64
    特点:ExecutableImage、LargeAddressAware
    子系统:WindowsGui (2)
    DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware


    [旧:2012 年的原始答案...]

    确定 .NET 应用程序是否在 GUI 模式下运行:

    bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;
    

    【讨论】:

    • +1 因为我遇到过这种方法有效的情况,而 Environment.UserInteractive 方法没有。这个案例是一个 NUnit 单元测试,我想在按下 ESC 键时中止测试。从 NUnit GUI 运行时,您无法调用 Console.KeyAvailable,因此我需要进行测试以了解何时跳过该代码。当我在 NUnit GUI 中运行与在控制台窗口中运行时,Glenn 的答案正确识别,而 Environment.UserInteractive 属性在两种情况下都是 TRUE。
    • @Trafz 注意System.IO是一个命名空间,这里引用的部分(Console)是在mscorlib.dll中实现的,所以您可能既没有要引用的额外程序集,也没有运行时多余的绑定。
    • 这为我工作了好几年。但是,这不再适用于最新版本的 Visual Studio(版本 16.9.3)。当您从 VS 运行应用程序时,VS 似乎正在创建自己的标准输入。如果您独立启动已编译的 .exe,它仍然可以工作,但您无法从 VS 进行调试
    • @00jt 有趣的是你刚才提到了这一点——在今天看到你的评论之后,我的回归中几乎立即出现了同样的问题(也在 VS 16.9.3 上)。确实发生了一些变化;正如你所提到的,这工作了很长时间,但显然调试器现在决定连接 stdin,这意味着也许这个长期存在的 hack 聚会已经结束了......
    • @GlennSlayden 我确实找到了另一种方法来查看 Assembly.GetEntryAssembly() 中的信息,然后使用该文件的路径并调用类似于此处所做的操作:stackoverflow.com/questions/30890104/…
    【解决方案2】:

    【讨论】:

    • fyi:“Environment.UserInteractive”在选中“允许服务与桌面交互”选项时,为服务返回true。 span>
    • 我的解决方案是简单地传递一个命令行参数来知道我处于服务模式。当我环顾四周时,我认为这是其他人也能想到的唯一确定的方法。 ;) 我敢肯定有办法,或者黑客,我只是不需要花时间去找到它。 ;) 也许有一种方法可以知道您以某种方式与服务主机挂钩(父进程?不确定)。也许您可以使用此页面上的其他答案 (stackoverflow.com/a/8711036/1236397) 来测试窗口是否打开。
    • 仅供参考:如何使用命令行开关(启动参数)安装 Windows 服务(使用 installutil)可以找到 herehere
    • 如果 FreeConsole()(在 kernel32.dll 中)被调用,这将不起作用。在我们的例子中,场景是一个同时支持命令行和交互模式的程序。它以控制台程序启动,但是当用户没有提供命令行选项时,控制台会使用 FreeConsole() 关闭。之后, Environment.UserInteractive 仍然为真。然后,最好测试 GetConsoleWindow() 是否返回有效指针。如果没有,则没有控制台。
    【解决方案3】:

    如果您要做的只是确定程序退出后控制台是否会继续存在(例如,您可以在程序退出之前提示用户点击Enter,那么您所要做的就是检查您的进程是否是唯一连接到控制台的进程。如果是,那么当您的进程退出时,控制台将被销毁。如果控制台上附加了其他进程,那么控制台将继续存在(因为您的程序不会是最后一个)。

    例如*:

    using System;
    using System.Runtime.InteropServices;
    
    namespace CheckIfConsoleWillBeDestroyedAtTheEnd
    {
        internal class Program
        {
            private static void Main(string[] args)
            {
                // ...
    
                if (ConsoleWillBeDestroyedAtTheEnd())
                {
                    Console.WriteLine("Press any key to continue . . .");
                    Console.ReadKey();
                }
            }
    
            private static bool ConsoleWillBeDestroyedAtTheEnd()
            {
                var processList = new uint[1];
                var processCount = GetConsoleProcessList(processList, 1);
    
                return processCount == 1;
            }
    
            [DllImport("kernel32.dll", SetLastError = true)]
            static extern uint GetConsoleProcessList(uint[] processList, uint processCount);
        }
    }
    

    (*) 改编自找到的代码here

    【讨论】:

    • 当我第一次问这个问题时,我很确定 Windows API 中的 GetConsoleProcessList() 不能直接从 C# 调用,所以这是一个非常好的更新。
    • @JeffLeonard GetConsoleProcessList 可以通过任何 .NET 版本的 P/Invoke 从 C# 直接调用,只要您运行的是 Windows XP 或更高版本的 Windows - docs.microsoft.com/en-us/windows/console/…
    【解决方案4】:

    我还没有测试过,但Environment.UserInteractive 看起来很有希望。

    【讨论】:

      【解决方案5】:

      Glenn Slayden 解决方案的可能改进:

      bool isConsoleApplication = Console.In != StreamReader.Null;
      

      【讨论】:

      • 感谢分享!这是否是一种改进将取决于你在寻找什么。 Console.In 会受到 Console.SetIn 的影响。在我的用例中,我正在查看正在执行的程序集是否是 WinForms。在那种情况下,我认为 Glenn 的解决方案 (Console.OpenStandardInput) 会更合适。但有选择是件好事!
      【解决方案6】:

      在交互式控制台中提示用户输入,但在没有控制台的情况下运行或输入已重定向时不执行任何操作:

      if (Environment.UserInteractive && !Console.IsInputRedirected)
      {
          Console.ReadKey();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-01
        • 1970-01-01
        • 2020-07-01
        • 2011-09-05
        • 1970-01-01
        • 1970-01-01
        • 2013-08-13
        • 1970-01-01
        相关资源
        最近更新 更多