STDOUT и STDERR вышли из строя

У меня есть сценарий perl, который записывает сообщения в STDOUT и STDERR (с помощью операторов print / croak), но я также перенаправляю STDOUT и STDERR в файл журнала:

File::Tee::tee STDOUT, ">>", "$logFile" ;
File::Tee::tee STDERR, ">>", "$logFile" ;

Теперь в выходном файле журнала сообщения от STDOUT и STDERR отображаются не по порядку. Кроме того, фактический вывод на терминал также не работает. Я попытался очистить буферы (как рекомендовано здесь: https://perl.plover.com/FAQs/Buffering.html), но это не помогает:

select(STDERR) ;
$| = 1 ;
select(STDOUT) ;
$| = 1 ;

Кто-нибудь знает, что я должен сделать, чтобы просмотреть вывод по порядку (я также пробовал дополнительно очистить дескриптор файла, соответствующий $ logfile, но он все тот же)?


РЕДАКТИРОВАТЬ:

Спасибо всем, кто ответил. Многие дискуссии по этому поводу закончились комментариями, поэтому я собираюсь перечислить несколько вещей, которые я пробовал, основываясь на отзывах всех вас.

  1. Я уже промывал STDOUT и STDERR до того, как использовал File :: Tee. Как подозревал @jimtut, виновником действительно был File :: Tee - его удаление восстановило порядок на консоли. Но я действительно хотел перенаправить STDOUT и STDERR.
  2. @mob предложил вместо этого использовать IO :: Tee, но я не совсем понял, как заставить это работать так, как я хочу в моем коде.
  3. @briandfoy указал, что не существует надежного способа гарантировать, что 2 отдельных дескриптора файлов будут отображаться в правильном порядке в реальном времени, а также предложил использовать процедуру регистрации, которая является единственным местом, которое может писать в STDOUT / STDERR. @zimd далее указал, что File :: Tee использует fork, что является сутью проблемы, поскольку 2 процесса не могут гарантировать какой-либо порядок вывода.
  4. Поскольку виноват File :: Tee, я попытался удалить это из кода. Я обновил функцию регистратора для печати в STDOUT / STDERR, а также для дополнительной печати в дескриптор файла $ log. Далее, чтобы записать предупреждения в журнал, я сделал следующее:
sub warning_handler {
  my $msg = $_[0] ;
  print STDERR $msg ;
  print $log $msg if defined $log ;
}
$SIG{__WARN__} = \&warning_handler ;

Это отлично сработало для всего кода под моим контролем. Теперь все печаталось в порядке и на консоли, и в файле журнала. Однако я понял, что не могу использовать это решение, поскольку я также звонил в чьи-то пакеты perl для некоторых функций, и, очевидно, я не мог перехватить печать / кваканье и т. Д., Которые писали в STDOUT / STDERR в пакете «с полки». Так что прямо сейчас у меня нет хорошего решения. Однако я подозреваю, что если я найду способ перехвата STDOUT / STDERR в perl, я смогу получить то, что мне нужно.


EDIT2: я добавил свой собственный ответ, который, вероятно, является наиболее близким к решению проблемы, изменяя решение моба с использованием IO :: Tee вместо File :: Tee, но даже это пропускает некоторые сообщения (хотя он исправляет порядок).


РЕДАКТИРОВАТЬ3: Наконец-то нашел `` решение ''

use IO::Tee ;
use Capture::Tiny qw(capture);
...
...
select(STDERR) ;
$| = 1 ;
select(STDOUT) ;
$| = 1 ;
open (my $log, ">", $logfilename) ;
*REALSTDOUT = *STDOUT ;
*REALSTDERR = *STDERR ;
*STDOUT = IO::Tee->new(\*REALSTDOUT, $log);
*STDERR = IO::Tee->new(\*REALSTDERR, $log);

# Regular Perl code here which sends output to STDOUT/STDERR
...
...
# system calls / calls to .so needs to be catpured 
&log_streams(sub { &some_func_which_calls_shared_object() ; }) ;

sub log_streams {
    my ($cr, @args) = @_;  # code reference, with its arguments
    my ($out, $err, $exit) = capture { $cr->(@args) };

    if ($out) {
        print STDOUT $out;
    }
    if ($err) {
        print STDERR $err;
    }
}

Использование IO :: Tee гарантирует, что все сгенерированные perl выходные данные на консоль также попадут в файл журнала, и это происходит немедленно, тем самым обновляя журнал и консоль в реальном времени. Поскольку IO :: Tee меняет значение дескрипторов файлов STDOUT / STDERR, чтобы теперь ссылаться на дескрипторы teed, он может перехватывать stdio только из операторов perl, он пропускает системные вызовы, поскольку они обходят дескрипторы STDOUT / STDERR perl. Таким образом, мы фиксируем вывод системного вызова, а затем используем процедуру log_streams, чтобы перенаправить его потокам STDOUT / STDERR, которые теперь имеют псевдоним. Это создает задержку в выводе сгенерированного системного вызова вывода, отображаемого в журнале / терминале, но нет задержки для вывода, сгенерированного perl, то есть лучшего из обоих миров. Обратите внимание, что порядок stderr и stdout, сгенерированный вызовом подпрограммы some_func_which_calls_shared_object, не сохраняется, поскольку в подпрограмме log_streams мы сначала печатаем в STDOUT, а затем в STDERR - до тех пор, пока системный вызов является атомарным и мало что делает в с точки зрения чередования сообщений stdout / stderr мы должны быть в порядке. Оцените решения от briandfoy, mob и zimd, ответы которых я объединил, чтобы прийти к этому решению! Никогда не думал, что для решения этой, казалось бы, очень простой проблемы потребуется детальное рассмотрение.


person backstreetrover    schedule 14.02.2020    source источник
comment
Если вы УДАЛЯЕТЕ использование tee и просто печатаете на консоли, это в порядке? Я предполагаю, что это так, и виноват tee.   -  person jimtut    schedule 14.02.2020
comment
Действительно File :: Tee :: tee - это проблема. Его удаление исправляет порядок вывода на терминале. Но я действительно хотел перенаправить это в файл журнала.   -  person backstreetrover    schedule 14.02.2020
comment
Если вы используете сценарий Perl командной строки (а не сценарий CGI), создайте сценарий оболочки (Shell или Batch, в зависимости от вашей платформы. Внутри этой оболочки вызовите сценарий Perl, а затем перенаправьте STDOUT и STDERR. Это похоже на это для Linux, и я считаю, что он почти идентичен для Windows: myscript.pl >> logfile 2>>&1   -  person jimtut    schedule 14.02.2020
comment
да, я знаю, что могу перенаправить за пределы скрипта ... но это не оболочка bash / csh, она предназначена для прямой утилиты perl.   -  person backstreetrover    schedule 14.02.2020
comment
Вы делали select(STDERR) ; $| = 1 ; select(STDOUT) ; $| = 1 ; перед звонком tee? Быстрый взгляд на код показывает, что это должно сработать.   -  person ikegami    schedule 15.02.2020
comment
@ikegami да, я сделал. Это не работает.   -  person backstreetrover    schedule 16.02.2020
comment
Обновите вопрос, чтобы показать все, что вы пробовали. Скрытие этого в комментариях не поможет людям вам помочь.   -  person brian d foy    schedule 16.02.2020


Ответы (4)


С двумя отдельными дескрипторами файлов нет контракта или гарантии, что вы увидите их в реальном времени. На это влияют различные настройки и буферы, поэтому вы видите автоматическую очистку ($|). То же самое и с файлами, и с терминалом.

Поймите, что это архитектурная проблема, а не синтаксическая. За один и тот же ресурс конкурируют две вещи. Обычно это заканчивается слезами. Я не решаюсь предлагать решение, когда я не знаю, в чем проблема, но подумайте о том, чтобы все, что пытается написать, STDOUT или STDERR записывается в какой-то брокер сообщений, который собирает все сообщения и является единственным, что записывает в конечный (общий) пункт назначения. Например, вещи, которые хотят добавить записи в системный журнал, не записываются в системный журнал; они отправляют сообщения тому, что пишет в системный журнал.

Еще один пример Perly: в Log4perl вы не пишете в конечные пункты назначения. Вы просто регистрируете сообщение, и регистратор - единственное, что решает, как его обработать. Когда мне нужно такое поведение модуля, я не использую напрямую средства вывода:

debug( "Some debug message" );

sub debug {
    my $message = shift;
    output( "DEBUG: $message" );
    }

sub output { # single thing that can output message
    ...
    }

Затем делайте все, что вам нужно, в output.

Но вы не всегда можете контролировать это в других вещах, которые также пытаются что-то выводить. Perl позволяет вам обойти это, переопределив то, что делают warn и его друзья, поместив ссылку на код в $SIG{__WARN__}. Вы можете записывать предупреждающие сообщения и делать с ними все, что захотите (например, отправлять их на стандартный вывод). Помимо этого есть черная магия, которая вновь открывает STDERR то, что вы можете контролировать. Это не так уж и плохо, и он изолирован в одном месте.

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

person brian d foy    schedule 14.02.2020
comment
Комментарии не подлежат расширенному обсуждению; этот разговор был перешел в чат. - person Bhargav Rao; 16.02.2020
comment
@briandfoy Ой, плохо .. извините за недоразумение. Неудивительно, что мои сообщения не в порядке, если его разветвляется! Я собираюсь избавиться от File :: Tee и просто обработать запись в файл в подпрограмме регистратора. И также будет использовать обработчик $ SIG {WARN} для перенаправления предупреждений в подпрограмму регистратора. Это должно решить все проблемы. - person backstreetrover; 16.02.2020

У вас будет два дескриптора файлов, пишущих в $logfile. Если File::Tee не позаботится о поиске конца дескриптора файла перед каждой записью (чего, по-видимому, не происходит), вы получите состояние гонки, при котором один дескриптор файла перезапишет другой.

Обходной путь - использовать параметр reopen для функции File::Tee::tee, которая будет закрывать файл после каждой записи и повторно открывать его (в правильном конце файла) перед следующей записью. Однако это может повредить вашей производительности, в зависимости от того, как часто вы пишете в эти дескрипторы файлов.


Возможно, вам больше повезет с IO::Tee, который является более простой реализацией (с использованием связанных файловых дескрипторов), чем то, что использует File::Tee (фоновый процесс для каждого File::Tee::tee вызова), поэтому вы можете получить меньше сюрпризов. Вот как может выглядеть IO::Tee решение:

use IO::Tee;
$| = 1;
open my $stdout, ">&=1";  # to write to original stdout
open my $stderr, ">&=2";  # to write to original stderr
open my $fh_log, ">>", $logfile;
*STDOUT = IO::Tee->new($stdout, $fh_log);
*STDERR = IO::Tee->new($stderr, $fh_log);
...

Нет никаких фоновых процессов, дополнительных потоков или чего-либо еще, что могло бы вызвать состояние гонки. И STDOUT, и STDERR будут писать в один и тот же дескриптор файла журнала из одного и того же процесса.

person mob    schedule 14.02.2020
comment
Но это не объясняет, почему выход на терминал тоже не в порядке. Конечно, это не имеет ничего общего с дескриптором файла LOGFILE. - person backstreetrover; 14.02.2020
comment
Я пробовал повторно открыть с помощью File :: Tee. Это не решило проблему. - person backstreetrover; 14.02.2020
comment
Тогда тебе может повезти с IO::Tee. Это более простая реализация мультиплексирования, которая, как я подозреваю, принесет меньше сюрпризов. - person mob; 15.02.2020
comment
Не знаю, как использовать IO :: Tee. Насколько я понимаю, IO :: Tee дает вам мультиплексирование от одного до многих, то есть вы можете писать в один дескриптор $ tee, и в конечном итоге он будет записывать несколько дескрипторов. Я как бы хочу противоположного - я хочу, чтобы STDOUT и STDERR для меня были объединены в один FILEHANDLE (при сохранении вывода консоли) - person backstreetrover; 15.02.2020
comment
Сначала откройте дескриптор файла для своего файла журнала, а затем установите оба объекта IO::Tee для записи в этот дескриптор файла. - person mob; 15.02.2020
comment
если я буду следовать тому, что вы говорите, у меня будет 2 дескриптора файлов teed, которые мне нужно будет использовать вместо STDOUT и STDERR в остальной части моего кода. Далее мне нужно будет перехватить предупреждения и перенаправить на файловый дескриптор teed. Хотя я могу указать ручку teed в распечатках, это не (?) Было бы возможно для кваканья и друзей (кудахтать / признаться) - person backstreetrover; 16.02.2020
comment
Я добавлю IO::Tee решение выше. - person mob; 16.02.2020
comment
спасибо за решение. Это выглядит идеально, и я определенно ожидал, что это сработает. Как ни странно это не работает. В этом решении сообщения, записываемые в файл журнала, находятся в правильном порядке, однако консоль упорядочивает STDOUT и STDERR неверно. Более того, сообщения из чужого пакета, который я вызываю (который, в свою очередь, переносится с C ++ на perl), полностью отсутствуют в журнале, хотя и отображаются в консоли. Понятия не имею, почему такое поведение. - person backstreetrover; 16.02.2020
comment
использование псевдонимов typeglob вместо упорядочивания исправлений dup на консоли и в файле журнала - см. мой ответ на исходный вопрос - person backstreetrover; 16.02.2020

Примечание Первая часть выполняется с помощью привязных ручек; решение во второй части использует Capture::Tiny


Простое подтверждение концепции подхода с использованием маркеров tie -d.

Пакет, который связывает дескриптор, печатая из него в файл и в (копию) STDOUT потока

package DupePrints; 

use warnings; 
use strict; 
use feature 'say'; 

my $log = 't_tee_log.out'; 
open my $fh_out, '>', $log or die $!;  # for logging

# An independent copy of STDOUT (via dup2), for prints to terminal
open my $stdout, '>&', STDOUT or die $!;

sub TIEHANDLE { bless {} } 

sub PRINT { 
    my $self = shift; 

    print $fh_out @_; 
    print $stdout @_;
}

1;

Программа, которая его использует

use warnings;
use strict;
use feature 'say';

use DupePrints;    
$| = 1;
tie *STDERR, 'DupePrints';
tie *STDOUT, 'DupePrints';

say "hi";

warn "\t==> ohno";

my $y;
my $x = $y + 7;

say "done";

Это печатает как на терминал, так и на t_tee_log.out текст

hi
        ==> ohno at main_DupePrints.pl line 14.
Use of uninitialized value $y in addition (+) at main_DupePrints.pl line 17.
done

См. perltie и Tie :: Handle и этот пост со связанными примерами и, возможно, этим сообщением

Запись в файл потоков STDOUT и STDERR (вместе с копией распечатки) работает и в других модулях, которые также могут быть used в основной программе.

Чтобы также иметь «чистые» отпечатки, которые не регистрируются, скопируйте дескриптор STDOUT в основной программе, как это сделано в модуле, и распечатайте его. Если вам нужно использовать это более избирательно и изощренно, пожалуйста, измените его по мере необходимости - в его нынешнем виде он предназначен только для базовой демонстрации.


С уточнением в редакции вопроса, вот другой подход: оберните вызов в Capture :: Tiny, который захватывает весь вывод любого кода, а затем управляет захваченными отпечатками по мере необходимости.

use warnings;
use strict;
use feature qw(say state);

use Capture::Tiny qw(capture);

sub log_streams {
    my ($cr, @args) = @_;  # code reference, with its arguments

    # Initialize "state" variable, so it runs once and stays open over calls
    state $fh_log = do {
        open my $fh, '>', 'tee_log.txt' or die $!;
        $fh;
    };

    my ($out, $err, $exit) = capture { $cr->(@args) };

    if ($out) {
        print $fh_log $out;
        print         $out;
    }
    if ($err) {
        print $fh_log $err;
        print         $err;
    }
}

log_streams( sub { say "hi" } );
log_streams( sub { warn "==> ohno" } );
log_streams( sub { my $y; my $x = $y + 7; } );

log_streams( sub { system('perl', '-wE', q(say "external perl one-liner")) } );

log_streams( sub { say "done" } );

Обратной стороной всего этого является то, что все должно работать через этот суб. Но опять же, это действительно хорошо, даже если иногда и неудобно.

функция состояния используется для «инициализации» дескриптора файла, поскольку переменная, объявленная как state, никогда не инициализировался повторно; поэтому файл открывается только один раз при первом вызове и остается открытым.

Это также демоверсия, которую нужно доработать.

person zdim    schedule 15.02.2020
comment
Читателю: я внимательно просмотрел это и не получил -1. Если кто-нибудь это сделает, я буду признателен за комментарии - person zdim; 16.02.2020
comment
@zimd большое спасибо за ответ. Однако я не уверен, что понимаю, как все это работает. В любом случае, я буду читать больше о связанных файловых дескрипторах, так как подозреваю, что это приведет меня к решению. - person backstreetrover; 16.02.2020
comment
@backstreetrover Ой, извините, добавлю комментарий. Это действительно один из способов получить желаемое. (Хотя опубликованная демонстрация работает как есть, ее следует заполнить согласно документации. Это не должно быть сложно.) - person zdim; 16.02.2020
comment
@backstreetrover Добавлен раздел с другим подходом, который делает все, что прописано в вашем редактировании - person zdim; 16.02.2020
comment
да, спасибо за пример Capture :: Tiny - на самом деле, возможно, я смогу, так как мне просто нужно заключить весь мой скрипт в log_streams. Но похоже, что он немного задерживает печать, поскольку он захватывает все во время выполнения, а затем выполняет печать в конце. Положительным моментом является то, что я получаю правильный порядок, недостатком является то, что вывод задерживается, что плохо, поскольку мой сценарий может занять некоторое время. - person backstreetrover; 16.02.2020
comment
@backstreetrover недостатком является то, что вывод задерживается --- правильно, этот модуль перенаправляет пакетно. Чтобы получать распечатки по мере их поступления, используйте IPC::Run (в любом случае полезно использовать всегда :)), например, как в этом сообщении. Затем вы можете использовать это в одной подпрограмме для внешних команд (с потенциально неизвестной динамикой и распечатками) и использовать подпрограмму из этого сообщения для своих собственных распечаток. Можно было бы сделать это для красивого маленького модуля (с другими связанными подпрограммами). Но я вижу, что вы уже собрали решение, и это здорово :) - person zdim; 17.02.2020

Получив подсказку из ответа @mob на использование IO :: Tie вместо File :: Tee (поскольку последний использует вилку, вызывающую нарушение порядка STDERR vs STDOUT), я немного изменил исходное решение моба, и оно сработало (почти - читайте дальше) :

use IO::Tee
...
...
open (my $log, ">", $logfilename) ;
*MYSTDOUT = *STDOUT ;
*MYSTDERR = *STDERR ;
*STDOUT = IO::Tee->new(\*MYSTDOUT, $log);
*STDERR = IO::Tee->new(\*MYSTDERR, $log);

Это привело к правильному порядку как на консоли, так и в файле журнала (исходное решение mob с использованием open to dup STDOUT / STDERR не сработало - это привело к правильному порядку в файле журнала, но не в порядке на консоли. Вместо этого использовался псевдоним typeglob of dup работает по какой-то странной причине).

Однако, как бы хорошо это ни звучало, оно пропустило печать некоторых сообщений из пакета, который я вызываю в файле журнала (хотя они печатаются на консоли). Мой исходный код, в котором был File :: Tee, действительно приводил к тому, что эти же сообщения из пакета отображались в файле журнала, так что где-то происходит какое-то вуду. Конкретный рассматриваемый пакет представляет собой файл .so, поэтому я не знаю, как именно он печатает свои сообщения.

РЕДАКТИРОВАТЬ: Я предполагаю, что файл .so так же хорош, как печать внешней системной команды на stdout / stderr. Поскольку он не проходит через ввод-вывод perl, дескрипторы, на которые указывают группы типов STDOUT / STDERR внутри perl, не будут отражать вывод внешних программ, вызываемых из perl. Я думаю, что лучшим решением было бы использовать комбинацию этого решения для сообщений, поступающих из кода perl, и использование Capture :: Tiny :: capture, как указано @zdim, для захвата и перенаправления сообщений из системных вызовов / вызовов, проходящих через интерфейс swig.

person backstreetrover    schedule 16.02.2020