学习使用 httpd 错误日志调试 CGI 程序
正如我在上一篇文章(LJ,第 34 期,WWW 章节)中讨论的那样,调试 CGI 程序通常比调试非 CGI 程序更困难,仅仅是因为我们的程序不是直接与用户的终端交互。我们只有一次机会从用户那里获取输入——当程序被调用时——也只有一次机会将响应发送回用户——就在程序终止之前,在我们的程序创建的 HTML 中。
这种困难因以下事实而进一步复杂化:正确运行 CGI 程序需要许多其他项到位。例如,您必须正确设置权限位,Web 服务器必须配置为从您的目录提供 CGI 程序,并且必须安装正确版本的 Perl(如果您正在使用 Perl 进行 CGI 任务)。这些都不难,特别是与 Unix 系统的完整管理相比,它并不复杂,但它不像编译和执行其他非 CGI 程序那样简单。
在本系列的上一篇文章中,我们研究了在出现问题时您可以使用的技术。但是,当所有权限和目录都设置正确,但您仍然在浏览器上收到神秘错误时,会发生什么情况?
答案是,虽然 Web 服务器向用户的 Web 浏览器发送通用错误消息,但它们也保留一个“错误日志”,这是一个文件,其中放置了有关每个错误的信息。本月,我们将检查错误日志以查看其中包含的内容,浏览几个错误以及它们在错误日志中如何显示,最后考虑一些我们可以使用错误日志来简化调试的方法。
正如 Unix 程序员所熟知的那样,程序通常可以支配三个基本文件描述符:标准输入 (stdin)、标准输出 (stdout) 和标准错误 (stderr)。
标准输入通常与用户的键盘关联;当您在 Emacs 或类似程序中输入文本时,程序很可能通过 stdin 收集您的击键。 相比之下,标准输出通常返回到您的控制台——无论是您正在输入的窗口还是整个屏幕。
标准错误通常发送到用户的控制台,因此经常与 stdout 混淆。 但两者是完全独立的,可以重定向到不同的位置。 您可以通过尝试以下命令来查看此操作:
ls *zz* > zz-files
在我的系统上,该命令在屏幕上产生以下输出:
ls: *zz*: No such file or directory由于没有任何文件包含 zz 在其名称中,因此文件 zz-files 是空的。 此错误消息被发送到 stderr,该错误消息保留在屏幕上,而(空)文件列表被发送到 stdout,我们将其重定向到名为 zz-files 的文件。
CGI 程序仍然可以访问 stdin、stdout 和 stderr,但是当程序不在用户的控制台上运行时,这些文件的使用方式略有不同。 对于 CGI 程序,stdin 反映了通过 POST 方法提交的 HTML 表单的内容。 毫不奇怪,Stdout 用于发送响应,该响应通常不是 HTML 格式,而是发送给用户。 最后,stderr 被重定向到 HTTP 服务器的错误日志,该日志通常远离网站访问者。
因此,如果我们的程序意外终止,错误消息将被定向到 httpd 错误日志,而不是我们的浏览器。 一方面,这可能是一件好事——毕竟,您可能不希望全世界都知道您的代码中发现的错误的数量和种类。 另一方面,这意味着您需要查看一个单独的文件,并且您需要登录到服务器计算机才能确切地知道您的程序发生了什么。
如果您在 Web 空间提供商或 Internet 服务提供商运行的 Web 服务器上租用空间——或者更重要的是,如果您正在考虑从这样的提供商那里租用空间——请确保您可以访问这些日志。 您当然可以在没有访问错误日志的情况下编写和调试 CGI 程序,但是如果您可以访问它,您的生活将会无限愉快。
每个 HTTP 服务器以略微不同的方式记录信息。 在本专栏中,我将描述 Apache 生成的日志,Apache 是从 NCSA httpd 派生的服务器,由美国国家超级计算应用中心 (NCSA) 编写和分发。 您可以从 https://apache.ac.cn/ 获取 Apache 的副本以及与此优秀服务器相关的文档。
在我的 Apache 副本中,错误消息放置在文件中,每个错误一行。 错误消息的格式为:
[DayOfWeek Month Date Time Year] Message
因此,错误日志中可能的一个条目是:
[Mon Dec 2 16:05:50 1996] Coke machine needs refilling您可能已经猜到,这意味着服务器在 1996 年 12 月 2 日星期一下午 4:05:50 记录了错误。
您的 httpd 可能会在错误日志中记录各种您可能不会归类为错误的内容。 例如,我系统的错误日志包含以下错误消息:
[Tue Nov 5 17:03:48 1996] access to \ /home/httpd/cgi-bin failed for ahad-haam, reason: \ script not found or unable to stat
上面的消息意味着服务器无法在我的 Web 服务器上找到特定文件。 是的,每当有人将其 Web 浏览器指向您的系统并请求不存在的文档时,就会记录错误消息。 这实际上是一件有用的事情,无论是出于安全原因(因为它可能很高兴知道是否有人试图通过您的 Web 服务器抓取敏感文件),还是为了帮助经常错误地请求特定文件的用户。 例如,如果经常以拼写错误的名称请求特定文件,则可以在您的系统上设置别名或符号链接。
错误日志对于系统管理员、网络管理员和网站管理员来说可能非常有趣和有用,但我们接下来将专注于错误日志在尝试调试 CGI 程序时的作用。 例如,尝试以下操作:
#!/usr/local/bin/perl5 -w use strict; # Check our syntax strictly use diagnostics; # Tell us how to fix mistakes use CGI; # Import the CGI module # Create an instance of CGI my $query = new CGI; # Notice that we are *not* sending a MIME # header, which we should always remember # to do. # Begin the HTML print $query->start_html(-title => "Hello, world!)"; # Print something fairly uninteresting print "<P>Hello, World Wide Web!</P>\n"; # End the HTML print $query->end_html();
如果从命令行运行上述程序,它将运行良好,因为唯一的错误是缺少 MIME 标头,这仅对于 CGI 程序是必需的。 但是,如果您尝试从浏览器运行它,您将收到可怕的消息:
Server Error. The server encountered an internal error or misconfiguration and was unable to complete your request.对于调试您的程序来说不是很有用,特别是自从从命令行运行它没有显示任何明显的错误以来。 您如何发现出了什么问题? 通过查看错误日志。
在我的系统上,错误日志包含以下条目:
[Mon Dec 2 17:32:04 1996] access to /home/reuven/Text/Websmith/test-8.pl failed for ahad-haam, reason: malformed header from script
Apache 不仅告诉我们存在错误,并且程序已终止,而且它甚至给出了原因,原因列为“来自脚本的格式错误的标头”。 对于新手来说,这不是最明显的消息,但它告诉您您需要知道的内容,即发送的标头格式不正确,因此导致了错误。
我们现在可以修复程序并通过在其他任何内容之前发送 MIME 标头来删除错误消息:
#!/usr/local/bin/perl5 -w use strict; # Check our syntax strictly use diagnostics; # Tell us how to fix mistakes use CGI; # Import the CGI module # Create an instance of CGI my $query = new CGI; # Send the MIME header print $query->header("text/html"); # Begin the HTML print $query->start_html(-title => "Hello, world!"); # Print something fairly uninteresting print "<P>Hello, World Wide Web!</P>\n"; # End the HTML print $query->end_html();
果然,修复后的程序不会在错误日志中产生任何新消息。
Perl 中的典型习惯用法可能是:
open(FILE, "$filename") || die "Cannot open $filename";
上面的行表示应打开 $filename 以进行读取,并且应通过名为 FILE 的描述符访问打开的文件。 如果程序由于某种原因无法打开文件,则程序应终止,并生成一条消息。 让我们看看这在 CGI 程序中是如何工作的:
#!/usr/local/bin/perl5 -w use strict; # Check our syntax strictly use diagnostics; # Tell us how to fix mistakes use CGI; # Import the CGI module # Create an instance of CGI my $query = new CGI; # Set this to a file my $filename = "/foo/bar/blah.txt"; # Send the MIME header print $query->header("text/html"); # Begin the HTML print $query->start_html(-title => "Hello, world!"); # Print the contents of $filename open (FILE, "$filename") || die "Cannot open $filename "; print while (<FILE>); close (FILE); # End the HTML print $query->end_html();看起来不错,对吧? 好吧,假设 $filename 存在,以上确实可以正常工作。 但是,如果它不存在,或者程序没有读取该文件的权限,则执行语句的后半部分——并且程序终止,并生成错误消息。
如果 $filename 不存在,并且您从命令行运行该程序,您将获得以下输出:
Content-type: text/html <HTML><HEAD><TITLE>Hello, world!</TITLE> </HEAD><BODY>Uncaught exception from user code: Cannot open /tmp/blah.txt at ./test-8.pl line 20.
但是,如果您从 Web 浏览器运行它,您将一无所获——没有错误消息,也没有响应,因为错误消息“Cannot open /tmp/blah.txt”被发送到 stderr,而不是 stdout。 但是,错误日志包含以下内容:
Uncaught exception from user code: Cannot open /tmp/blah.txt at /home/reuven/Text/Websmith/test-8.pl line 20.请注意,错误消息已插入日志中,没有日期或时间信息,因为文本是由 Perl 程序(或 Perl 二进制文件)直接发送的,而不是 Web 服务器。 Web 服务器会自动将当前时间和日期戳记到其自己的错误消息中,但是您的程序不享有该功能——除非,当然,您包含 CGI.pm 附带的 Carp 库,该库重新定义了 die 例程以及其他几个例程,以便错误消息前面有相关的时间和日期。 因此,如果您在上述程序的顶部包含以下行:
use CGI::Carp;程序仍然按预期失败,但您的错误日志现在包含以下条目:
[Tue Dec 3 11:55:14 1996] test-8.pl: Cannot open /tmp/blah.txt at /home/reuven/Text/Websmith/test-8.pl line 21.现在我们不仅知道哪里出了问题,还知道它发生的时间。 当调试 CGI 程序时,Carp 库可能非常有用,特别是如果您发现自己重复运行同一个程序时。 这样,就不会有任何疑问,错误是发生在您第一次还是第五次运行程序时,因为程序运行的时间紧挨着错误消息写入。
安装 Carp 后,您也可以使用它来记录非致命错误。 例如,如果您正在尝试跟踪特定变量的值,则可以包含以下行:
carp 'variablename = "'. $variablename . '"';
因为它包含时间和日期信息,所以这比以下内容更好:
print STDERR 'variablename = "', $variablename, '"';虽然使用 Perl 的内置语法检查器和警告(使用 -w 标志以及 strict 和 diagnostics 模块)是一个好主意,但请注意,加载 CGI::Carp 模块会发出警告,因为它重新定义了几个子例程。 您可以忽略这些警告,但当它们出现时不要感到惊讶。
一直以来,我们都忽略了我们的用户,他们实际上应该是我们努力的重点。 我们改进错误消息以用于调试程序固然很好,但是到目前为止我们研究过的所有想法都没有为用户看到的内容做太多事情。
问题是,如果您使用 die 向错误日志输出消息,则程序会在有机会为用户生成任何类型的“糟糕”消息之前就终止了。 如果您正在构建交互式网站,那么您最不希望做的事情就是向用户呈现空白结果页面。 至少,您应该告诉他出了点问题,并且您正在尝试解决该问题。
一种简单的方法是用以下内容替换行:
open (FILE, "$filename") || die "Cannot open $filename ";
使用以下内容:
open (FILE, "$filename") || &log_and_die ("Cannot open $filename ");这个新版本根据需要打开文件,但如果无法打开文件,则调用名为 log_and_die 的子例程。 然后,您可以将子例程定义如下:
sub log_and_die { # Get arguments my $string = shift; # Begin the HTML print $query->start_html(-title => "Error"); # Give a message print "<P>Sorry, but this program "; print "has encountered an error. The system "; print "administrator has been "; print "informed.</P>\n"; print "<P>Would you like to return to the; print "<a href=\"/\">home page</a>?\n"; # End the HTML print $query->end_html(); # Now die with the error message die $string; }对于大多数口味来说,上述例程显然有点简陋,但重点很明显:通过将错误包装到单独的例程中,您可以两者兼得。 用户会收到一条错误消息,指示他或她访问另一个站点,并为程序的错误深表歉意。 系统管理员或网站管理员会在错误日志中收到一条注释,描述确切的错误原因,从而可以更轻松地修复程序。
您显然可以添加到此例程中。 首先,一个好主意可能是通过电子邮件通知系统管理员或程序员,让他或她知道发生了错误。 如果该程序是关键系统的一部分,那么甚至可能最好向连接到字母数字寻呼系统的系统发送电子邮件。
在以上所有示例中,我们仅在已经将 MIME 标头发送到浏览器(使用内置的 CGI.pm “header”方法)之后才调用 die。 不这样做可能会产生各种奇怪的问题,并导致可怕的消息:
Server Error. The server encountered an internal error or misconfiguration and was unable to complete your request.
例如,运行以下 CGI 程序:
#!/usr/local/bin/perl5 -w use strict; # Check our syntax strictly use diagnostics; # Tell us how to fix mistakes use CGI; # Import the CGI module use CGI::Carp; # Create an instance of CGI my $query = new CGI; # Don't do very much die "This program crashed on purpose ";上面的程序在到达最后一行时终止,并在 httpd 错误日志中插入适当的消息。 用户将在其浏览器上看到什么? 在上面的示例中,用户将收到可怕的:
The server encountered an internal error or misconfiguration and was unable to complete your request.您可以通过在执行任何重要计算之前发送 MIME 标头来缓解此问题:
#!/usr/local/bin/perl5 -w use strict; # Check our syntax strictly use diagnostics; # Tell us how to fix mistakes use CGI; # Import the CGI module use CGI::Carp; # Create an instance of CGI my $query = new CGI; print $query->header("text/html"); # Don't do very much die "This program crashed on purpose ";注意:虽然 CGI::Carp 的文档声称可以缓解此问题,但使用我系统上安装的版本(来自 1996 年 9 月初)进行的实验似乎并非如此。 因此,我建议您尽可能在程序开始时输出 MIME 标头,以缓解此类问题。
尽管当您的程序产生错误消息时,您可能会想到一些色彩丰富的语言,但良好的警告和日志在编写和调试代码时非常有用。 对于 CGI 程序来说尤其如此,在 CGI 程序中,大部分工作都在幕后进行,并且唯一向用户的输出是通过 Web 浏览器进行的。 学习有效地使用 httpd 错误日志,虽然您的编程问题肯定不会消失,但您的错误应该更容易识别和纠正。
Reuven M. Lerner 自 1993 年初以来一直在玩 Web,当时它看起来更像是一个有趣的玩具,而不是世界的下一个伟大媒介。 他目前在以色列海法的公寓里担任独立的 Internet 和 Web 顾问。 在不从事 Web 工作或非正式地为学龄儿童做志愿者时,他喜欢阅读(几乎所有主题,尤其是计算机、政治和哲学——单独和一起)、烹饪、解决纵横字谜和远足。 您可以通过 reuven@the-tech.mit.edu 或 reuven@netvision.net.il 与他联系。