C# Windows 控制台应用程序如何判断它是否以交互方式运行

发布于 2024-07-29 22:35:54 字数 95 浏览 6 评论 0原文

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

How can a Windows console application written in C# determine whether it is invoked in a non-interactive environment (e.g. from a service or as a scheduled task) or from an environment capable of user-interaction (e.g. Command Prompt or PowerShell)?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

相守太难 2024-08-05 22:35:54

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

I haven't tested it, but Environment.UserInteractive looks promising.

夜访吸血鬼 2024-08-05 22:35:54

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

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

A possible improvement of Glenn Slayden's solution:

bool isConsoleApplication = Console.In != StreamReader.Null;
千寻… 2024-08-05 22:35:54

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

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

To prompt for user input in an interactive console, but do nothing when run without a console or when input has been redirected:

if (Environment.UserInteractive && !Console.IsInputRedirected)
{
    Console.ReadKey();
}
傲娇萝莉攻 2024-08-05 22:35:54

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

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

If all you're trying to do is to determine whether the console will continue to exist after your program exits (so that you can, for example, prompt the user to hit Enter before the program exits), then all you have to do is to check if your process is the only one attached to the console. If it is, then the console will be destroyed when your process exits. If there are other processes attached to the console, then the console will continue to exist (because your program won't be the last one).

For example*:

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

(*) Adapted from code found here.

无所谓啦 2024-08-05 22:35:54

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

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

<强>


1. Just the code, please...

要确定 .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 标头。 对于控制台应用程序,该值将为 3 而不是 2

<强>


2. Discussion

正如相关问题中所述,最可靠的指标GUI vs. consolePE 标头 可执行映像。 以下 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,
};

尽管该代码(在另一个答案中)很简单,但我们这里的情况可以大大简化。 由于我们只对自己正在运行的进程(必须加载)特别感兴趣,因此您不必打开任何文件或从磁盘读取来获取子系统值。 我们的可执行映像保证已经映射到内存中。 通过调用 GetModuleHandleW 函数:

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

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

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

public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;

public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;

[正式结束新答案]


3. Bonus Discussion

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

显然,通过更改之前使用的最终字段偏移量(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. Output of the demo code

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

MachineType:        Amd64
Characteristics:    ExecutableImage, LargeAddressAware
Subsystem:          WindowsCui (3)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware

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

MachineType:        Amd64
Characteristics:    ExecutableImage, LargeAddressAware
Subsystem:          WindowsGui (2)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware


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

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

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

[EDIT: 4/2021 - new answer...]

Due to a recent change in the Visual Studio debugger, my original answer stopped working correctly when debugging. To remedy this, I'm providing an entirely different approach. The text of the original answer is included at the bottom.


1. Just the code, please...

To determine if a .NET application is running in GUI mode:

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

This checks the Subsystem value in the PE header. For a console application, the value will be 3 instead of 2.


2. Discussion

As noted in a related question, the most reliable indicator of GUI vs. console is the "Subsystem" field in the PE header of the executable image. The following C# enum lists the allowable (documented) values:

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

As easy as that code (in that other answer) is, our case here can be vastly simplified. Since we are only specifically interested in our own running process (which is necessarily loaded), you don't have to open any file or read from the disk to obtain the subsystem value. Our executable image is guaranteed to be already mapped into memory. And it is simple to retrieve the base address for any loaded file image by calling the GetModuleHandleW function:

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

Although we might provide a filename to this function, again things are easier and we don't have to. Passing null, or in this case, default(IntPtr.Zero) (which is the same as IntPtr.Zero), returns the base address of the virtual memory image for the current process. This eliminates the extra steps (alluded to earlier) of having to fetch the entry assembly and its Location property, etc. Without further ado, here is the new and simplified code:

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

public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;

public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;

[official end of the new answer]


3. Bonus Discussion

For the purposes of .NET, Subsystem is perhaps the most—or only—useful piece of information in the PE Header. But depending on your tolerance for minutiae, there could be other invaluable tidbits, and it's easy to use the technique just described to retrieve additional interesting data.

Obviously, by changing the final field offset (0x5C) used earlier, you can access other fields in the COFF or PE header. The next snippet illustrates this for Subsystem (as above) plus three additional fields with their respective offsets.

NOTE: To reduce clutter, the enum declarations used in the following can be found 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.

To improve things when accessing multiple fields in unmanaged memory, it's essential to define an overlaying struct. This allows for direct and natural managed access using C#. For the running example, I merged the adjacent COFF and PE headers together into the following C# struct definition, and only included the four fields we deemed interesting:

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

NOTE: A fuller version of this struct, without the omitted fields, can be found here

Any interop struct such as this has to be properly setup at runtime, and there are many options for doing so. Ideally, its generally better to impose the struct overlay "in-situ" directly on the unmanaged memory, so that no memory copying needs to occur. To avoid prolonging the discussion here even further however, I will instead show an easier method that does involve copying.

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. Output of the demo code

Here is the output when a console program is running...

MachineType:        Amd64
Characteristics:    ExecutableImage, LargeAddressAware
Subsystem:          WindowsCui (3)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware

...compared to GUI (WPF) application:

MachineType:        Amd64
Characteristics:    ExecutableImage, LargeAddressAware
Subsystem:          WindowsGui (2)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware


[OLD: original answer from 2012...]

To determine if a .NET application is running in GUI mode:

bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文