我该如何排除我的Perl CGI脚本问题?

105

我有一个 Perl CGI 脚本无法正常工作,我不知道如何开始缩小问题范围。我该怎么办?


注意:我添加了这个问题,因为我真的想将我的非常冗长的答案添加到 Stack Overflow 中。我在其他答案中一直外部链接它,它应该出现在这里。如果您有什么要补充的,请不要犹豫编辑我的答案。


如果您的网络浏览器显示出现了行噪声,那么它可能正在打印Perl脚本。在这种情况下,请参阅http://stackoverflow.com/questions/2621161/cgi-perl-script-is-printing-the-html-along-with-content-type-text-html-strin - Andrew Grimm
8个回答

135

这篇答案是用作解决Perl CGI脚本问题的一般框架,最初发布于Perlmonks,链接为Troubleshooting Perl CGI Scripts。 它并不是每个可能遇到的问题的完整指南,也不是有关调试错误的教程。它只是我在20年(加上!)调试CGI脚本方面经验的总结。这个页面似乎有许多不同的地址,我似乎忘记了它的存在,所以我将其添加到StackOverflow。您可以通过bdfoy@cpan.org向我发送任何评论或建议。它也是社区wiki,但请不要太过疯狂。 :)


你是否使用了Perl内置功能来帮助找到问题?

打开警告功能,让Perl提示您代码中可疑的部分。您可以从命令行使用-w开关来完成此操作,因此您无需更改任何代码或向每个文件添加pragma:

 % perl -w program.pl

不过,你应该强制自己通过将warnings pragma添加到所有文件中来清理有疑问的代码:

 use warnings;

如果您需要更多信息而不仅仅是简短的警告消息,请使用 diagnostics pragma 来获取更多信息,或者查看 perldiag 文档:
 use diagnostics;

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

服务器期望来自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中。如果出现投诉,则说明它正在帮助我们编写更安全的脚本。任何来自程序外部(即环境)的数据都被认为是有污染的。像PATHLD_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

无论哪种情况,输出的第一件事应该是CGI头,然后是一个空行。
当您在类似CGI的环境下从命令行运行它时会发生什么?
Web服务器环境通常比命令行环境受限,并且具有有关请求的额外信息。如果您的脚本在命令行中运行正常,则可以尝试模拟Web服务器环境。如果出现问题,则说明存在环境问题。
取消设置或删除这些变量
- PATH - LD_LIBRARY_PATH - 所有ORACLE_*变量
设置这些变量
- REQUEST_METHOD(根据需要设置为GET、HEAD或POST) - 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::SimpleCGI::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]中消灭一些坏蛋,可能会给你带来新的视角,让你重新解决问题。

你口头表达了问题吗?

再次说真的。有时将问题口头解释出来,会让我们找到答案。跟企鹅(毛绒玩具)交谈,因为你的同事们并没有在听。如果你对这个作为一个严肃的调试工具感兴趣(如果你到现在还没有找到问题,我建议你尝试一下),你可能也会喜欢阅读《计算机编程心理学》


似乎您想要删除CGI元FAQ的链接。5.12.1被认为是“稳定”的吗? - Snake Plissken
1
为什么不使用$|=1而是用$|++ - reinierpost
为什么要使用$|=1而不是$|++?实际上并没有什么区别,即使如此,$|也是一个神奇的变量。 - brian d foy
2
不错的回答,我认为值得一提的是,其中一些解决方案应该纯粹用于故障排除,而不应该放入生产代码中。use strict通常在任何时候都是好的选择,而使用fatalsToBrowser可能不建议在生产环境中使用,特别是如果你正在使用die - vol7ron
1
“去看电影”可以获得额外的奖励分数!这乍一看似乎微不足道,但实际上并非如此。我无法计算有多少次我离开了问题,回来后问题突然变得明显,就像魔术一样。这确实是一个有价值的故障排除提示。 - Hawk
显示剩余3条评论

10

7
我是一位有用的助手,可以为您翻译文本。

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

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建议的那样,也是一个非常有用且可配置的工具,用于此目的。


3
@Ether:“<DATA>”是一个特殊的文件句柄,可以读取当前脚本中从“__END__”开始的内容。使用join函数提供列表上下文,因此<fh>返回一个数组,每个项为一行。然后使用join函数将它们连接起来(用''空字符连接)。最后,使用eval函数执行。 - derobert
@Ether:更易读的第二行代码写法是:eval join(q{}, <DATA>); - derobert
@derobert:实际上,__DATA__是用于开始数据段的标记,而不是__END__(我想那是我的困惑)。 - Ether
1
@Ether:实际上,它们都可以在顶层脚本中使用(根据perldata manpage)。但是由于__DATA__更受欢迎,所以我已经改变了答案。 - derobert
@derobert:感谢提供文档链接;我之前并不知道__END__具有向后兼容的行为! - Ether

7
我想知道为什么没有人提到名为"RemotePort"的"PERLDB_OPTS"选项; 尽管在网络上很少有有效的例子(perldebug中甚至没有提到"RemotePort") - 我也有点棘手,但这是解决问题的方法(以下是一个Linux示例)。
要做一个正确的示例,首先我需要找到一些能够通过单个命令行进行非常简单的CGI Web服务器模拟的东西。在找到Simple command line web server for running cgis. (perlmonks.org)之后,我发现IO::All - A Tiny Web Server适用于此测试。
在这里,我将在"/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

我需要观察在网页浏览器中加载并显示的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

这是个有点棘手的问题,可以参见shell script - How can I use environment variables in my shebang? - Unix & Linux Stack Exchange。但是,解决方法似乎是不要派生处理test.plperl解释器 - 一旦我们遇到它,就不要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包装脚本)。

希望这能帮到其他人 - 我肯定很想偶然发现这个,而不是自己编写它:)
干杯!


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");

2
说实话,在这篇文章之前你可以做所有有趣的事情。 尽管如此,我发现最简单且最积极的解决方案是只需“打印它”即可。
例如: (普通代码)
`$somecommand`;

为了确定它是否真正执行我想要的操作:(故障排除)

print "$somecommand";

你能解释一下这如何应用于调试CGI脚本吗? - U. Windl

1

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

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

% perl myscript.cgi 

如果出现问题,Perl 会告诉你。这种调试方法消除了任何与文件权限、Web 浏览器或 Web 服务器相关的问题。

Perl并不总是告诉你错误发生的行号,它只会告诉你它意识到有问题的行号。错误很可能已经发生了。 - brian d foy

0

您可以使用以下命令在终端中运行 Perl CGI 脚本

 $ perl filename.cgi

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

2
抱歉,命令$ perl -c filename.cgi 用于验证代码的语法并报告任何错误。它不会提供cgi的html代码。 - D.Karthikeyan
调用 perl -c filename 确实只会检查语法。但是 perl filename 会打印 HTML 输出。虽然这并不能保证不会出现 500 CGI 错误,但它是一个很好的第一次测试。 - Nagev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接