简体   繁体   English

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

[英]How can a C# Windows Console application tell if it is run interactively

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

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

Due to a recent change in the Visual Studio debugger, my original answer stopped working correctly when debugging.由于 Visual Studio 调试器最近发生了变化,我的原始答案在调试时停止正常工作。 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... 1. 只是代码,请...

To determine if a .NET application is running in GUI mode:要确定 .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; } }

This checks the Subsystem value in the PE header.这会检查 PE 标头中的Subsystem值。 For a console application, the value will be 3 instead of 2 .对于控制台应用程序,该值将是3而不是2


2. Discussion 2. 讨论

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.相关问题所述, GUI控制台的最可靠指标是可执行映像的PE 标头中的“ Subsystem ”字段。 The following C# enum lists the allowable (documented) values:以下 C# enum列出了允许的(记录在案的)值:

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;

As easy as that code is, our case here can be simplified.尽管代码很简单,但我们这里的案例可以简化。 Since we are only interested in our the running process--which is necessarily loaded, it isn't necessary 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.我们的可执行映像保证已经映射到内存中。 It is simple to retrieve the base address for any loaded file image by calling the following 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.传递null ,或者在这种情况下, default(IntPtr.Zero) (与IntPtr.Zero相同),返回当前进程的虚拟内存映像的基地址。 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:这消除了必须获取条目程序集及其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;


[end of official answer] 【官方回答结束】


3. Bonus Discussion 3. 奖金讨论

For the purposes of .NET, Subsystem is perhaps the most— or only —useful piece of information in the PE Header .就 .NET 而言, Subsystem可能是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.显然,通过更改之前使用的最终字段偏移量 ( 0x5C ),您可以访问 COFF 或 PE 标头中的其他字段。 The next snippet illustrates this for Subsystem (as above) plus three additional fields with their respective offsets.下一个片段说明了Subsystem (如上所述)加上三个具有各自偏移量的附加字段。

NOTE: To reduce clutter, the enum declarations used in the following can be found here注意:为了减少混乱,可以在此处找到以下使用的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.

To improve things when accessing multiple fields in unmanaged memory, it's essential to define an overlaying struct .为了改善访问非托管内存中的多个字段时的情况,必须定义一个覆盖struct This allows for direct and natural managed access using C#.这允许使用 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:对于运行示例,我将相邻的 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; };

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.任何像这样的互操作struct都必须在运行时正确设置,并且有很多选项可以这样做。 Ideally, its generally better to impose the struct overlay " in-situ " directly on the unmanaged memory, so that no memory copying needs to occur.理想情况下,通常最好将struct覆盖“原位”直接施加到非托管内存上,这样就不需要进行内存复制。 To avoid prolonging the discussion here even further however, I will instead show an easier method that does involve copying.然而,为了避免进一步延长这里的讨论,我将展示一种更简单的方法,它确实涉及复制。

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


4. Output of the demo code 4. 演示代码的输出

Here is the output when a console program is running...这是控制台程序运行时的输出...

 MachineType: Amd64机器类型:Amd64\nCharacteristics: ExecutableImage, LargeAddressAware特性:ExecutableImage、LargeAddressAware\nSubsystem: WindowsCui (3)子系统:WindowsCui (3)\nDllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware

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

 MachineType: Amd64机器类型:Amd64\nCharacteristics: ExecutableImage, LargeAddressAware特性:ExecutableImage、LargeAddressAware\nSubsystem: WindowsGui (2)子系统:WindowsGui (2)\nDllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware


[OLD: original answer from 2012...] [旧:2012 年的原始答案...]

To determine if a .NET application is running in GUI mode:要确定 .NET 应用程序是否在 GUI 模式下运行:

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

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

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.如果您要做的只是确定程序退出后控制台是否会继续存在(例如,这样您就可以在程序退出之前提示用户按Enter ,那么您所要做的就是检查您的进程是否是唯一附加到控制台的进程。 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 . (*) 改编自此处找到的代码。

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

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM