一款用C#编写的Windows控制台应用程序如何确定它是在非交互环境(例如从服务或作为计划任务)还是从具有用户交互功能的环境(例如命令提示符或PowerShell)中调用的?
一款用C#编写的Windows控制台应用程序如何确定它是在非交互环境(例如从服务或作为计划任务)还是从具有用户交互功能的环境(例如命令提示符或PowerShell)中调用的?
[编辑:2021年4月 - 新答案...]
由于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;
}
}
Subsystem
值。对于控制台应用程序,该值将为3
而不是2
。
Subsystem
”字段。以下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,
};
GetModuleHandleW
函数,可以轻松检索任何已加载文件映像的基地址:[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandleW(IntPtr lpModuleName);
虽然我们可能会向此函数提供文件名,但再次使用默认值更容易且不必要。传递 null
,或在这种情况下,default(IntPtr.Zero)
(与 IntPtr.Zero
相同),将返回当前进程的虚拟内存映像的基地址。这消除了先前所提到的获取入口程序集及其 Location
属性等额外步骤。不再拖延,这是新的简化代码:
static Subsystem GetSubsystem()
{
var p = GetModuleHandleW(default); // VM base address of mapped PE image
p += Marshal.ReadInt32(p, 0x3C); // RVA of COFF/PE within DOS header
return (Subsystem)Marshal.ReadInt16(p + 0x5C); // PE offset to 'Subsystem' word
}
public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;
public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;
[新答案的官方结束]
对于.NET来说,Subsystem
或许是PE头文件中最有用的信息,但如果您对细节容忍度高,可能还有其他非常有价值的信息,而且可以使用上述技术轻松检索到这些数据。
显然,通过更改先前使用的最后一个字段偏移量(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
覆盖层 "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}");
当控制台程序运行时,以下是输出结果...
机器类型:Amd64 特征:可执行映像、大地址空间感知 子系统:WindowsCui (3) DLL特征:高熵VA、动态基址、Nx兼容、无SEH、TSAware
...相比于GUI(WPF)应用程序:
机器类型:Amd64 特征:可执行映像、大地址空间感知 子系统:WindowsGui (2) DLL特征:高熵VA、动态基址、Nx兼容、无SEH、TSAware
bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;
Environment.UserInteractive Property
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);
}
}
(*) 本文内容改编自此处发现的代码。
GetConsoleProcessList
可以直接通过 P/Invoke 在任何 .NET 版本中调用,只要您运行的是 Windows XP 或更高版本的 Windows - https://learn.microsoft.com/en-us/windows/console/getconsoleprocesslist#requirements - C. Augusto Proietebool isConsoleApplication = Console.In != StreamReader.Null;
在交互式控制台中提示用户输入,但在没有控制台或输入被重定向时不执行任何操作:
if (Environment.UserInteractive && !Console.IsInputRedirected)
{
Console.ReadKey();
}
Console.IsInputRedirected
似乎也可以与 docker build
一起使用。 - Matthias
System.IO
是一个命名空间,这里引用的部分(Console
)是在mscorlib.dll中实现的,因此您可能既没有额外的程序集需要引用,也没有运行时的过度绑定。 - Glenn Slayden