Как я могу обеспечить атомарность операции get-and-set для перенаправления Console.Out для регистрации выходных данных консоли?

Мне нужно перехватить поток (ы) консольного вывода, чтобы захватить его для журнала, но все же передать вещи в исходный поток, чтобы приложение работало правильно. Это, очевидно, означает хранение оригинала Console.Out TextWriter перед его заменой Console.SetOut(new MyTextWriterClass(originalOut)),

Я предполагаю, что отдельные операции для получения свойства Out и вызова метода SetOut() осуществляются Console потокобезопасным способом. Но я хотел бы убедиться, что какой-то другой поток (например, запуск кода клиентского приложения, который я не контролирую и не могу ожидать изменить, и поэтому я не могу полагаться на свою собственную схему блокировки), не может случайно изменил его между моими get и set и в итоге перезаписал мои изменения (нарушив поведение их приложения!). Поскольку другой код может просто вызывать SetOut(), мой код в идеале должен получить такую ​​же блокировку, которая используется внутри Console (при условии, что есть один).

К несчастью, Console это (статический) класс, а не экземпляр, так что вы не можете просто lock (Console), И, глядя на документацию класса, похоже, нет никаких упоминаний о блокировке. Это не обычно ожидаемое использование этих методов консоли, но должен быть какой-то безопасный способ сделать это как элементарную операцию.

В случае отсутствия стандартной схемы блокировки, есть ли другой способ обеспечить это? Для такого короткого критического раздела (и выполняется только один раз) даже кратковременная блокировка всех других потоков может быть приемлемой, если это единственный способ сделать это. Мы используем C# и.NET2.0.

Если даже это невозможно (без нарушения работы клиентского приложения), то нам просто нужно полагаться на то, что маловероятно, что клиентское приложение перенаправит вывод своей консоли и произойдет между операциями get и set. Я просто хотел бы охватить все базы, на всякий случай.

Изменить: Теперь, когда у нас есть конкретный ответ с примером кода, я перефразировал заголовок вопроса, чтобы более широко отражать случаи использования, где ответы могут помочь, чтобы быть более ясным. Также добавлен тег для "атомного".

4 ответа

Решение

Если вы посмотрите на реализацию 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, а затем заблокировать его.

Предостережение: это крайне плохая идея, и ее следует использовать только тогда, когда не существует другого варианта. Вы можете заставить фреймворк вести себя непредсказуемо и завершить процесс.

Вам также необходимо обратить внимание на любые пакеты обновления и основные выпуски, чтобы убедиться, что внутренняя переменная все еще используется. Поскольку его внутренняя часть не обещает, что он будет там в следующем выпуске. Напишите свой код defensivley и попробуйте и ухудшите пользовательский опыт, если вы не объект с отражением.

Удачи:-)

Хорошо, теперь у меня было некоторое время, чтобы на самом деле выработать подход к отражению, предложенный Джошем. Основной код (по моему 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 and Error; очевидно, фактический перехват, обработка и переход к предыдущему TextWriters требует дополнительного кода, но это не является частью этого вопроса.

Обратите внимание, что (в исходном коде, предоставленном для.NET2.0) класс Console использует InternalSyncObject для защиты инициализации Out и Error и для защиты SetOut() и SetError(), но он не использует блокировку чтений Out и Error как только они были предварительно инициализированы. Этого достаточно для моего конкретного случая, но могут возникнуть атомные нарушения, если вы сделали что-то более сложное (и безумное); Я не могу придумать каких-либо полезных сценариев с этой проблемой, но преднамеренно патологические можно представить.

Если вы можете сделать это рано Main() у вас будет гораздо больше шансов избежать любых условий гонки, особенно если вы сможете сделать это до того, как что-то создаст рабочий поток. Или в статическом конструкторе некоторого класса.

Если Main() отмечен [STAThread] атрибут будет работать в однопоточной квартире, так что это тоже должно помочь.

В общем, я бы использовал модель безопасности, чтобы предотвратить перенаправление клиентского кода на консоль, пока вы не смотрите. Я уверен, что для этого требуется полное доверие, поэтому, если клиентский код выполняется с частичным доверием, он не сможет вызвать SetOut().

Другие вопросы по тегам