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

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

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

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

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

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

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

Ответы [ 4 ]

2 голосов
/ 15 декабря 2008

Если вы посмотрите на реализацию 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 и попытайтесь ухудшить пользовательский опыт, если вы не объект с отражением.

Удачи: -)

1 голос
/ 11 февраля 2009

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

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

0 голосов
/ 13 января 2009

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

0 голосов
/ 15 декабря 2008

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

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

...