简体   繁体   中英

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

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

[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:

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. 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); // 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

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.

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


4. Output of the demo code

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

 MachineType: Amd64\nCharacteristics: ExecutableImage, LargeAddressAware\nSubsystem: WindowsCui (3)\nDllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware 

...compared to GUI (WPF) application:

 MachineType: Amd64\nCharacteristics: ExecutableImage, LargeAddressAware\nSubsystem: WindowsGui (2)\nDllCharacteristics: 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;

我还没有测试过,但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. 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();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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