Perl: $ SIG {__ DIE__}, eval {} и трассировка стека - PullRequest
12 голосов
/ 09 июня 2009

У меня есть фрагмент кода на Perl, похожий на следующий (сильно упрощенный): Есть несколько уровней вложенных вызовов подпрограмм (фактически, методов), а некоторые из внутренних выполняют свою собственную обработку исключений:

sub outer { middle() }

sub middle {
    eval { inner() };
    if ( my $x = $@ ) { # caught exception
        if (ref $x eq 'ARRAY') {
            print "we can handle this ...";
        }
        else {
            die $x; # rethrow
        }
    }
}

sub inner { die "OH NOES!" }

Теперь я хочу изменить этот код так, чтобы он делал следующее:

  • выводит полную трассировку стека для каждого исключения, которое «всплывает» вплоть до самого внешнего уровня (sub outer). В частности, трассировка стека должна не останавливаться на первом уровне "eval { }".

  • Нет необходимости изменять реализацию любого из внутренних уровней.

Прямо сейчас я могу установить локализованный обработчик __DIE__ внутри подпрограммы outer:

use Devel::StackTrace;

sub outer {
    local $SIG{__DIE__} = sub {
        my $error = shift;
        my $trace = Devel::StackTrace->new;
        print "Error: $error\n",
              "Stack Trace:\n",
              $trace->as_string;
    };
    middle();
}

[ EDIT : я допустил ошибку, приведенный выше код на самом деле не работает так, как я хочу, он фактически обходит обработку исключений в подпрограмме middle. Так что я думаю, что вопрос должен быть: возможно ли такое поведение, которое я хочу?]

Это прекрасно работает , единственная проблема в том, что, если я правильно понимаю документы, он опирается на явное устаревшее поведение, а именно на тот факт, что обработчики __DIE__ запускаются даже для "die "внутри" eval { }, что они действительно не должны. И perlvar, и perlsub утверждают, что это поведение может быть удалено в будущих версиях Perl.

Есть ли другой способ, которым я могу достичь этого, не полагаясь на устаревшее поведение, или это безопасно, на которое можно положиться, даже если в документах сказано иначе?

Ответы [ 3 ]

10 голосов
/ 09 июня 2009

ОБНОВЛЕНИЕ: Я изменил код для переопределения die во всем мире, чтобы исключения из других пакетов также можно было перехватывать.

Делает ли следующее то, что вы хотите?

#!/usr/bin/perl

use strict;
use warnings;

use Devel::StackTrace;

use ex::override GLOBAL_die => sub {
    local *__ANON__ = "custom_die";
    warn (
        'Error: ', @_, "\n",
        "Stack trace:\n",
        Devel::StackTrace->new(no_refs => 1)->as_string, "\n",
    );
    exit 1;
};

use M; # dummy module to functions dying in other modules

outer();

sub outer {
    middle( @_ );
    M::n(); # M::n dies
}

sub middle {
    eval { inner(@_) };
    if ( my $x = $@ ) { # caught exception
        if (ref $x eq 'ARRAY') {
            print "we can handle this ...";
        }
        else {
            die $x; # rethrow
        }
    }
}

sub inner { die "OH NOES!" }
8 голосов
/ 09 июня 2009

не безопасно полагаться на все, что в документации считается устаревшим. Поведение может (и, вероятно, изменится) в следующем выпуске. Полагаясь на устаревшее поведение, вы блокируете версию Perl, которую вы используете сегодня.

К сожалению, я не вижу пути, который бы соответствовал вашим критериям. «Правильным» решением является изменение внутренних методов для вызова Carp::confess вместо die и удаление пользовательского обработчика $SIG{__DIE__}.

use strict;
use warnings;
use Carp qw'confess';

outer();

sub outer { middle(@_) }

sub middle { eval { inner() }; die $@ if $@ }

sub inner { confess("OH NOES!") }
__END__
OH NOES! at c:\temp\foo.pl line 11
    main::inner() called at c:\temp\foo.pl line 9
    eval {...} called at c:\temp\foo.pl line 9
    main::middle() called at c:\temp\foo.pl line 7
    main::outer() called at c:\temp\foo.pl line 5

Так как вы все равно умираете, вам может не понадобиться перехватывать вызов на inner(). (В вашем примере это не так, ваш реальный код может отличаться.)

В вашем примере вы пытаетесь вернуть данные через $@. Вы не можете сделать это. Используйте

my $x = eval { inner(@_) };

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

4 голосов
/ 09 июня 2009

Обратите внимание, что переопределение die будет перехватывать только реальные вызовы die, , а не Ошибки Perl, такие как разыменование undef.

Я не думаю, что общий случай возможен; весь смысл eval состоит в том, чтобы потреблять ошибки. Вы МОЖЕТЕ быть в состоянии положиться на устаревшее поведение именно по этой причине: в настоящее время нет другого способа сделать это. Но я не могу найти какой-либо разумный способ получить трассировку стека в каждом случае без потенциального нарушения какого-либо кода обработки ошибок, даже если он находится далеко вниз по стеку.

...