3

我需要拦截控制台输出流以便将其捕获为日志,但仍将内容传递给原始流,以便应用程序正常工作。这显然意味着在将原始Console.OutTextWriter 更改为Console.SetOut(new MyTextWriterClass(originalOut)).

我假设获取 Out 属性和调用 SetOut() 方法的各个操作Console是以线程安全的方式实现的。但我想确保其他一些线程(例如,运行我无法控制且无法更改的客户端应用程序代码,因此我不能依赖我自己的自定义锁定方案)不能在我的 get 和 set 之间意外更改它,最终被我对它的更改覆盖(破坏了他们的应用程序的行为!)。由于其他代码可能只是调用 SetOut(),因此我的代码理想情况下应该获得内部使用的相同锁Console(假设有一个)。

不幸的是,Console是一个(静态)类,而不是一个实例,所以你不能只是lock (Console). 在类文档中查看似乎没有提到锁定。这不是这些 Console 方法的通常预期用法,但应该有一些安全的方法来作为原子操作执行此操作。

如果标准锁定方案失败,是否有其他方法可以确保这一点?对于如此短的关键部分(并且只完成一次),即使是暂时阻塞所有其他线程也是可以接受的,如果这是唯一的方法的话。我们正在使用 C# 和 .NET2.0。

如果这甚至不可能(不中断客户端应用程序),那么我们将不得不依赖它,客户端应用程序不太可能重定向其控制台输出碰巧在我们的 get 和 set 操作之间进行。我只想覆盖所有的基础,以防万一。

编辑:现在我们有了示例代码的具体答案,我已经改写了问题标题,以更普遍地反映答案可以提供帮助的用例,更清楚。此外,还为“原子”添加了一个标签。

4

4 回答 4

2

如果您查看 SetOut 的实现,它对我来说看起来是线程安全的:

[HostProtection(SecurityAction.LinkDemand, UI=true)]
public static void SetOut(TextWriter newOut)
{
    if (newOut == null)
    {
        throw new ArgumentNullException("newOut");
    }
    new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
    _wasOutRedirected = true;
    newOut = TextWriter.Synchronized(newOut);
    lock (InternalSyncObject)
    {
        _out = newOut;
    }
}

编辑

我能想出的最接近解决方案的方法是使用反射来获取它们的 InternalSyncObject,然后锁定它。

请注意,这是一个非常糟糕的主意,只有在没有其他选项存在时才应使用。您可能会导致框架出现意外行为并导致进程崩溃。

您还需要注意任何服务包和主要版本,以确保仍然使用内部变量。由于它是内部的,因此无法保证它会在下一个版本中出现。如果您不是具有反射的对象,请编写您的代码防御性并尝试很好地降低用户体验。

祝你好运:-)

于 2008-12-15T14:46:56.467 回答
1

好的,现在我有一些时间来实际研究 Josh 建议的反射方法的实现。基本代码(在我的ConsoleIntercepter类中,继承自TextWriter)是:

private static object GetConsoleLockObject()
{
    object lockObject;
    try
    {
        const BindingFlags bindingFlags = BindingFlags.GetProperty |
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
        // It's currently private, but we'd be happy if it were public, too.
        Type consoleType = typeof(Console);

        lockObject = consoleType.InvokeMember("InternalSyncObject", bindingFlags,
                                              null, null, null);
    }
    catch
    {
        lockObject = null; // Return null on any failure.
    }
    return lockObject;
}
public static void RegisterConsoleIntercepter()
{
    object lockObject = GetConsoleLockObject();
    if (lockObject != null)
    {
        // Great!  We can make sure any other changes happen before we read
        // or after we've written, making this an atomic replacement operation.
        lock (lockObject)
        {
            DoIntercepterRegistration();
        }
    }
    else
    {
        // Couldn't get the lock object, but we still need to work, so
        // just do it without an outer lock, and keep your fingers crossed.
        DoIntercepterRegistration();
    }
}

然后DoIntercepterRegistration()会是这样的:

private static void DoIntercepterRegistration()
{
    Console.SetOut(new ConsoleIntercepter(Console.Out));
    Console.SetError(new ConsoleIntercepter(Console.Error));
}

这只是为了对 Out 和 Error 进行锁定保护的原子替换;显然,实际的拦截、处理和传递给前一个TextWriters 需要额外的代码,但这不是这个问题的一部分。

Note that (in the source code provided for .NET2.0) the Console class uses InternalSyncObject to protect initialization of Out and Error and to protect SetOut() and SetError(), but it does not use the lock around reads of Out and Error once they have been previously initialized. This is sufficient for my particular case, but could have atomicitiy violations if you did something more complex (and crazy); I can't think of any useful scenarios with that problem, but deliberately pathological ones can be imagined.

于 2009-02-11T20:28:01.623 回答
0

如果您能尽早做到这一点,Main()您将有更好的机会避免任何竞争条件,特别是如果您可以在创建工作线程之前做到这一点。或者在某个类的静态构造函数中。

如果Main()标有该[STAThread]属性,它将在单线程单元中运行,因此这也应该有所帮助。

于 2008-12-15T03:50:03.843 回答
0

一般来说,我会使用安全模型来防止客户端代码在您不查看时重定向控制台。我确定这需要完全信任,因此如果客户端代码以部分信任运行,它将无法调用 SetOut()。

于 2009-01-13T18:54:14.633 回答