import repository from arizona
[raven.git] / lib / arizona-lib / arizonaerror.py
1 #! /usr/bin/env python
2
3 """
4 <Program Name>
5    storkerror.py
6
7 <Started>
8    November 4, 2005
9
10 <Author>
11    Jeffry Johnston
12
13 <Purpose>
14    Error reporting routines used by stork.
15    See also: arizonageneral.error_report()
16 """
17
18 import sys
19 import arizonaconfig
20 import arizonageneral
21 import traceback
22 import arizonawarning
23
24
25
26
27 #           [option, long option,               variable,          action,        data, default, metavar, description]
28 """arizonaconfig
29    options=[
30             ["",     "--disableerrorreporting", "senderrorreport", "store_false", None, True,    None,    "disable sending of error reports to the Stork team"],
31             ["",     "--debug",                 "debug",           "store_true",  None, False,   None,    "disable catching errors and generation/sending of error reports"],
32             ["",     "--filtererror",           "filtererror",     "append",      "string", None, None,   "filter error messages from emailed error reports"]
33            ]
34    includes=[]
35 """
36
37
38
39
40 DEFAULT_ERROR_URL = "http://stork-repository.cs.arizona.edu/errorlog/report"
41
42 glo_program = None
43 glo_output = ""
44 glo_orig_stdout = None
45 glo_orig_stdout = None
46 glo_last_stream = None
47 glo_traceback = False
48
49 glo_developer_email = "stork@cs.arizona.edu"
50
51 def send_web_error_report(webPostUrl, dict):
52     try:
53         import urllib, urllib2
54         data = urllib.urlencode(dict)
55         s = urllib2.urlopen(webPostUrl + "?", data)
56         #result = s.readlines()
57         #for line in result:
58         #    print >> sys.stderr, line
59         return True
60     except Exception, e:
61         print >> sys.stderr, "Exception", e, "while trying to post error report"
62         return False
63
64 def error_report(exc_info, program=None, version=None, output="", webPostUrl=None, files=[], listdirs=[], savedir="/tmp", title="STORK ERROR REPORT"):
65    """
66    <Purpose>
67       Collects system information for an error report, saves it to disk,
68       and optionally e-mails it to the desired recipient.
69
70    <Arguments>
71       exc_info:
72               Exception information.  May be obtained by calling
73               sys.exc_info().
74       program:
75               (Default: None)
76               Name of the program being run.  If None, tries to figure it
77               out automatically.
78       version:
79               (Default: None)
80               Version of program being run.
81       output:
82               (Default: "")
83               Program output as a string.  Although there is a default
84               option, not providing output really hinders debugging.
85       email:
86               (Default: None)
87               List of email address to send report to, or None to not
88               mail a report.
89       files:
90               (Default: [])
91               List of filenames (including absolute paths for each) whose
92               contents should be included in the error report.
93       listdirs:
94               (Default: [])
95               List of directories whose files and subdirectories should be
96               recursively listed.
97       savedir:
98               (Default: "/tmp")
99               Directory in which to save the error report, or None to not
100               save the report to disk.
101       title:
102               (Default: "STORK ERROR REPORT")
103               Title of the error report.
104
105    <Author>
106       Jeffry Johnston
107
108    <Exceptions>
109       None.
110
111    <Side Effects>
112       None.
113
114    <Returns>
115       A 3-tuple: mailed, saved, report.
116       Where mailed is True if an error report was mailed, False otherwise.
117             saved is the name of the saved error report, otherwise None.
118             report is the text of the generated error report.
119    """
120    # Note: I intentionally do not check types up front.  This routine must
121    #       continue no matter what.  It does the best it can in the face
122    #       of errors.
123
124    # Note: If you must add code to this function please follow these rules:
125    #       1) Wrap the new code in try/except blocks.  No exceptions.
126    #          If a variable is assigned in the try: block, it must also be
127    #          assigned in the except: block.
128    #       2) Add a comment so we know what the new code does.
129    #       3) DO NOT assume that the new code will always work.  This
130    #          includes when building the report string.  Assume the
131    #          opposite, in fact.  Pretend that each line of your new code
132    #          fails.  Then handle the failures appropriately.
133    #       4) Do not put code above the time code.  We need an accurate
134    #          reading of the current time.
135    #       5) Check your indentation.  Use exactly 3 spaces for
136    #          indentation.  Do not use tabs.
137    #       6) Be extra careful.  Check your typing, double check the code.
138    #          Test that it loads, run it.  Make sure you didn't break it.
139    #          If this code generates an exception then error reporting
140    #          fails and the user will get an ugly error.  Or worse...
141
142    # Note: All except: blocks in this function are generic, because we do
143    #       not want this code to fail under any circumstances.
144
145    # collect information...
146
147    # date and time, DO THIS FIRST SO WE GET A MORE ACCURATE TIME READING
148    try:
149       import time
150    except: # leave this as general exception handler
151       pass
152    try:
153       now = time.localtime()
154    except: # leave this as general exception handler
155       now = (1980, 1, 1, 0, 0, 0, 0, 1, 0)
156    # !!!!!!! DON'T PUT *ANY* NEW CODE ABOVE THIS LINE !!!!!!!!
157    try:
158       time_str = time.strftime("%a, %d %b %Y %H:%M:%S %Z", now)
159    except: # leave this as general exception handler
160       time_str = ""
161
162    try:
163       tb = exc_info[2]
164       while (tb != None):
165           exc_lineno = str(tb.tb_lineno)
166           tb = tb.tb_next
167    except:
168       exc_lineno = "unknown line number"
169
170    try:
171       tb = exc_info[2]
172       while (tb != None):
173           exc_filename = str(tb.tb_frame.f_code.co_filename)
174           tb = tb.tb_next
175    except:
176       exc_filename = "unknown exception filename"
177
178    # exception name
179    try:
180       except_name = str(exc_info[1].__class__.__name__)
181    except: # leave this as general exception handler
182       except_name = ""
183
184    # stack trace
185    try:
186       trace = "".join(traceback.format_exception(exc_info[0], exc_info[1], exc_info[2]))
187    except: # leave this as general exception handler
188       trace = "Unable to process stack trace.\n"
189
190    # load averages
191    try:
192       import os
193    except: # leave this as general exception handler
194       pass
195    try:
196       load_avg = str(os.getloadavg())
197    except: # leave this as general exception handler
198       load_avg = "Unable to determine load averages."
199
200    # process listing
201    try:
202       tmp = os.popen("ps -wweF")
203       ps = "".join(tmp.readlines())
204       tmp.close()
205    except: # leave this as general exception handler
206       ps = "Unable to retrieve process listing.\n"
207
208    # memory info
209    try:
210       tmp_in, tmp_out, tmp_err = os.popen3("free -t")
211       tmp_in.close()
212       tmp_str = "".join(tmp_err.readlines()).strip()
213       tmp_err.close()
214       if tmp_str != "":
215          memory = "Unable to retrieve memory information.\n"
216       else:
217          memory = "".join(tmp_out.readlines())
218       tmp_out.close()
219    except: # leave this as general exception handler
220       memory = "Unable to retrieve memory information.\n"
221
222    # disk info
223    try:
224       tmp_in, tmp_out, tmp_err = os.popen3("df -ah")
225       tmp_in.close()
226       tmp_str = "".join(tmp_err.readlines()).strip()
227       tmp_err.close()
228       if tmp_str != "":
229          disk = "Unable to retrieve disk information.\n"
230       else:
231          disk = "".join(tmp_out.readlines())
232       tmp_out.close()
233    except: # leave this as general exception handler
234       disk = "Unable to retrieve disk information.\n"
235
236    # runtime environment
237    try:
238       uname = " ".join(os.uname())
239    except: # leave this as general exception handler
240       uname = ""
241    try:
242       cwd = os.getcwd()
243    except: # leave this as general exception handler
244       cwd = ""
245    try:
246       path = os.getenv("PATH", "Unable to determine path.")
247    except: # leave this as general exception handler
248       path = "Unable to determine path."
249    try:
250       username = arizonageneral.getusername()
251    except: # leave this as general exception handler
252       username = "Unable to determine username."
253    try:
254       hostname = arizonageneral.gethostname()
255       if hostname == None:
256          hostname = "Unable to determine hostname."
257    except: # leave this as general exception handler
258       hostname = "Unable to determine hostname."
259    try:
260       cmdline = " ".join(sys.argv[0:])
261    except: # leave this as general exception handler
262       cmdline = "Unable to determine command line."
263
264    # file contents
265    file_contents = ""
266    try:
267       iter(files)
268    except: # leave this as general exception handler
269       files = []
270    for filename in files:
271       if not isinstance(filename, str):
272          continue
273       try:
274          file_contents += "\n" + filename + "\n" + ("-" * len(filename)) + "\n"
275          tmp = file(filename, "r")
276          file_contents += "".join(tmp.readlines()) + "\n"
277          tmp.close()
278       except: # leave this as general exception handler
279          file_contents += "Unable to read file.\n\n"
280
281    # directory listings
282    directory_list = ""
283    try:
284       iter(listdirs)
285    except: # leave this as general exception handler
286       listdirs = []
287    for directory in listdirs:
288       if not isinstance(directory, str):
289          continue
290       try:
291          tmp_out = os.popen("ls -AlFRq " + directory + " 2>&1")
292          directory_list += "".join(tmp_out.readlines()) + "\n"
293          tmp_out.close()
294       except: # leave this as general exception handler
295          directory_list += "Unable to read directory tree: " + directory + "\n\n"
296
297    # output
298    if not isinstance(output, str):
299       output = "No output information given.\n"
300    else:
301       # remove duplicate strings in the output
302       try:
303          output = uniq_string(output)
304       except: # leave this as general exception handler
305          pass
306
307    if not output.endswith("\n"):
308       output += "\n"
309
310    # title
311    if not isinstance(title, str):
312       title = "STORK ERROR REPORT"
313
314    # program
315    if not isinstance(program, str):
316       try:
317          # try to determine the name of the program
318          program = os.path.basename(sys.argv[0])
319          if program == "":
320             # running from the python shell
321             program = "Python Interpreter Interface"
322       except: # leave this as general exception handler
323          program = "Unknown"
324
325    # version
326    if version == None:
327       version = "Unknown version"
328
329    try:
330        subject = title + ": " + hostname + ": " + except_name
331    except: # leave this as general exception handler
332        subject = "STORK ERROR REPORT: failed to build subject string"
333
334    # build error report
335    try:
336       report = subject + "\n\n\n" + \
337                "Summary\n=======\n" + program + ", " + version + ", " + hostname + "\n\n\n" + \
338                "Stack trace\n===========\n" + trace + "\n" + \
339                "Time\n====\n" + time_str + "\n\n" + \
340                "Program reporting the error\n===========================\n" + program + "\n\n" + \
341                "Command line\n============\n" + cmdline + "\n\n" + \
342                "Program output\n==============\n" + output + "\n" + \
343                "Current directory\n=================\n" + cwd + "\n\n" + \
344                "System\n======\n[" + username + "@" + hostname + "] " + uname + "\n\n" + \
345                "Path\n====\n" + path + "\n\n" + \
346                "Memory usage\n============\n" + memory + "\n" + \
347                "Load averages (1, 5, 15 min)\n============================\n" + load_avg + "\n\n" + \
348                "Process listing\n===============\n" + ps + "\n" + \
349                "Disk usage\n==========\n" + disk + "\n"
350       if len(directory_list) > 0:
351          report += "Directory listings\n==================\n" + directory_list
352       if len(file_contents) > 0:
353          report += "File contents\n=============\n" + file_contents
354    except: # leave this as general exception handler
355       report = "Could not assemble error report string."
356
357    # write to a file
358    saved = None
359    if savedir != None:
360       if not isinstance(savedir, str):
361          savedir = "/tmp"
362       try:
363          os.makedirs(savedir)
364       except: # leave this as general exception handler
365          pass
366       try:
367          filename = os.path.join(savedir, program + ".error." + time.strftime("%d%b%Y_%H.%M.%S_%Z", now))
368          outfile = file(filename, "w")
369          outfile.write(report)
370          outfile.close()
371          saved = filename
372       except: # leave this as general exception handler
373          pass
374
375    # send mail
376    mailed = False
377    if webPostUrl != None:
378        try:
379             dict = {"program": program,
380                     "version": version,
381                     "hostname": hostname,
382                     "trace": trace,
383                     "time": time_str,
384                     "cmdline": cmdline,
385                     "username": username,
386                     "uname": uname,
387                     "savefilename": saved,
388                     "lineno": exc_lineno,
389                     "filename": exc_filename,
390                     "exception": except_name}
391             mailed = send_web_error_report(webPostUrl, dict)
392        except:
393             pass
394
395    # return results
396    return mailed, saved, report
397
398
399
400 class copy_out:
401    def __init__(self):
402       pass
403
404    def flush(self):
405       global glo_output
406       glo_orig_stdout.flush()
407       if not glo_output.endswith("\n"):
408          glo_output += "\n"
409
410    def write(self, s):
411       global glo_output
412       global glo_last_stream
413       if s.endswith("\n"):
414          print >> glo_orig_stdout, s[0:len(s) - 1]
415          glo_orig_stdout.flush()
416       else:
417          glo_orig_stdout.write(s)
418          glo_orig_stdout.flush()
419       if glo_output.endswith("\n") or glo_last_stream != 1:
420          glo_output += "[out]"
421          glo_last_stream = 1
422       glo_output += s          
423
424    # Jeremy 3/7/07:
425    # added this stub so it can be
426    # called in the case someone
427    # tries to close stdout inorder
428    # to make a daemon
429    def close(self):
430       pass 
431
432
433
434
435
436 class copy_err:
437    def __init__(self):
438       pass
439    
440    def flush(self):
441       global glo_output
442       glo_orig_stderr.flush()
443       if not glo_output.endswith("\n"):
444          glo_output += "\n"
445
446    def write(self, s):
447       global glo_output
448       global glo_last_stream
449       global glo_traceback
450       if s.startswith("Traceback (most recent call last)") and not arizonaconfig.get_option("debug"):
451          glo_traceback = True
452       # if traceback is set: save the output, but do not display it   
453       if not glo_traceback:   
454          if s.endswith("\n"):
455             print >> glo_orig_stderr, s[0:len(s) - 1]
456             glo_orig_stderr.flush()
457          else:
458             glo_orig_stderr.write(s)
459             glo_orig_stderr.flush()
460       if glo_output.endswith("\n") or glo_last_stream != 2:
461          glo_output += "[err]"
462          glo_last_stream = 2
463       glo_output += s          
464
465    # Jeremy 3/7/07:   
466    # added this stub so it can be
467    # called in the case someone
468    # tries to close stderr inorder
469    # to make a daemon
470    def close(self):
471       pass
472
473
474
475
476
477 def init_error_reporting(program):
478    """
479    <Purpose>
480       Sets up a handler to report errors to the Stork team
481
482    <Arguments>
483       program:
484               Name of the program being run.
485
486    <Exceptions>
487       None.
488    
489    <Side Effects>
490       Inits glo_program, glo_output, glo_last_stream
491       Sets new stdout/err, originals in glo_orig_stdout, glo_orig_stderr
492
493    <Returns>
494       None.
495    """
496    global glo_program
497    global glo_output
498    global glo_orig_stdout
499    global glo_orig_stderr
500    global glo_last_stream
501    global glo_traceback
502
503    glo_program = program
504    glo_output = ""
505    glo_last_stream = None
506    glo_traceback = False
507
508    # create new stdout and stderr to capture a copy of all output
509    glo_orig_stdout = sys.stdout
510    glo_orig_stderr = sys.stderr
511    sys.stdout = copy_out()
512    sys.stderr = copy_err()
513
514    sys.excepthook = __error_report_hook
515
516
517
518
519 def __error_report_hook(exc, value, trace):
520    """
521    <Purpose>
522       Catches an exception and passes it to the error reporting routine.
523
524    <Exceptions>
525       None.
526
527    <Side Effects>
528       None.
529
530    <Returns>
531       None.
532    """
533    exc_info = [exc, value, trace]
534
535    try:
536       debug = arizonaconfig.get_option("debug")
537    except:
538       debug = False
539
540    # if --debug is used, don't report, just print a normal trace and exit
541    if debug:
542       traceback.print_exception(exc, value, trace)
543       sys.exit(1)
544
545    # determine config file name
546    try:
547       configfile = arizonaconfig.get_option("configfile")
548    except:
549       configfile = "/usr/local/stork/etc/stork.conf"
550
551    # build list of e-mail recipients
552    try:
553       if arizonaconfig.get_option("senderrorreport"):
554          webPostUrl = DEFAULT_ERROR_URL
555       else:
556          webPostUrl = None
557    except:
558       webPostUrl = None
559
560    # restore original output streams
561    try:
562       sys.stdout.flush()
563       sys.stdout = glo_orig_stdout
564       sys.stdout.flush()
565    except:
566       pass
567    try:
568       sys.stderr.flush()
569       sys.stderr = glo_orig_stderr
570       sys.stderr.flush()
571    except:
572       pass
573
574    # generate the error report
575    mailed, saved, report = \
576       error_report(exc_info,
577                                   glo_program,
578                                   arizonaconfig.version_string,
579                                   glo_output,
580                                   webPostUrl,
581                                   [configfile],
582                                   ["/usr/local/stork/bin", "/usr/local/stork/etc"])
583    
584    # display error report status
585    print >> sys.stderr, ""
586    print >> sys.stderr, ""
587    if mailed:
588       # report was mailed
589       print >> sys.stderr, "This program has encountered an internal error."
590       print >> sys.stderr, "An error report has been generated and automatically sent to the developers."
591       if saved != None:
592          print >> sys.stderr, "A copy of the error report has been saved to:\n\t" + saved
593       print >> sys.stderr, "If you wish to inquire further, please mail " + glo_developer_email
594    elif saved != None:
595       # requested that the report not be mailed
596       print >> sys.stderr, "This program has encountered an internal error."
597       if webPostUrl == None:
598          print >> sys.stderr, "An error report has been generated, but it was not sent to the developers."
599       else:
600          print >> sys.stderr, "An error report has been generated, but it could not be automatically sent to the developers."
601       print >> sys.stderr, "A copy of the error report has been saved to:\n\t" + saved
602    else:
603       # something is very wrong.. print to screen
604       print >> sys.stderr, report
605       print >> sys.stderr, "This program has encountered an internal error."
606       if webPostURl == None:
607          print >> sys.stderr, "An error report has been generated, but it was not sent to the developers, per your request."
608       else:
609          print >> sys.stderr, "An error report has been generated, but it could not be automatically sent to the developers."
610