我有一个 Perl CGI 脚本无法正常工作,我不知道如何开始缩小问题范围。我该怎么办?
注意:我添加了这个问题,因为我真的想将我的非常冗长的答案添加到 Stack Overflow 中。我在其他答案中一直外部链接它,它应该出现在这里。如果您有什么要补充的,请不要犹豫编辑我的答案。
我有一个 Perl CGI 脚本无法正常工作,我不知道如何开始缩小问题范围。我该怎么办?
注意:我添加了这个问题,因为我真的想将我的非常冗长的答案添加到 Stack Overflow 中。我在其他答案中一直外部链接它,它应该出现在这里。如果您有什么要补充的,请不要犹豫编辑我的答案。
这篇答案是用作解决Perl CGI脚本问题的一般框架,最初发布于Perlmonks,链接为Troubleshooting Perl CGI Scripts。 它并不是每个可能遇到的问题的完整指南,也不是有关调试错误的教程。它只是我在20年(加上!)调试CGI脚本方面经验的总结。这个页面似乎有许多不同的地址,我似乎忘记了它的存在,所以我将其添加到StackOverflow。您可以通过bdfoy@cpan.org向我发送任何评论或建议。它也是社区wiki,但请不要太过疯狂。 :)
打开警告功能,让Perl提示您代码中可疑的部分。您可以从命令行使用-w
开关来完成此操作,因此您无需更改任何代码或向每个文件添加pragma:
% perl -w program.pl
不过,你应该强制自己通过将warnings
pragma添加到所有文件中来清理有疑问的代码:
use warnings;
diagnostics
pragma 来获取更多信息,或者查看 perldiag 文档: use diagnostics;
服务器期望来自CGI脚本的第一个输出是CGI标头。通常,这可能很简单,例如print "Content-type: text/plain\n\n";
或使用CGI.pm及其派生物,print header()
。一些服务器对错误输出(在STDERR
上)在标准输出(在STDOUT
上)之前显示非常敏感。
添加此行:
use CGI::Carp 'fatalsToBrowser';
将以下代码添加到你的脚本中,这样编译错误也会发送到浏览器窗口。在进入生产环境之前一定要删除它,因为额外的信息可能会造成安全风险。
服务器保存错误日志(或者至少应该保存)。从服务器和你的脚本输出的错误都应该显示在其中。找到错误日志并查看其内容。没有一个标准的日志文件位置。可以查看服务器配置以确定其位置,或者询问服务器管理员。你还可以使用 CGI::Carp 等工具来保留自己的日志文件。
如果出现“Permission denied”或“Method not implemented”等错误,则可能意味着你的脚本不可读且无法执行web服务器用户。在Unix系统上,建议更改模式为755:chmod 755 filename
。永远不要将模式设置为777!
use strict
?记住,当你首次使用变量时,Perl会自动创建变量。这是一个特性,但有时如果你拼写变量名错误,就会导致错误。使用use strict
指令可以帮助你找到这些错误。虽然这很烦人,但是在一段时间后你的编程将显著提高,并且你将有机会犯不同的错误。
你可以使用-c
开关检查编译错误。集中注意力处理首次报告的错误。反复检查。如果出现非常奇怪的错误,请确保你的脚本具有正确的行结束符。如果你以二进制模式FTP,从CVS检出或执行其他不处理行结束符转换的操作,则web服务器可能会将你的脚本视为一个大行。以ASCII模式传输Perl脚本。
-T
开关打开taint模式,这是一件好事,因为它可以避免传递未经检查的数据到shell中。如果出现投诉,则说明它正在帮助我们编写更安全的脚本。任何来自程序外部(即环境)的数据都被认为是有污染的。像PATH
和LD_LIBRARY_PATH
这样的环境变量特别麻烦。您必须将其设置为安全值或完全取消设置,建议取消设置。无论如何,您都应该使用绝对路径。如果taint检查投诉其他内容,请确保已将数据除污。有关详细信息,请参见perlsec手册。
从命令行运行脚本时,它是否输出您期望的内容?头部输出首先显示,接着是一个空行吗?请记住,如果您在终端上(例如交互式会话),则STDERR
可能会与STDOUT
合并,并且由于缓冲可能以混乱的顺序显示。通过将$|
设置为真值来打开Perl的自动刷新功能。通常,您可以在CGI程序中看到$|++;
。一旦设置,每个打印和写入都会立即发送到输出而不是被缓冲。您必须为每个文件句柄设置此选项。使用select
更改默认文件句柄,例如:
$|++; #sets $| for STDOUT
$old_handle = select( STDERR ); #change to STDERR
$|++; #sets $| for STDERR
select( $old_handle ); #change back to STDOUT
use CGI qw(-debug)
die()
或 warn
吗?除非你重新定义了它们,否则这些函数会输出到 STDERR
。它们也不会输出 CGI 标头。你可以使用像 CGI::Carp 这样的包来获得相同的功能。
如果你认为你的脚本在做正确的事情,并且手动执行请求时得到了正确的输出,那么浏览器可能是罪魁祸首。在测试过程中清除缓存并将缓存大小设置为零。记住,有些浏览器非常愚蠢,即使你告诉它重新加载新内容,它也实际上不会重新加载。这在 URL 路径相同但内容更改的情况下尤为普遍(例如,动态图像)。
脚本的文件系统路径与脚本的 URL 路径不一定直接相关。确保你拥有正确的目录,即使你必须编写一个简短的测试脚本来测试这一点。此外,你确定正在修改正确的文件吗?如果你对你的更改没有看到任何影响,可能是你修改了其他文件,或者将文件上传到错误的位置。(顺便说一下,这是我最常见的问题原因 ;))
CGI.pm
或其衍生产品?如果你的问题与解析 CGI 输入有关,并且你没有使用像 CGI.pm
、CGI::Request
、CGI::Simple
或 CGI::Lite
这样广泛测试过的模块,则使用这些模块并继续生活。 CGI.pm
具有一个 cgi-lib.pl
兼容模式,可以帮助你解决由于旧 CGI 解析器实现而导致的输入问题。
如果你正在使用 system
、反引号或其他IPC工具来运行外部命令,则应该使用绝对路径来指定外部程序的位置。这样不仅可以确切知道你要运行什么,还可以避免一些安全问题。如果你需要打开文件进行读写操作,请使用绝对路径。CGI脚本可能会与你的当前目录有不同的想法。或者,你可以显式地使用 chdir()
来进入正确的目录。
大多数Perl函数会告诉你它们是否正常运行,并在失败时设置 $!
。你是否检查了返回值并检查过 $!
是否有错误信息?如果你使用了 eval
,是否检查了 $@
?
最新的稳定版Perl是5.28(这取决于上次编辑的时间)。你使用的是旧版本吗?不同版本的Perl可能会有不同的警告信息。
不同的服务器在相同情况下可能会有不同的行为。同一种服务器产品在不同的配置下也可能会有不同的行为。在请求帮助时,请尽可能提供尽可能多的信息。
严肃的CGI程序员应该尽可能了解服务器,包括服务器的特性和行为,以及本地配置。如果你使用的是商业产品,则可能无法获取你的服务器文档。否则,文档应该在你的服务器上。如果没有,请在网上寻找。
comp.infosystems.www.authoring.cgi
的档案中进行了搜索吗?这曾经很有用,但所有好的作者都已经离开了。
很可能有人之前遇到了你的问题,并且某个人(可能是我)在这个新闻组中回答了它。虽然这个新闻组已经过时了,但过去积累的智慧有时仍然很有用。
在大型系统中,跟踪错误可能很困难,因为会发生许多事情。尝试用最简短的脚本重现问题行为。知道问题是解决问题的一半。这可能需要耗费大量时间,但你还没有找到问题,而且已经没有其他选项了。 :)
说真的。有时我们太过于沉浸在问题中,导致出现"感知收窄"(隧道视野)。休息一下,喝杯咖啡,或者在[Duke Nukem,Quake,Doom,Halo,COD]中消灭一些坏蛋,可能会给你带来新的视角,让你重新解决问题。
再次说真的。有时将问题口头解释出来,会让我们找到答案。跟企鹅(毛绒玩具)交谈,因为你的同事们并没有在听。如果你对这个作为一个严肃的调试工具感兴趣(如果你到现在还没有找到问题,我建议你尝试一下),你可能也会喜欢阅读《计算机编程心理学》。
$|=1
而是用$|++
? - reinierpost$|=1
而不是$|++
?实际上并没有什么区别,即使如此,$|
也是一个神奇的变量。 - brian d foyuse strict
通常在任何时候都是好的选择,而使用fatalsToBrowser
可能不建议在生产环境中使用,特别是如果你正在使用die
。 - vol7ron
die
语句和其他致命的运行时和编译时错误会被打印到STDERR
中,这可能很难找到,并且可能与您站点上其他网页的消息混淆。在调试脚本时,最好将致命错误消息以某种方式显示在您的浏览器中。
一种方法是调用
use CGI::Carp qw(fatalsToBrowser);
在您的脚本顶部添加该调用将安装一个$SIG{__DIE__}
处理程序(请参阅perlvar),在必要时使用有效的标头在浏览器中显示致命错误。我在听说CGI::Carp
之前使用的另一个CGI调试技巧是使用DATA
和__END__
工具与eval
来捕获编译时错误:
#!/usr/bin/perl
eval join'', <DATA>;
if ($@) { print "Content-type: text/plain:\n\nError in the script:\n$@\n; }
__DATA__
# ... actual CGI script starts here
这种更冗长的技术与CGI::Carp
相比,稍微具有一定的优势,因为它可以捕获更多的编译时错误。
更新:我从未使用过它,但看起来CGI::Debug
,就像Mikael S建议的那样,也是一个非常有用且可配置的工具,用于此目的。
eval join(q{}, <DATA>);
。 - derobert$ cd /tmp
$ perl -MIO::All -e 'io(":8080")->fork->accept->(sub { $_[0] < io(-x $1 ? "./$1 |" : $1) if /^GET \/(.*) / })'
使用webserver命令会在终端上阻塞,但同时也会在本地启动Web服务器(在127.0.0.1或localhost
上)- 然后,我可以打开Web浏览器,并请求该地址:
http://127.0.0.1:8080/test.pl
我需要观察在网页浏览器中加载并显示的test.pl
脚本所产生的print
信息。
现在,为了使用RemotePort
来调试这个脚本,我们首先需要在网络上建立一个监听器来与Perl调试器进行交互;我们可以使用命令行工具netcat
(nc
),如此在这里看到:Perl如何remote debug?。因此,在一个终端中运行netcat
监听器 - 它将阻塞并等待在端口7234上的连接(这将是我们的调试端口):
$ nc -l 7234
接下来,我们希望在调试模式下使用RemotePort
启动perl
,当调用test.pl
时(即使是通过服务器的CGI模式)。 在Linux中,可以使用以下“shebang wrapper”脚本实现 - 这里还需要在/tmp
中,并且必须可执行:
cd /tmp
cat > perldbgcall.sh <<'EOF'
#!/bin/bash
PERLDB_OPTS="RemotePort=localhost:7234" perl -d -e "do '$@'"
EOF
chmod +x perldbgcall.sh
test.pl
的perl
解释器 - 一旦我们遇到它,就不要exec
,而是直接“原样”调用perl
,并使用do
“源”我们的test.pl
脚本(请参见How do I run a Perl script from within a Perl script?)。/tmp/perldbgcall.sh
,我们可以更改test.pl
文件,使其在她的shebang行中引用这个可执行文件(而不是通常的Perl解释器) - 这里是修改后的/tmp/test.pl
:#!./perldbgcall.sh
# this is test.pl
use 5.10.1;
use warnings;
use strict;
my $b = '1';
my $a = sub { "hello $b there" };
$b = '2';
print "YEAH " . $a->() . " CMON\n";
$b = '3';
print "CMON " . &$a . " YEAH\n";
$DB::single=1; # BREAKPOINT
$b = '4';
print "STEP " . &$a . " NOW\n";
$b = '5';
print "STEP " . &$a . " AGAIN\n";
test.pl
和它的新shebang处理程序perldbgcall.sh
都在/tmp
目录中;我们已经让nc
在7234端口上监听调试连接 - 所以我们最终可以打开另一个终端窗口,切换到/tmp
目录,并在那里运行一行式Web服务器(将在8080端口上监听Web连接):cd /tmp
perl -MIO::All -e 'io(":8080")->fork->accept->(sub { $_[0] < io(-x $1 ? "./$1 |" : $1) if /^GET \/(.*) / })'
http://127.0.0.1:8080/test.pl
。然而,现在当网络服务器试图执行脚本时,它会通过perldbgcall.sh
shebang启动perl
进入远程调试模式。因此,脚本执行将暂停,网页浏览器也会锁定,等待数据。现在,我们可以切换到netcat
终端,然后就应该看到熟悉的Perl调试器文本,但是通过nc
输出:$ nc -l 7234
Loading DB routines from perl5db.pl version 1.32
Editor support available.
Enter h or `h h' for help, or `man perldebug' for more help.
main::(-e:1): do './test.pl'
DB<1> r
main::(./test.pl:29): $b = '4';
DB<1>
nc
用作“终端” - 因此我们可以键入r
(并按Enter键)以运行脚本,直到断点语句(请参见在Perl中,$DB::single = 1和2有什么区别?),然后再次停止(请注意,在此时浏览器仍然会锁定)。
因此,现在我们可以通过nc
终端逐步执行其余的test.pl
代码:
....
main::(./test.pl:29): $b = '4';
DB<1> n
main::(./test.pl:30): print "STEP " . &$a . " NOW\n";
DB<1> n
main::(./test.pl:31): $b = '5';
DB<1> n
main::(./test.pl:32): print "STEP " . &$a . " AGAIN\n";
DB<1> n
Debugged program terminated. Use q to quit or R to restart,
use o inhibit_exit to avoid stopping after program termination,
h q, h R or h o to get additional info.
DB<1>
然而,在这一点上,浏览器会锁定并等待数据。只有在我们使用q
退出调试器之后才能继续执行:
DB<1> q
$
浏览器何时停止锁定 - 最终显示 test.pl
的(完整)输出:
YEAH hello 2 there CMON
CMON hello 3 there YEAH
STEP hello 4 there NOW
STEP hello 5 there AGAIN
当然,即使不运行Web服务器,也可以进行这种调试 - 不过,这里的巧妙之处在于,我们根本不会触及Web服务器; 我们从Web浏览器“本地”触发执行(对于CGI),而CGI脚本本身所需的唯一更改是修改shebang(当然还有,在同一目录中作为可执行文件存在的shebang包装脚本)。
希望这能帮到其他人 - 我肯定很想偶然发现这个,而不是自己编写它:)
干杯!
对我来说,我使用log4perl。它非常有用且易于使用。
use Log::Log4perl qw(:easy);
Log::Log4perl->easy_init( { level => $DEBUG, file => ">>d:\\tokyo.log" } );
my $logger = Log::Log4perl::get_logger();
$logger->debug("your log message");
`$somecommand`;
为了确定它是否真正执行我想要的操作:(故障排除)
print "$somecommand";
值得一提的是,当您从命令行执行Perl脚本时(例如SSH会话),Perl将始终告诉您错误发生在哪一行。
如果其他方法都失败了,我通常会这样做。我会SSH到服务器并手动执行Perl脚本。例如:
% perl myscript.cgi
您可以使用以下命令在终端中运行 Perl CGI 脚本
$ perl filename.cgi
perl -c filename
确实只会检查语法。但是 perl filename
会打印 HTML 输出。虽然这并不能保证不会出现 500 CGI 错误,但它是一个很好的第一次测试。 - Nagev