【发布时间】:2010-11-14 09:22:44
【问题描述】:
用 C# 编写的 Windows 控制台应用程序如何确定它是在非交互式环境(例如从服务或作为计划任务)中调用,还是从能够与用户交互的环境(例如命令提示符或 PowerShell)中调用?
【问题讨论】:
标签: c# console-application user-interaction
用 C# 编写的 Windows 控制台应用程序如何确定它是在非交互式环境(例如从服务或作为计划任务)中调用,还是从能够与用户交互的环境(例如命令提示符或 PowerShell)中调用?
【问题讨论】:
标签: c# console-application user-interaction
[编辑:2021 年 4 月 - 新答案...]
由于 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.讨论
如related question 中所述,GUI vs. console 最可靠的指标是“Subsystem”字段可执行映像的PE header。以下 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,
};
尽管代码很简单,但我们这里的案例可以简化。由于我们只对正在运行的进程感兴趣——它必须被加载,因此不需要打开任何文件或从磁盘读取来获取 subsystem 值。我们的可执行映像保证已经映射到内存中。通过调用以下函数来检索任何加载的文件图像的基地址很简单:
[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 标头 中最有用的信息——或唯一。但是,根据您对细节的容忍度,可能还有其他宝贵的花絮,并且使用刚刚描述的技术来检索其他有趣的数据很容易。
显然,通过更改之前使用的最终字段偏移量 (0x5C),您可以访问 COFF 或 PE 标头中的其他字段。下一个 sn-p 说明了 Subsystem(如上)加上三个附加字段及其各自的偏移量。
注意:为了减少混乱,下面使用的
enum声明可以找到 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.
为了改善访问非托管内存中的多个字段时的情况,必须定义一个覆盖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;
};
注意:可以在here
找到此结构的完整版本,没有省略的字段
任何诸如此类的互操作struct 都必须在运行时正确设置,并且有很多选项可以这样做。理想情况下,最好将struct 覆盖“in-situ”直接施加在非托管内存上,这样就不需要进行内存复制。然而,为了避免在这里进一步延长讨论,我将展示一个涉及复制的更简单的方法。
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.演示代码的输出
这是控制台程序运行时的输出...
机器类型:Amd64 特点:ExecutableImage、LargeAddressAware 子系统:WindowsCui (3) DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware
...与 GUI (WPF) 应用程序相比:
机器类型:Amd64 特点:ExecutableImage、LargeAddressAware 子系统:WindowsGui (2) DllCharacteristics:HighEntropyVA、DynamicBase、NxCompatible、NoSeh、TSAware
[旧:2012 年的原始答案...]
确定 .NET 应用程序是否在 GUI 模式下运行:
bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;
【讨论】:
System.IO是一个命名空间,这里引用的部分(Console)是在mscorlib.dll中实现的,所以您可能既没有要引用的额外程序集,也没有运行时多余的绑定。
【讨论】:
如果您要做的只是确定程序退出后控制台是否会继续存在(例如,您可以在程序退出之前提示用户点击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);
}
}
(*) 改编自找到的代码here。
【讨论】:
GetConsoleProcessList 可以通过任何 .NET 版本的 P/Invoke 从 C# 直接调用,只要您运行的是 Windows XP 或更高版本的 Windows - docs.microsoft.com/en-us/windows/console/…
我还没有测试过,但Environment.UserInteractive 看起来很有希望。
【讨论】:
Glenn Slayden 解决方案的可能改进:
bool isConsoleApplication = Console.In != StreamReader.Null;
【讨论】:
在交互式控制台中提示用户输入,但在没有控制台的情况下运行或输入已重定向时不执行任何操作:
if (Environment.UserInteractive && !Console.IsInputRedirected)
{
Console.ReadKey();
}
【讨论】: