Perl Devel::StackTrace reverse order of frames - perl

I am trying to use the Perl module Devel::StackTrace to display errors with stack trace, the frames are showed bottom to top of the source of the error, is there a way to reverse this order to display the error source frame at the top because this is the first thing you need to know then follow tracing if needed to the bottom.
Below is the test code to reproduce the issue.
#!/usr/bin/perl
#!C:\perl\bin\perl.exe
BEGIN {
$|=1;
use CGI::Carp qw(fatalsToBrowser set_message);
use Devel::StackTrace;
use Devel::StackTrace::AsHTML;
use PadWalker;
use Devel::StackTrace::WithLexicals;
sub handle_errors {
my $msg = shift;
#my $trace = Devel::StackTrace->new(indent => 1, message => $msg);
my $trace = Devel::StackTrace::WithLexicals->new(indent => 1, message => $msg);
#print $trace->as_html;
print $trace->as_string;
}
set_message(\&handle_errors);
}
show_error();
exit;
Below is the text format error message:
Undefined subroutine &main::show_error called at C:/apache/htdocs/tests/test.cgi line 24.
at C:\apache\htdocs\tests\test.cgi line 17
main::handle_errors('Undefined subroutine &main::show_error called at C:/apache/htdocs/tests/test.cgi line 24.^J') called at C:\perl\site\lib\CGI\Carp.pm line 525
eval {...} at C:\perl\site\lib\CGI\Carp.pm line 524
CGI::Carp::fatalsToBrowser('Undefined subroutine &main::show_error called at C:/apache/htdocs/tests/test.cgi line 24.^J') called at C:\perl\site\lib\CGI\Carp.pm line 444
CGI::Carp::die('Undefined subroutine &main::show_error called at C:/apache/htdocs/tests/test.cgi line 24.^J') called at C:\apache\htdocs\tests\test.cgi line 24

After looking at the module Devel::StackTrace source code, I was able to answer my question.
The module has the method frames where it returns or sets the frames. So I reversed the order simply by this line before calling the as_string or as_html methods:
$trace->frames(reverse $trace->frames);
Here is the modified code with the solution:
#!/usr/bin/perl
#!C:\perl\bin\perl.exe
BEGIN {
$|=1;
use CGI::Carp qw(fatalsToBrowser set_message);
use Devel::StackTrace;
use Devel::StackTrace::AsHTML;
use PadWalker;
use Devel::StackTrace::WithLexicals;
sub handle_errors {
my $msg = shift;
my $trace = Devel::StackTrace->new(indent => 1, message => "$msg\n");
#my $trace = Devel::StackTrace::WithLexicals->new(indent => 1, message => $msg);
$trace->frames(reverse $trace->frames);
#print $trace->as_html;
print $trace->as_string;
}
set_message(\&handle_errors);
}
show_error();
exit;
It does exactly what I want.

Related

Perl failing to create cgi session

I have the code as shown below. A BEGIN which loads the session or, if none is yet created, it creates one. But it doesn't do it all the time. It's a login script; If I enter the PIN and it's wrong, the script displays the login form again, which is submitted to this same script. Up to 3 attempts permitted but, it will fail to load the session, usually on attempt 2. Inconsistent so, please can anyone see what might be wrong and why is the session sometimes not loading.
I do have warnings enabled and I have shown that in the code.
I used to start the script with 'print $session->header' but, having changed to 'print $cgi->header;' I can see clearly that the session is undefined, when the script fails. I should add that, if I refresh the failed page perhaps as many as 5 times, the session does eventually reload with all data intact.
#!/usr/bin/perl
#use CGI::Carp qw/warningsToBrowser fatalsToBrowser/;
use strict;
use warnings 'all';
use CGI qw(:all);
use CGI::Session;
use Crypt::PasswdMD5;
use DBI;
use Data::Dumper;
my $cgi = CGI->new;
my $session;
my $sessions_dir_location;
my $session_id;
BEGIN{
unshift #INC, "/var/www/vhosts/example.com/subDomain.example.com/cgi-bin/library";
my $document_root = $ENV{'DOCUMENT_ROOT'};
$document_root =~ s/\///;
my ( $var
, $www
, $vhosts
, $domain
) = split ('/', $document_root, 5);
$sessions_dir_location = '/' . $var . '/' . $www . '/' . $vhosts . '/' . $domain;
$session = CGI::Session->load() or die CGI::Session->errstr();
if ( $session->is_expired ) {
print $session->header(),
$cgi->start_html(),
$cgi->p("Your session timed out! Refresh the screen to start new session!"),
$cgi->end_html();
exit(0);
}
if ( $session->is_empty ) {
$session = new CGI::Session(undef, undef,
{Directory=>"$sessions_dir_location/sessions_storage/"}) or die CGI::Session->errstr;
}
#add the library dir to #INC;
use lib do {
use Cwd 'realpath';
my ($dir) = __FILE__ =~ m{^(.*)/};
realpath("$dir/library");
};
use feature 'say';
use FindBin '$RealBin';
use lib $RealBin;
use lib "$RealBin/library";
}
my $self = $cgi->url;
my %login = $cgi->Vars;
print $cgi->header;
# capture and display warnings
local $SIG{__WARN__} = sub {
my $message = shift;
print $cgi->header;
print qq($message);
};
print qq(<pre>);
print Dumper \%login;
print qq(</pre>);
print qq(<pre>session);
print Dumper \$session; #undef
print qq(</pre>);
#next is line 141
my $session_stored_user_name = $session->param("entered_user_name");
Error message is this:
Can't call method "param" on an undefined value at /var/www/vhosts/example.com/subDomain.example.com/cgi-bin/dashboard-login/login-with-pin.pl line 141, <DAT> line 45.
Please, also, what or where is <DAT> line 45?

Call from a code reference in Template Toolkit

I have a simple higher-order function that builds a message formatter.
use strict;
use warnings;
sub make_formatter {
my $level = shift;
return sub {
my $message = shift;
return "[$level] $message";
}
}
I use it from Perl like that:
my $component_formatter = make_formatter('ComponentError');
print $component_formatter->('Hello') . "\n";
I want to use make_formatter from a Template Toolkit template. I have tried to do the following:
use Template;
use Template::Constants;
my $template = Template->new({
# DEBUG => Template::Constants::DEBUG_ALL,
VARIABLES => {
make_formatter => make_formatter,
}
});
my $template_str = "
[% my_formatter = make_formatter('MyFormatter') %]
<h1>[% my_formatter('Sample message') %]</h1>
";
$template->process(\$template_str);
The output of this script is:
$ perl test.pl
Use of uninitialized value $level in concatenation (.) or string at test.pl line 10.
<h1>[] MyFormatter</h1>
Is it possible to call my_formatter using only Template Toolkit syntax ? Calling external Perl code that is not callable by default from Template Toolkit is not an option.
First let me please point out that putting use strict; use warnings; at the beginning of your script is strongly advised.
If you do that for your snippet generating the $template,
you will get a Bareword "make_formatter" not allowed while "strict subs" in use error, which should help you determine this is not a useful notation.
Now if you call make_formatter() instead, this will output <h1>[] MyFormatter</h1>. This makes sense: your function returned the sub, which is called with 'MyFormatter' in your template ( and $level is undef, as you called make_formatter with no input ).
As Mr. Haegland pointed out,
my $template = Template->new({
VARIABLES => {
make_formatter => \&make_formatter,
}
});
leads to the output I understand you want:
<h1>[MyFormatter] Sample message</h1>
\&make_formatter gives you a subroutine reference,
which in perl normally you could call using:
my $ref = \&make_formatter; $ref->( 'Input' );
This can then be called in the first line of your template,
returning another code ref, which is then called in your second line.
Hope this helps!

log4perl run from within another package not firing messages sent through email

I'm seeing behavior that I can't explain when using log4perl to send a message by email.
So the following test script works just fine and an email is sent without problems:
#!/usr/bin/perl
use strict;
use warnings;
use Log::Log4perl qw(:easy);
use Log::Dispatch;
my $appender_email = Log::Log4perl::Appender->new(
"Log::Dispatch::Email::SSMTP",
threshold => "INFO",
to => 'myemail#mail.com',
subject => 'Perl script message'
);
my $email_logger = get_logger();
$email_logger->level($INFO);
$email_logger->add_appender($appender_email);
$email_logger->info('hi');
The Log::Dispatch::Email::SSMTP is a module I wrote to send emails using the ssmtp command.
The weirdness begins when this same exact code is moved to another package in another file in the same directory as my original script. When I do that and use that package from within my original script, no email gets sent and there are no errors thrown.
However, if I change:
Log::Dispatch::Email::SSMTP
to
Log::Log4perl::Appender::Screen
It prints out "hi" to the screen just fine when I run my script.
So if log4perl works when sending the message to the screen, why doesn't it work when trying to send an email? And why does the same code fire an email from within the original script and not from a package? Again, there are no errors getting thrown or any kind of indication that something went wrong. And I have verified that my module gets loaded with print statements. So my module's code is definitely getting loaded but the email is still not firing.
UPDATE
Here is the code when it's not working per request in comments.
maillog.pl
#!/usr/bin/perl
BEGIN { unshift #INC, "/home/steve/perl/perl-lib" }
use strict;
use warnings;
use Testy;
print 'start' . "\n";
Here is the Testy.pm package:
package Testy;
BEGIN { unshift #INC, "/home/steve/perl/perl-lib" }
use strict;
use warnings;
use Log::Log4perl qw(:easy);
use Log::Dispatch;
print 'end' . "\n";
my $appender_email = Log::Log4perl::Appender->new(
"Log::Dispatch::Email::SSMTP",
#"Log::Log4perl::Appender::Screen",
threshold => "INFO",
to => 'myemail#mail.com',
subject => 'Perl script message'
);
my $email_logger = get_logger();
$email_logger->level($INFO);
$email_logger->add_appender($appender_email);
$email_logger->info('hi');
1;
And here is my SSMTP module located in /home/steve/perl/perl-lib/Log/Dispatch/Email/SSMTP:
package Log::Dispatch::Email::SSMTP;
use strict;
use warnings;
use Log::Dispatch::Email;
use Data::Dumper;
use base qw( Log::Dispatch::Email );
print "hi, i'm here!\n";
sub send_email {
my $self = shift;
my %p = #_;
my $to = escape ( join ',', #{$self->{to}} );
my $subject = $self->{subject};
my $message = $p{message};
$message =~ s/'/'\\''/g;
print $to . "\n";
print $subject . "\n";
print $message . "\n";
print "I'm working!";
system("echo 'To: $to\nFrom: \'Me\' <myemail\#gmail.com>\nSubject:$subject\n\n$message' | /usr/sbin/ssmtp $to");
}
sub escape {
my $address = shift;
$address =~ s/#/\\#/g;
return $address;
}
1;
When I run ./maillog.pl no email is sent when using the code in the Testy package (the same code works when in maillog.pl file. However, if I uncomment Log::Dispatch::Email::SSMTP and replace with Log::Log4perl::Appender::Screen it works.
UPDATE #2
If I change Log::Log4perl::Appender::Screen to Log::Dispatch::Screen it works as well. So maybe come kind of bug in Log::Dispatch::Email?
Found the problem with some help from the FAQ at click here
Apparently, there is some buffering going on so emails do not get sent out immediately until some threshold for the number of messages generated is reached. Though it's still a mystery to me as to why emails are sent immediately when the code is in the main package.
So here is the code that works with the buffered property set to 0:
my $appender_email = Log::Log4perl::Appender->new(
"Log::Dispatch::Email::SSMTP",
threshold => "INFO",
to => 'me#mymail.com',
buffered => 0,
subject => 'Perl script message'
);

Error Handling Using Perl Tkx

I am working on a Perl Tkx application and I am having trouble getting useful/correct error messages. I am attempting to use croak so it will tell me exactly where it fails, but every time it croaks it just says the error is located "at C:/Perl64/lib/Tkx.pm line 347."
I have written a very simple script to demonstrate what is happening:
#!/usr/bin/perl
use strict;
use warnings;
use Carp;
use Tkx;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => sub { croak; },
);
$b->g_pack;
Tkx::MainLoop();
When I run this script and click the button I get the following popup box:
How can I make Tkx handle errors similar to using croak in a regular Perl script? is there a way to force croak to print to STDOUT or STDERR?
EDIT:
Michael Carman brings up a good point that generally with a GUI the console will be suppressed and the user will never see an error sent to STDOUT or STDERR, but what about finding a way to modify the text in the "Application Error" dialogue box to say something useful? Is that possible?
The text doesn't necessarily have to be useful to the end user, but should at least be understandable by the programmer so they know where to look for the issue when it is reported by the end user.
EDIT2:
Okay it appears that using die or confess instead of croak prints a more useful error message to the "Application Error" window, but once I click "OK" the program just continues and doesn't actually die. How can I "kill it until it's dead" and make sure it stays dead?
POSSIBLE SOLUTION:
Here is a possible solution built off of Michael Carman's response. I modified it slightly because I think a messageBox looks a little bit nicer :)
#!/usr/bin/perl
use strict;
use warnings;
use Tkx;
use Carp;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => sub { die "uh-oh"; },
);
$b->g_pack;
Tkx::eval(<<'EOT');
proc bgerror {message} {
tk_messageBox -title "Application Error" -message $message -icon error -type ok
destroy .
}
EOT
Tkx::MainLoop();
So far this is the best solution but 5 days still remain in the bounty so keep those answers coming!
croak reports errors from the perspective of the caller. Use die to report errors from the perspective of your code.
croak is typically used when writing modules so that you can report problems in the way your code is used. (e.g. for argument validation) GUI programming is sort of a mirror image of this. Instead of writing a library for someone else to use, you're injecting your code into a library that was written by someone else. When you pass a code reference to -command the caller becomes Tkx, so it's better to report any errors from the perspective of your code.
Tkx catches fatal errors in callbacks and reports them via the "Application Error" dialog you're seeing. It's not uncommon for GUI applications to be disconnected from the console, meaning that STDOUT and STDERR are closed and any messages written to them are lost. Without the dialog your application would simply vanish and the user would have no clue as to why.
e.g.
use Tkx;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => sub { die "uh-oh" },
);
$b->g_pack;
Tkx::MainLoop();
When I run this (and press the button) the dialog message is
uh-oh at c:\temp\foo.pl line 9.
If you need full control over how Tk handles errors you can override the default handler. The catch is that you have to do it in Tcl. Here's a minimal version to create a dialog with the error message and exit the application when it's closed.
Tkx::eval(<<'EOT');
proc bgerror {message} {
tk_dialog .error "Error" $message [] 0 Close
destroy .
}
EOT
The following scripts prints the error message to STDOUT:
use strict;
use warnings;
use Carp;
use Tkx;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => \&error_test,
);
$b->g_pack;
Tkx::MainLoop();
sub error_test {
eval {
confess;
};
print "$#\n";
}
Output:
at ./p.pl line 20.
eval {...} called at ./p.pl line 19
main::error_test() called at /home/hakon/perl5/lib/perl5/Tkx.pm line 347
eval {...} called at /home/hakon/perl5/lib/perl5/Tkx.pm line 347
Tkx::i::DoOneEvent(0) called at /home/hakon/perl5/lib/perl5/Tkx.pm line 56
Tkx::MainLoop() called at ./p.pl line 16
Edit
The following could be used to print the error message to a text field of the Tk GUI:
use strict;
use warnings;
use Carp;
use Tkx;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => \&error_test,
);
$b->g_grid(-column => 0, -row => 0);
my $text = $mw->new_tk__text(-width => 100, -height => 30);
$text->g_grid(-column => 0, -row => 1);
Tkx::MainLoop();
sub error_test {
eval {
confess;
};
print "$#\n";
$text->insert("end", "$#\n");
}
Edit
To destroy the window after error message has been shown, you could use:
use strict;
use warnings;
use Carp;
use Tkx;
my $mw = Tkx::widget->new(".");
my $b = $mw->new_button(
-text => "Hello, world",
-command => \&error_test,
);
$b->g_pack;
Tkx::MainLoop();
sub error_test {
eval {
confess;
};
Tkx::tk___messageBox(-message => "$#");
$mw->g_destroy;
}

Report correct location with carp inside Try::Tiny catch block

I'm am using carp in a Try::Tiny catch block, and it always reports the error from the wrong location. The documentation for Try::Tiny says that it adds itself to %Carp::Internal so that it doesn't appear in longer backtraces. But it still appears in the summary line. The documentation for Carp says that adding a package name to %Carp::CarpInternal will remove the package from both the stack trace and the summary line. So I've done that, but the reported location is still incorrect. Here's the code I'm working with:
MyPackage::foo();
package MyPackage;
use Try::Tiny;
use Carp;
use vars qw(%Carp::CarpInternal);
$Carp::CarpInternal{'Try::Tiny'}++;
use Data::Dumper;
print Dumper \%Carp::CarpInternal;
print Dumper \%Carp::Internal;
sub foo {
carp 'bar1';
try{
carp 'bar2'
}catch {
carp $_;
}
}
1;
And here's what it prints:
bar1 at myFile.t line 1.
bar2 at myFile.t line 21.
$VAR1 = {
'Try::Tiny' => 1,
'warnings' => 1,
'Carp' => 1
};
$VAR1 = {
'Try::Tiny' => 1,
'Exporter::Heavy' => 1,
'Exporter' => 1
};
The two variables are what I expect them to be, but I want the errors to both be reported at line 1 of myFile.t. Does anyone know how to fix the error reporting location to be what I want?
In my experiment, this solution only works when the caller comes from a different file. Adding Try::Tiny to the #CARP_NOT variable for the package fixes in this case:
#first file
use MyPackage;
MyPackage::foo();
#second file
package MyPackage;
use Carp qw(carp cluck);
our #CARP_NOT = qw(Try::Tiny);
use Try::Tiny;
sub foo {
carp 'bar1';
cluck 'bar2';
try{
carp 'bar3'
}catch {
cluck $_;
}
}
1;
It prints the following:
bar1 at myFile.t line 2.
bar2 at MyPackage.pm line 8.
MyPackage::foo() called at myFile.t line 2
bar3 at myFile.t line 2.