30

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

4

6 回答 6

43

[编辑: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;
    }
}

这将检查SubsystemPE 标头中的值。对于控制台应用程序,该值将3代替2.


2. 讨论

相关问题所述, GUI 控制台最可靠的指标是可执行映像的PE 标头中的“ Subsystem”字段。以下 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,
};

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

[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 而言,它可能是PE HeaderSubsystem中最有用或唯一有用的信息。但是根据您对细节的容忍度,可能还有其他宝贵的花絮,并且使用刚刚描述的技术来检索其他有趣的数据很容易。

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

注意:为减少混乱,enum可以在此处找到以下使用的声明

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

注意:可以在此处找到此结构的完整版本,没有省略的字段

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

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;
于 2012-01-03T10:44:25.653 回答
43

Environment.UserInteractive属性

于 2009-07-27T15:00:30.430 回答
6

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

于 2009-07-27T15:00:14.347 回答
6

如果您要做的只是确定程序退出后控制台是否会继续存在(例如,您可以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);
    }
}

(*) 改编自此处找到的代码。

于 2018-05-09T01:18:39.867 回答
3

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

bool isConsoleApplication = Console.In != StreamReader.Null;
于 2019-03-17T19:19:32.143 回答
1

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

if (Environment.UserInteractive && !Console.IsInputRedirected)
{
    Console.ReadKey();
}
于 2020-11-27T17:23:18.877 回答