如何对Perl CGI脚本进行故障排除?


100

我有一个不起作用的Perl脚本,而且我不知道如何开始缩小问题的范围。我能做什么?


注意:我添加这个问题是因为我真的很想在Stackoverflow中添加很长的答案。我在其他答案中一直保持外部链接,因此值得在这里。如果您要添加一些内容,请不要羞于编辑我的答案。


5
@Evan-我的意思是,即使非CW,它仍然可以编辑- 如果您有足够的业力;CW为100,否则为2k。因此,现在您有了2060,您应该可以编辑非CW帖子。
Marc Gravell

1
@Evan,魔术要点列在此处右手提示中的工具提示中:stackoverflow.com/privileges
cjm 2010年

如果您的Web浏览器显示线路噪音,则可能是打印了perl脚本。在这种情况下,看到stackoverflow.com/questions/2621161/...
安德鲁·格林

Answers:


129

该答案旨在作为解决Perl CGI脚本问题的通用框架,最初作为Perl CGI脚本故障排除出现在Perlmonks上。它不是您可能遇到的每个问题的完整指南,也不是有关漏洞压缩的教程。这只是我调试CGI脚本二十年(加倍!)的经验的结晶。该页面似乎有许多不同的位置,而且我似乎忘记了它的存在,因此将其添加到StackOverflow中。您可以通过bdfoy@cpan.org向我发送任何意见或建议。它也是社区Wiki,但不要太过分。:)


您是否正在使用Perl的内置功能来帮助您发现问题?

打开警告,使Perl可以警告您有关代码中有问题的部分。您可以使用命令行从命令行执行此操作,-w因此无需更改任何代码或向每个文件添加杂注:

 % perl -w program.pl

但是,您应该通过向warnings所有文件添加编译指示来强迫自己始终清除可疑的代码:

 use warnings;

如果您需要除简短警告消息之外的更多信息,请使用diagnostics编译指示获取更多信息,或查看perldiag文档:

 use diagnostics;

您是否首先输出了有效的CGI标头?

服务器期望CGI脚本的第一个输出是CGI标头。通常,这可能print "Content-type: text/plain\n\n";CGI.pm及其衍生版本一样简单print header()。某些服务器对STDERR在标准输出(on STDOUT)之前显示的错误输出(on )敏感。

尝试向浏览器发送错误

添加此行

 use CGI::Carp 'fatalsToBrowser';

到您的脚本。这还会将编译错误发送到浏览器窗口。在移至生产环境之前,请确保将其删除,因为额外的信息可能会带来安全风险。

错误日志说了什么?

服务器保留错误日志(或者至少应该保留)。服务器和脚本的错误输出应显示在此处。找到错误日志并查看错误信息。日志文件没有标准位置。在服务器配置中查找其位置,或询问服务器管理员。您也可以使用CGI :: Carp之类的工具 来保存自己的日志文件。

脚本的权限是什么?

如果看到诸如“拒绝权限”或“未实现方法”之类的错误,则可能意味着Web服务器用户无法读取和执行您的脚本。在Unix上,建议将模式更改为755 : chmod 755 filename。切勿将模式设置为777!

您正在使用use strict吗?

请记住,Perl首次使用时会自动创建变量。这是一项功能,但是如果您错误输入变量名有时会导致错误。实用程序 use strict将帮助您找到那些错误。在您习惯之前,这很烦人,但是一段时间之后您的编程将大为改善,并且您可以自由地犯其他错误。

脚本可以编译吗?

您可以使用-c 开关检查编译错误。专注于报告的第一个错误。冲洗,重复。如果您遇到了非常奇怪的错误,请检查并确保脚本的行尾正确。如果以二进制模式FTP,从CVS签出或其他不处理行尾转换的内容,则Web服务器可能会将您的脚本视为一大行。以ASCII模式传输Perl脚本。

脚本是否抱怨不安全的依赖关系?

如果您的脚本抱怨不安全的依赖关系,那么您可能正在使用该-T开关打开异味模式,这是一件好事,因为它可以使您将未经检查的数据传递给外壳程序。如果它抱怨,它正在努力帮助我们编写更安全的脚本。来自程序外部(即环境)的任何数据均被视为污染。环境变量(例如PATH和) LD_LIBRARY_PATH 特别麻烦。您必须按照我的建议将它们设置为安全值或完全取消设置它们。无论如何,您都应该使用绝对路径。如果污点检查抱怨其他问题,请确保您未污染数据。有关 详细信息,请参见perlsec手册页。

从命令行运行它会发生什么?

从命令行运行时,脚本是否输出您期望的结果?标题是先输出,然后是空行吗?请记住, 如果您在终端上(例如,交互式会话),STDERR可能会与之合并STDOUT,并且由于缓冲,可能会以混乱的顺序显示。通过设置$|为true值来打开Perl的自动刷新功能。通常,您可能会$|++;在CGI程序中看到。设置后,每次打印和写入将立即进入输出而不是被缓冲。您必须为每个文件句柄设置此设置。使用select更改默认的文件句柄,就像这样:

$|++;                            #sets $| for STDOUT
$old_handle = select( STDERR );  #change to STDERR
$|++;                            #sets $| for STDERR
select( $old_handle );           #change back to STDOUT

无论哪种方式,第一件事的输出都应该是CGI标头,后跟空白行。

在类似CGI的环境中从命令行运行它会发生什么?

Web服务器环境通常比命令行环境要受限制得多,并且具有有关请求的更多信息。如果您的脚本可以从命令行正常运行,则可以尝试模拟Web服务器环境。如果出现问题,则说明您有环境问题。

取消设置或删除这些变量

  • PATH
  • LD_LIBRARY_PATH
  • 所有ORACLE_*变量

设置这些变量

  • REQUEST_METHOD(设置为GETHEADPOST适当)
  • SERVER_PORT (通常设置为80)
  • REMOTE_USER (如果您正在做受保护的访问操作)

CGI.pm(> 2.75)的最新版本要求该-debug标志具有旧的(有用的)行为,因此您可能必须将其添加到CGI.pm导入中。

use CGI qw(-debug)

您在使用die()还是warn

STDERR除非您重新定义了这些功能,否则这些功能会打印到。它们也不输出CGI标头。您可以使用CGI :: Carp之类的软件包获得相同的功能。

清除浏览器缓存后会发生什么?

如果您认为脚本执行正确的操作,并且在手动执行请求时获得正确的输出,则浏览器可能是罪魁祸首。在测试时,清除缓存并将缓存大小设置为零。请记住,有些浏览器确实很愚蠢,即使您告诉这样做,也不会实际重新加载新内容。这在URL路径相同但内容更改(例如动态图像)的情况下尤其普遍。

您认为脚本在哪里吗?

脚本的文件系统路径不一定与脚本的URL路径直接相关。确保您具有正确的目录,即使您必须编写简短的测试脚本来对此进行测试。此外,您确定要修改正确的文件吗?如果您所做的更改没有效果,则可能是您正在修改其他文件,或者将文件上传到错误的位置。(顺便说一下,这是造成此类麻烦的最常见原因;)

您在使用CGI.pm还是它的衍生产品?

如果你的问题与解析CGI输入和你没有使用广泛的测试模块一样CGI.pmCGI::RequestCGI::Simple或者CGI::Lite,使用模块和享受生活。 CGI.pm具有cgi-lib.pl兼容模式,可以帮助您解决由于较旧的CGI解析器实现而导致的输入问题。

您使用绝对路径了吗?

如果您正在使用system,后退标记或其他IPC工具运行外部命令 ,则应使用指向外部程序的绝对路径。您不仅可以确切知道正在运行的内容,而且还可以避免一些安全问题。如果要打开文件进行读取或写入,请使用绝对路径。CGI脚本对当前目录的想法可能与您不同。另外,您可以明确chdir()地将您放在正确的位置。

您检查返回值了吗?

大多数Perl函数都会告诉您它们是否起作用,并且会$!在失败时进行设置。您是否检查了返回值并检查$!了错误消息?您检查了 $@是否在使用eval吗?

您正在使用哪个版本的Perl?

Perl的最新稳定版本是5.28(或不是,取决于它的最后编辑时间)。您使用的是旧版本吗?不同版本的Perl可能有不同的警告概念。

您正在使用哪个Web服务器?

不同的服务器在相同情况下的行为可能不同。同一服务器产品在不同配置下的行为可能有所不同。在任何请求帮助中都应尽可能包含这些信息。

您检查服务器文档了吗?

认真的CGI程序员应该尽可能了解服务器-不仅包括服务器功能和行为,还包括本地配置。如果您使用的是商业产品,则服务器的文档可能不可用。否则,文档应在您的服务器上。如果不是,请在网络上查找。

您搜索的档案了comp.infosystems.www.authoring.cgi吗?

这项功能很有用,但所有好的海报都已死亡或徘徊。

可能有人曾经遇到过您的问题,并且有人(可能是我)已经在此新闻组中回答了问题。尽管此新闻组已经过了鼎盛时期,但从过去收集的智慧有时还是有用的。

您可以用简短的测试脚本重现该问题吗?

在大型系统中,由于发生了很多事情,可能难以跟踪错误。尝试使用尽可能短的脚本重现问题行为。知道问题是大多数解决办法。这可能确实很耗时,但是您尚未发现问题,并且选件已用完。:)

您决定去看电影吗?

说真的 有时,我们对问题的关注如此之深,以至于我们发展出“感知范围缩小”(隧道视野)。休息片刻,喝杯咖啡,或在[杜克·努克(Duke Nukem),雷神之锤(Quake),毁灭战士(Doom),光晕(Halo),化学需氧量(COD)]中炸死一些坏人,可能会为您提供重新解决问题所需的崭新视角。

你说过问题了吗?

再次认真。有时大声解释问题会使我们得到自己的答案。与您的企鹅(毛绒玩具)交谈,因为您的同事没有在听。如果您对此感兴趣,可以将其作为一种认真的调试工具使用(如果您现在还没有发现问题,我建议您这样做),那么您可能还想阅读《计算机编程心理学》


4
如果要添加一些内容,请不要怕编辑我的答案。
brian d foy 2010年

似乎您可能希望删除指向CGI meta FAQ的链接。5.12.1是否被视为“稳定”?
Snake Plissken

1
为什么不$|=1代替$|++呢?
reinierpost 2011年

为什么要$|=1代替$|++?它并没有真正的改变,即使那样,$|也是神奇的。
brian d foy

2
很好的答案,我认为可能值得一提的是,其中一些解决方案应仅用于故障排除,而不应用于生产代码中。 use strict通常在任何时候都fatalsToBrowser可以使用,但在生产中可能不建议使用,尤其是在使用时die
vol7ron 2012年


7

调试时是否使用错误处理程序?

die语句和其他致命的运行时和编译时错误被打印到STDERR,这可能很难找到,并且可能与站点上其他网页的消息混淆。在调试脚本时,最好将致命错误消息以某种方式显示在浏览器中。

一种方法是致电

   use CGI::Carp qw(fatalsToBrowser);

在脚本的顶部。该调用将安装一个$SIG{__DIE__}处理程序(请参阅perlvar),在您的浏览器中显示致命错误,并在必要时添加一个有效的标头。我之前从未听说过的另一个CGI调试技巧是与脚本上的and 工具CGI::Carp一起使用eval,以捕获编译时错误:DATA__END__

   #!/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::DebugMikael S所建议的那样,也是用于此目的的非常有用且可配置的工具。


3
@Ether:<DATA>是一个魔术文件句柄,可读取以开头的当前脚本__END__。Join为它提供了列表上下文,因此<fh>返回一个数组,每一项一行。然后join将其放回一起(与”联接)。最后,评估。
derobert 2010年

@Ether:编写第2行的方法更容易理解:eval join(q{}, <DATA>);
derobert 2010年

@derobert:实际上,__ DATA__是用于启动数据部分的令牌,而不是__END__(我认为这是我的困惑)。
以太2010年

1
@Ether:好吧,实际上,它们都在顶级脚本中工作(根据perldata联机帮助页)。但是由于首选DATA,因此我更改了答案。
derobert 2010年

@derobert:感谢您的文档链接;我不知道__END__的向后兼容行为!
以太2010年

7

我不知道为什么没有人提到PERLDB_OPTS称为的选项RemotePort。尽管公认的是,网络上没有很多可用的示例(RemotePort甚至在perldebug中也没有提到)-我想出这个示例有点麻烦,但是可以解决了(这是一个Linux示例)。

为了做一个合适的例子,首先我需要做一些可以对CGI Web服务器进行非常简单的模拟的东西,最好是通过单个命令行。找到用于运行cgis的简单命令行Web服务器之后。(perlmonks.org),我发现IO :: All- 一个小型Web服务器适用于此测试。

在这里,我将在/tmp目录中工作;CGI脚本将/tmp/test.pl(包含在下面)中。请注意,该IO::All服务器仅在与CGI相同的目录中提供可执行文件,因此chmod +x test.pl此处是必需的。因此,为了进行常规的CGI测试运行,我/tmp在终端中将目录更改为,然后在其中运行单层Web服务器:

$ 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

......我应该遵守print由制造的S test.pl加载-和显示-在Web浏览器。


现在,要使用调试此脚本RemotePort,首先,我们需要网络上的侦听器,通过该侦听器与Perl调试器进行交互;我们可以使用命令行工具netcatnc,在这里看到:Perl如何远程调试?)。因此,首先netcat在一个终端上运行侦听器-它将阻塞并等待端口7234(这将是我们的调试端口)上的连接:

$ nc -l 7234

然后,我们希望perl在调用RemotePort时以调试模式启动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

这有点棘手-请参阅shell脚本-如何在shebang中使用环境变量?-Unix和Linux堆栈交换。但是,这里的技巧似乎不是派生perl处理的解释器test.pl -因此,一旦我们击中它,我们就不会这样做exec,而是调用perl“简单地”,并基本上使用“源”我们的test.pl脚本do(请参阅如何运行脚本)。来自Perl脚本中的Perl脚本?)。

现在,我们已经perldbgcall.sh/tmp-我们可以改变的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,然后运行单行网络服务器(它将侦听端口8080上的网络连接):

cd /tmp
perl -MIO::All -e 'io(":8080")->fork->accept->(sub { $_[0] < io(-x $1 ? "./$1 |" : $1) if /^GET \/(.*) / })'

完成此操作后,我们可以转到Web浏览器,并请求相同的地址http://127.0.0.1:8080/test.pl。但是,现在,当网络服务器尝试执行脚本时,它将通过perldbgcall.shshebang 来执行-这将perl在远程调试器模式下启动。因此,脚本执行将暂停-这样,Web浏览器将锁定,等待数据。现在我们可以切换到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为“ run” 键入(和回车)-脚本将运行断点语句(另请参见在perl中,$之间的区别是什么DB :: single = 1 and 2?),然后再次停止(请注意,此时浏览器仍将锁定)。

因此,现在我们可以说test.pl,通过nc终端逐步完成其余的:

....
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服务器。我们从网络浏览器“本地”触发执行(对于CGI)-CGI脚本本身唯一需要的更改就是shebang的更改(当然,shebang包装脚本的存在,作为可执行文件在同一文件夹中)目录)。

好吧,希望这对某人有帮助-我当然会喜欢偶然发现这一点,而不是自己写下来:)
加油!


5

对我来说,我使用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");

1

老实说,您可以在这篇文章上做所有有趣的事情。我发现,最简单,最主动的解决方案是“打印”。

例如:(普通代码)

`$somecommand`;

查看它是否正在执行我真正想要的操作:(故障排除)

print "$somecommand";

1

值得一提的是,当您从命令行执行Perl脚本时,Perl总是会告诉您错误发生在哪一行。(例如SSH会话)

如果其他所有方法都失败,我通常会这样做。我将通过SSH进入服务器并手动执行Perl脚本。例如:

% perl myscript.cgi 

如果有问题,Perl会告诉您。此调试方法消除了任何与文件权限有关的问题或Web浏览器或Web服务器问题。


Perl并不总是告诉您发生错误的行号。它告诉您发现存在问题的行号。该错误可能已经发生。
brian d foy 2014年

0

您可以使用以下命令在终端中运行perl cgi-script

 $ perl filename.cgi

它解释代码并提供带有HTML代码的结果。如果有错误,它将报告错误。


1
抱歉,命令$ perl -c filename.cgi验证代码的语法并报告错误(如果有)。它不会提供cgi的html代码。
D.Karthikeyan '16

调用perl -c filename确实只会检查语法。但是perl filename会打印HTML输出。虽然不能保证不会出现500 CGI错误,但这是一个很好的第一个测试。
纳吉夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.