簡體   English   中英

C# Windows 控制台應用程序如何判斷它是否以交互方式運行

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

用 C# 編寫的 Windows 控制台應用程序如何確定它是在非交互式環境(例如從服務或作為計划任務)還是從能夠進行用戶交互的環境(例如命令提示符或 PowerShell)中調用?

[編輯:4/2021 - 新答案...]

由於 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. 討論

相關問題所述, GUI控制台的最可靠指標是可執行映像的PE 標頭中的“ Subsystem ”字段。 以下 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;

盡管代碼很簡單,但我們這里的案例可以簡化。 由於我們只對我們正在運行的進程感興趣——它必須被加載,因此不需要打開任何文件或從磁盤讀取來獲取子系統值。 我們的可執行映像保證已經映射到內存中。 通過調用以下函數來檢索任何加載的文件圖像的基地址很簡單:

 [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 Header中最有用或唯一有用的信息。 但是,根據您對細節的容忍度,可能還有其他無價的花絮,並且很容易使用剛剛描述的技術來檢索其他有趣的數據。

顯然,通過更改之前使用的最終字段偏移量 ( 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覆蓋“原位”直接施加到非托管內存上,這樣就不需要進行內存復制。 然而,為了避免進一步延長這里的討論,我將展示一種更簡單的方法,它確實涉及復制。

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


4. 演示代碼的輸出

這是控制台程序運行時的輸出...

 機器類型:Amd64\n特性:ExecutableImage、LargeAddressAware\n子系統:WindowsCui (3)\n DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware

...與GUI (WPF) 應用程序相比:

 機器類型:Amd64\n特性:ExecutableImage、LargeAddressAware\n子系統:WindowsGui (2)\n DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware


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

要確定 .NET 應用程序是否在 GUI 模式下運行:

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

我還沒有測試過,但Environment.UserInteractive看起來很有希望。

如果您要做的只是確定程序退出后控制台是否會繼續存在(例如,這樣您就可以在程序退出之前提示用戶按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);
    }
}

(*) 改編自此處找到的代碼。

Glenn Slayden 解決方案的可能改進:

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

要在交互式控制台中提示用戶輸入,但在沒有控制台的情況下運行或輸入已重定向時不執行任何操作:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM