[英]How can a C# Windows Console application tell if it is run interactively
用 C# 編寫的 Windows 控制台應用程序如何確定它是在非交互式環境(例如從服務或作為計划任務)還是從能夠進行用戶交互的環境(例如命令提示符或 PowerShell)中調用?
[編輯:4/2021 - 新答案...]
由於 Visual Studio 調試器最近發生了變化,我的原始答案在調試時停止正常工作。 為了解決這個問題,我提供了一種完全不同的方法。 原始答案的文本包含在底部。
要確定 .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
。
如相關問題所述, 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;
【官方回答結束】
就 .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
這是控制台程序運行時的輸出...
機器類型: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.