gpt4 book ai didi

c# - 在 .NET 应用程序中检测重定向控制台输出中的关闭管道

转载 作者:可可西里 更新时间:2023-11-01 08:46:35 25 4
gpt4 key购买 nike

.NET Console 类及其默认的 TextWriter 实现(可作为 Console.Out 隐含在例如 Console.WriteLine 中()) 当应用程序将其输出通过管道传输到另一个程序时,不会发出任何错误信号,并且另一个程序在应用程序完成之前终止或关闭管道。这意味着应用程序可能会运行超过必要的时间,将输出写入黑洞。

如何检测重定向管道另一端的关闭?

更详细的解释如下:

这里有一对演示问题的示例程序。 Produce 相当慢地打印大量整数,以模拟计算效果:

using System;
class Produce
{
static void Main()
{
for (int i = 0; i < 10000; ++i)
{
System.Threading.Thread.Sleep(100); // added for effect
Console.WriteLine(i);
}
}
}

Consume 只读取前 10 行输入然后退出:

using System;
class Consume
{
static void Main()
{
for (int i = 0; i < 10; ++i)
Console.ReadLine();
}
}

如果编译这两个程序,并且第一个程序的输出通过管道传输到第二个程序,如下所示:

Produce | Consume

...可以观察到 ProduceConsume 终止后继续运行很长时间。

实际上,我的Consume 程序是 Unix 风格的head,而我的Produce 程序打印计算成本很高的数据。我想在管道的另一端关闭连接时终止输出。

我如何在 .NET 中执行此操作?

(我知道一个明显的替代方法是传递一个命令行参数来限制输出,这确实是我目前正在做的,但我仍然想知道如何做到这一点,因为我希望能够对何时终止读取做出更多可配置的判断;例如,在 head 之前通过 grep 管道。)

更新:它看起来非常像 .NET 中的 System.IO.__ConsoleStream 实现被硬编码为忽略错误 0x6D (ERROR_BROKEN_PIPE ) 和 0xE8 (ERROR_NO_DATA)。这可能意味着我需要重新实现控制台流。唉……)

最佳答案

为了解决这个问题,我不得不在 Win32 文件句柄上编写自己的基本流实现。这并不是特别困难,因为我不需要实现异步支持、缓冲或查找。

不幸的是,需要使用不安全的代码,但这对于将在本地运行并完全信任的控制台应用程序来说通常不是问题。

这是核心流:

class HandleStream : Stream
{
SafeHandle _handle;
FileAccess _access;
bool _eof;

public HandleStream(SafeHandle handle, FileAccess access)
{
_handle = handle;
_access = access;
}

public override bool CanRead
{
get { return (_access & FileAccess.Read) != 0; }
}

public override bool CanSeek
{
get { return false; }
}

public override bool CanWrite
{
get { return (_access & FileAccess.Write) != 0; }
}

public override void Flush()
{
// use external buffering if you need it.
}

public override long Length
{
get { throw new NotSupportedException(); }
}

public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

static void CheckRange(byte[] buffer, int offset, int count)
{
if (offset < 0 || count < 0 || (offset + count) < 0
|| (offset + count) > buffer.Length)
throw new ArgumentOutOfRangeException();
}

public bool EndOfStream
{
get { return _eof; }
}

public override int Read(byte[] buffer, int offset, int count)
{
CheckRange(buffer, offset, count);
int result = ReadFileNative(_handle, buffer, offset, count);
_eof |= result == 0;
return result;
}

public override void Write(byte[] buffer, int offset, int count)
{
int notUsed;
Write(buffer, offset, count, out notUsed);
}

public void Write(byte[] buffer, int offset, int count, out int written)
{
CheckRange(buffer, offset, count);
int result = WriteFileNative(_handle, buffer, offset, count);
_eof |= result == 0;
written = result;
}

public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}

public override void SetLength(long value)
{
throw new NotSupportedException();
}

[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool ReadFile(
SafeHandle hFile, byte* lpBuffer, int nNumberOfBytesToRead,
out int lpNumberOfBytesRead, IntPtr lpOverlapped);

[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("kernel32.dll", SetLastError=true)]
static extern unsafe bool WriteFile(
SafeHandle hFile, byte* lpBuffer, int nNumberOfBytesToWrite,
out int lpNumberOfBytesWritten, IntPtr lpOverlapped);

unsafe static int WriteFileNative(SafeHandle hFile, byte[] buffer, int offset, int count)
{
if (buffer.Length == 0)
return 0;

fixed (byte* bufAddr = &buffer[0])
{
int result;
if (!WriteFile(hFile, bufAddr + offset, count, out result, IntPtr.Zero))
{
// Using Win32Exception just to get message resource from OS.
Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
int hr = ex.NativeErrorCode | unchecked((int) 0x80000000);
throw new IOException(ex.Message, hr);
}

return result;
}
}

unsafe static int ReadFileNative(SafeHandle hFile, byte[] buffer, int offset, int count)
{
if (buffer.Length == 0)
return 0;

fixed (byte* bufAddr = &buffer[0])
{
int result;
if (!ReadFile(hFile, bufAddr + offset, count, out result, IntPtr.Zero))
{
Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
int hr = ex.NativeErrorCode | unchecked((int) 0x80000000);
throw new IOException(ex.Message, hr);
}
return result;
}
}
}
如果需要,

BufferedStream 可以围绕它进行缓冲,但对于控制台输出,TextWriter 无论如何都会进行字符级缓冲,并且只会刷新换行符。

流滥用 Win32Exception 来提取错误消息,而不是调用 FormatMessage 本身。

基于这个流,我能够为控制台 I/O 编写一个简单的包装器:

static class ConsoleStreams
{
enum StdHandle
{
Input = -10,
Output = -11,
Error = -12,
}

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetStdHandle(int nStdHandle);

static SafeHandle GetStdHandle(StdHandle h)
{
return new SafeFileHandle(GetStdHandle((int) h), true);
}

public static HandleStream OpenStandardInput()
{
return new HandleStream(GetStdHandle(StdHandle.Input), FileAccess.Read);
}

public static HandleStream OpenStandardOutput()
{
return new HandleStream(GetStdHandle(StdHandle.Output), FileAccess.Write);
}

public static HandleStream OpenStandardError()
{
return new HandleStream(GetStdHandle(StdHandle.Error), FileAccess.Write);
}

static TextReader _in;
static StreamWriter _out;
static StreamWriter _error;

public static TextWriter Out
{
get
{
if (_out == null)
{
_out = new StreamWriter(OpenStandardOutput());
_out.AutoFlush = true;
}
return _out;
}
}

public static TextWriter Error
{
get
{
if (_error == null)
{
_error = new StreamWriter(OpenStandardError());
_error.AutoFlush = true;
}
return _error;
}
}

public static TextReader In
{
get
{
if (_in == null)
_in = new StreamReader(OpenStandardInput());
return _in;
}
}
}

最终的结果是,在管道的另一端终止连接后写入控制台输出,导致一个很好的异常消息:

The pipe is being closed

通过捕获并忽略最外层的 IOException,看起来我可以继续了。

关于c# - 在 .NET 应用程序中检测重定向控制台输出中的关闭管道,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/469860/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com