1 import commands
2 import os
3 import os.path
4 import re
5 import signal
6 import socket
7 import string
8 import sys
9 import time
10
12 """Reporter - Module for creating Inca reporters
13
14 from inca.Reporter import Reporter
15 reporter = Reporter(
16 name = 'hack.version',
17 version = 0.1,
18 description = 'A really helpful reporter description',
19 url = 'http://url.to.more.reporter.info'
20 )
21
22 This module creates Inca reporters--objects that produce XML that follows
23 the Inca Report schema. The constructor may be called with a number of
24 reporter attributes that can later be set and queried with their
25 corresponding get/set functions (described below). For example,
26
27 reporter = Reporter()
28 reporter.setUrl('http://url.to.more.reporter.info')
29 reporter.setVersion(0.1)
30 """
31
33 """Class constructor that returns a new Reporter object.
34
35 The constructor may be called with any of the following named attributes
36 as parameters.
37
38 body
39 the XML body of the report. See the Inca Report schema for format.
40
41 completed
42 boolean indicating whether or not the reporter has completed
43 generating the information it is intended to produce
44
45 description
46 a verbose description of the reporter
47
48 fail_message
49 a message describing why the reporter failed to complete its task
50
51 name
52 the name that identifies this reporter
53
54 url
55 URL to get more information about the reporter
56
57 version
58 the version of the reporter; defaults to '0'
59 """
60
61 self.args = {}
62 self.argv = []
63 self.body = None
64 self.completed = False
65 self.cwd = os.getcwd()
66 self.description = None
67 self.dependencies = []
68 self.fail_message = None
69 self.log_entries = []
70 self.log_pat = '^$'
71 self.name = os.path.basename(sys.argv[0])
72 self.temp_paths = []
73 self.url = None
74 self.version = '0'
75
76 self.addArg('help', 'display usage information (no|yes)', 'no', 'no|yes')
77 self.addArg(
78 'log', 'log message types included in report', '0',
79 '[012345]|debug|error|info|system|warn'
80 )
81 self.addArg('verbose', 'verbosity level (0|1|2)', '1', '[012]')
82 self.addArg('version', 'show reporter version (no|yes)', 'no', 'no|yes')
83 self.addDependency('inca.Reporter')
84
85 for attr in attributes.keys():
86 if hasattr(self, attr):
87 setattr(self, attr, attributes[attr])
88 else:
89 sys.stderr.write("'" + attr + "' is an invalid attribute\n");
90
91 - def addArg(self, name, description=None, default=None, pattern=None):
92 """Adds a command line argument (invocation syntax -name=value) to the
93 reporter. If supplied, the optional description will be included in the
94 reporter help XML and display. If supplied, default indicates that the
95 argument is optional; the argValue method will return default if the
96 command line does not include a value for the argument. The optional
97 pattern specifies a pattern for recognizing valid argument values; the
98 default is '.*', which means that any text is acceptable for the
99 argument value.
100 """
101 if pattern == None:
102 pattern = '.*'
103 self.args[name] = {
104 'description' : description, 'default' : default, 'pat' : pattern
105 }
106
108 """Add one or more dependencies to the list of modules on which this
109 reporter depends. Dependencies are reported as part of reporter help
110 output to assist reporter repository tools in their retrievals. NOTE:
111 dependencies on the standard Inca reporter library modules are added by
112 the modules themselves, so a reporter only needs to invoke this method
113 to report external dependencies. The Inca Reporter Instance Manager
114 presently only supports dependencies on Inca repository packages.
115 """
116 self.dependencies.extend(dependencies)
117
118 - def argValue(self, name, position=None):
119 """Called after the processArgv method, this returns the value of the
120 position'th instance (starting with 1) of the name command-line
121 argument. Returns the value of the last instance if position is None.
122 Returns None if name is not a recognized argument. Returns the default
123 value for name if it has one and name is included fewer than position
124 times on the command line.
125 """
126 if not self.args.has_key(name):
127 sys.stderr.write(
128 "'" + name + "' is not a valid command line argument name"
129 );
130 return None
131 argv = self.argv[:]
132 if position == None:
133 argv.reverse()
134 position = 1
135 for arg in argv:
136 found = re.match(name + '=(.*)', arg)
137 if found:
138 position -= 1
139 if position < 1:
140 return found.group(1)
141 return self.args[name]['default']
142
144 """Called after the processArgv method, this returns an array of all values
145 specified for the name command-line argument. Returns None if name is
146 not a recognized argument. Returns a single-element array containing
147 the default value for name if it has one and name does not appear on the
148 command line.
149 """
150 if not self.args.has_key(name):
151 sys.stderr.write(
152 "'" + name + "' is not a valid command line argument name"
153 );
154 return None
155 default = self.args[name]['default']
156 result = []
157 for arg in self.argv:
158 found = re.match(name + '=(.*)', arg)
159 if found:
160 result.append(found.group(1))
161 if len(result) == 0 and default != None:
162 result.append(default)
163 return result
164
166 """A convenience; compiles and runs a program, removes the source and exec
167 files, and returns the program's combined stderr/out output. See
168 compiledProgramStatusOutput for a list of recognized params.
169 """
170 (status, output) = self.compiledProgramStatusOutput(**params)
171 return output
172
174 """A convenience; compiles and runs a program, removes the source and exec
175 files, and returns a tuple that contains the program's exit code and
176 its combined stderr/out output.
177
178 Recognized params:
179
180 code
181 the code to compile; required
182
183 compiler
184 the compiler to invoke; defaults to cc
185
186 language
187 source file language--one of 'c', 'c++', 'fortran', or 'java';
188 defaults to 'c'.
189
190 out_switch
191 the switch to use to specify the compiler output file; default '-o '
192
193 switches
194 additional switches to pass to the compiler; defaults to ''
195
196 timeout
197 max seconds compilation/execution may take; returns a non-zero exit
198 status and any partial program output on time-out
199 """
200 code = params['code']
201 compiler = 'cc'
202 if params.has_key('compiler'):
203 compiler = params['compiler']
204 lang = 'c'
205 if params.has_key('language'):
206 lang = params['language']
207 extension = 'c'
208 if lang == 'c++':
209 extension = 'C'
210 elif lang == 'fortran':
211 extension = 'f'
212 elif lang == 'java':
213 extension = 'java'
214 prefix = 'src' + str(os.getpid())
215 if lang == 'java':
216 code = re.sub(r'class\s+\w+', 'class ' + prefix, code)
217 if os.environ['CLASSPATH'] != None:
218 os.environ['CLASSPATH'] += ':'
219 os.environ['CLASSPATH'] += '.'
220 path = prefix + '.' + extension
221 output = open(path, 'w')
222 if not output:
223 return None
224 output.write(code + '\n')
225 output.close()
226 out = '-o '
227 if params.has_key('out_switch'):
228 out = params['out_switch']
229 switches = ''
230 if params.has_key('switches'):
231 switches = params['switches']
232 timeout = None
233 if params.has_key('timeout'):
234 timeout = params['timeout']
235 cmd = None
236 if lang == 'java':
237 cmd = '('+compiler+' '+path+' '+switches+' && java '+prefix+')'
238 else:
239 cmd='('+compiler+' '+path+' '+out+prefix+' '+switches+' && ./'+prefix+')'
240 oldLd = None
241 if os.environ.has_key('LD_LIBRARY_PATH'):
242 oldLd = os.environ['LD_LIBRARY_PATH']
243 if re.search(r'-L\s*\S+', switches):
244 paths = re.findall(r'-L\s*(\S+)', switches)
245 os.environ['LD_LIBRARY_PATH'] = string.join(paths, ':')
246 if oldLd != None:
247 os.environ['LD_LIBRARY_PATH'] += ':' + oldLd
248 (status, output) = self.loggedCommandStatusOutput(cmd, timeout)
249 if oldLd != None:
250 os.environ['LD_LIBRARY_PATH'] = oldLd
251 elif os.environ.has_key('LD_LIBRARY_PATH'):
252 del os.environ['LD_LIBRARY_PATH']
253 self.loggedCommandOutput('/bin/rm -f ' + prefix + '*')
254 return (status, output)
255
257 """A convenience; calls setResult(0, msg) and printReport() before exiting
258 the reporter.
259 """
260 self.setResult(0, msg)
261 self.printReport()
262 sys.exit(0)
263
265 """Returns the body of the report."""
266 return self.body
267
269 """Returns the completion indicator of the reporter."""
270 return self.completed
271
273 """Returns the initial working directory of the reporter."""
274 return self.cwd
275
277 """Returns the initial working directory of the reporter."""
278 return self.description
279
281 """Returns the failure message of the reporter."""
282 return self.fail_message
283
285 """Returns the name that identifies this reporter."""
286 return self.name
287
289 """Returns the url which describes the reporter in more detail."""
290 return self.url
291
293 """Returns the version of the reporter."""
294 return self.version
295
296 - def log(self, type, *msgs):
297 """Appends each element of msgs to the list of type log messages stored
298 in the reporter. type must be one of 'debug', 'error', 'info', 'system',
299 or 'warn'."""
300 if not re.search(self.log_pat, type):
301 return
302 for msg in msgs:
303 if self.argValue('verbose') == '0':
304 sys.stderr.write(type + ': ' + msg + '\n')
305 self.log_entries.append({
306 'type' : type, 'time' : time.time(), 'msg' : msg
307 })
308
310 """A convenience; appends cmd to the 'system'-type log messages stored in
311 the reporter, then runs cmd and returns its combined stderr/stdout.
312 If timeout is specified and the command doesn't complete within timeout
313 seconds, aborts the execution of cmd and returns any partial output.
314 """
315 (status, output) = self.loggedCommandStatusOutput(cmd, timeout)
316 return output
317
319 """A convenience; appends cmd to the 'system'-type log messages stored in
320 the reporter, then runs cmd and returns a tuple that contains its exit
321 code and combined stderr/stdout. If timeout is specified and the
322 command doesn't complete within timeout seconds, aborts the execution
323 of cmd and returns a non-zero exit code and any partial output.
324 """
325 self.log('system', cmd)
326 if timeout == None:
327 (status, output) = commands.getstatusoutput(cmd)
328 return (status, output + "\n")
329
330
331 (readfd, writefd) = os.pipe();
332 childPid = os.fork()
333 if childPid == 0:
334 os.close(readfd)
335 os.dup2(writefd, 1)
336 os.dup2(writefd, 2)
337 os.setpgrp()
338 os.execl('/bin/sh', '/bin/sh', '-c', cmd)
339 os.exit(1)
340 os.close(writefd)
341 readFile = os.fdopen(readfd, 'r')
342 timedOut = False
343
344 oldHandler = signal.signal(signal.SIGALRM, self._timeoutException)
345 output = '';
346 signal.alarm(int(timeout))
347 try:
348 line = readFile.readline()
349 while line:
350 output += line
351 line = readFile.readline()
352 except SystemExit:
353 timedOut = True
354 signal.alarm(0)
355 if timedOut:
356 os.killpg(childPid, 9)
357 (childPid, status) = os.waitpid(childPid, 0)
358 status = os.WEXITSTATUS(status)
359 if timedOut:
360 status = 1
361 signal.signal(signal.SIGALRM, oldHandler)
362 readFile.close()
363 return (status, output)
364
366 """A convenience; prints report(verbose) to stdout."""
367 print self.report(verbose) + "\n"
368
370 """Processes argv which is a list of command-line arguments of the form
371 -name=value
372
373 The following options are predefined:
374
375 help
376 yes
377 Prints help information describing the reporter inputs, then
378 forces the reporter to exit. If the verbose level is 0, the
379 output will be text; otherwise, it will be Inca Report XML.
380 no (default)
381 Normal reporter execution.
382
383 log
384 0 (default)
385 log no messages
386 1
387 log error messages
388 2
389 log error and warning messages
390 3
391 log error, warning, and system messages
392 4
393 log error, warning, system, and info messages
394 5
395 log error, warning, system, info, and debug messages
396 debug
397 log only debug messages
398 error
399 log only error messages
400 info
401 log only info messages
402 system
403 log only system messages
404 warn
405 log only warning messages
406
407 verbose
408 0
409 print will only produce "completed" or "failed".
410 1 (default)
411 print will produce Inca Report XML.
412 2
413 print will produce Inca Report XML that includes help information.
414
415 version
416 yes
417 Prints the reporter version number and exits.
418 no (default)
419 Normal reporter execution.
420 """
421 if len(argv) == 1:
422
423
424 argv = argv[0].split('&')
425 elif len(argv) == 0 and os.environ.has_key('QUERY_STRING'):
426
427 argv = os.environ['QUERY_STRING'].split('&')
428
429 argValues = []
430 badArg = None
431 missing = []
432 patterns = []
433
434 for arg in argv:
435 pieces = arg.split('=')
436 name = re.sub('^--?', '', pieces[0])
437 if len(pieces) == 1:
438 value = 'yes'
439 else:
440 value = pieces[1]
441 if not self.args.has_key(name):
442 badArg = "unknown argument '" + name + "'"
443 elif not re.search(self.args[name]['pat'], value):
444 badArg = "'" + value + "' is not a valid value for -" + name
445 argValues.append(name + '=' + value)
446 self.argv = argValues
447 if badArg != None:
448 self.failPrintAndExit(badArg)
449
450 if self.argValue('help') != 'no':
451 if self.argValue('verbose') == '0':
452 description = self.getDescription()
453 version = self.getVersion()
454 if version == None:
455 version = "No version"
456 url = self.getUrl()
457 if url == None:
458 url = "No URL"
459 text = ''
460 usage = os.path.basename(sys.argv[0])
461 args = self.args.keys()
462 args.sort()
463 for arg in args:
464 argDefault = self.args[arg]['default']
465 argDescription = self.args[arg]['description']
466 text += ' -' + arg + '\n'
467 if argDescription != None:
468 text += '\t' + argDescription + '\n'
469 usage += ' -' + arg
470 if argDefault != None:
471 usage += '=' + str(argDefault)
472 print "NAME:\n " + self.getName() + "\n" + \
473 "VERSION:\n " + str(version) + "\n" + \
474 "URL:\n " + url + "\n" + \
475 "SYNOPSIS:\n " + usage + "\n" + text + "\n"
476 else:
477 print self._reportXml(self._helpXml()) + "\n"
478 sys.exit(0)
479 if self.argValue('version') != 'no':
480 print self.getName() + ' ' + str(self.getVersion()) + "\n"
481 sys.exit(0)
482
483 for arg in self.args.keys():
484 if self.argValue(arg) == None:
485 missing.append(arg)
486 if len(missing) == 1:
487 self.failPrintAndExit("Missing required argument '" + missing[0] + "'");
488 elif len(missing) > 0:
489 self.failPrintAndExit \
490 ("Missing required arguments '" + string.join(missing, "', '") + "'");
491
492 for arg in self.argValues('log'):
493 if re.match('[012345]$', arg):
494 allTypes = ['error', 'warn', 'system', 'info', 'debug']
495 arg = string.join(allTypes[0:int(arg)], '|')
496 patterns.append(arg)
497 if len(patterns) > 0:
498 self.log_pat = '^(' + string.join(patterns, '|') + ')$'
499
500 - def report(self, verbose=None):
501 """Returns report text or XML, depending on the value (0, 1, 2) of verbose.
502 Uses the value of the -verbose switch if verbose is None.
503 """
504 completed = self.getCompleted()
505 msg = self.getFailMessage()
506 if verbose == None:
507 verbose = self.argValue('verbose')
508 if verbose == '0':
509 if completed:
510 result = 'completed'
511 else:
512 result = 'failed'
513 if msg != None:
514 result += ': ' + msg
515 else:
516 if completed and self.getBody() == None:
517 self.setBody(self.reportBody())
518 if msg != None:
519 messageXml = self.xmlElement('errorMessage', 1, msg)
520 else:
521 messageXml = None
522 if completed:
523 completed = 'true'
524 else:
525 completed = 'false'
526 completedXml = self.xmlElement('completed', 1, completed)
527 if verbose == '2':
528 helpXml = self._helpXml()
529 else:
530 helpXml = None
531 result = self._reportXml(
532 self.xmlElement('body', 0, self.getBody()),
533 self.xmlElement('exitStatus', 0, completedXml, messageXml),
534 helpXml
535 )
536 return result
537
538 - def reportBody(self):
539 """Constructs and returns the XML contents of the report body. Child
540 classes should override the default implementation, which returns None.
541 """
542 return None
543
544 - def setBody(self, body):
545 """Sets the body of the report to body."""
546 self.body = body
547
549 """Sets the completion indicator of the reporter to completed."""
550 self.completed = completed
551
553 """Sets the initial working directory of the reporter to cwd."""
554 self.cwd = cwd
555
557 """Sets the description of the reporter to description."""
558 self.description = description
559
561 """Sets the failure message of the reporter to msg."""
562 self.fail_message = msg
563
565 """Sets the name that identifies this reporter to name."""
566 self.name = name
567
569 """A convenience; calls setCompleted(completed) and setFailMessage(msg)."""
570 self.setCompleted(completed)
571 self.setFailMessage(msg)
572
574 """Sets the url for the reporter to url."""
575 self.url = url
576
578 """Sets the version of the reporter to version. Recognizes and parses CVS
579 revision strings.
580 """
581 if version == None:
582 return
583 found = re.search('Revision: (.*) ', version)
584 if found:
585 self.version = re.group(1)
586 else:
587 self.version = version
588
590 """A convenience. Adds each element of paths to a list of temporary files
591 that will be deleted automatically when the reporter is destroyed.
592 """
593 self.temp_paths.extend(paths)
594
596 """Returns the XML element name surrounding contents. escape should be
597 true only for leaf elements; in this case, each special XML character
598 (<>&) in contents is replaced by the equivalent XML entity.
599 """
600 innards = ''
601 for content in contents:
602 if content == None:
603 continue
604 content = str(content)
605 if escape:
606 content = re.sub('&', '&', content)
607 content = re.sub('<', '<', content)
608 content = re.sub('>', '>', content)
609 content = re.sub('^<', '\n<', content, 1)
610 innards += content
611 innards = re.sub('(?m)^( *)<', r' \1<', innards)
612 innards = re.sub('>$', '>\n', innards, 1)
613 return '<' + name + '>' + innards + '</' + name + '>'
614
616 """Class destructor."""
617 if len(self.temp_paths) > 0:
618 commands.getoutput(
619 '/bin/rm -fr ' + string.join(self.temp_paths, ' ')
620 )
621
623 """Returns help information formatted as the body of an Inca report."""
624 argsAndDepsXml = []
625 args = self.args.keys()
626 args.sort()
627 for arg in args:
628 info = self.args[arg]
629 defaultXml = info['default']
630 if defaultXml != None:
631 defaultXml = self.xmlElement('default', 1, defaultXml)
632 description = info['description']
633 if description == None:
634 description = ''
635 argsAndDepsXml.append(self.xmlElement('argDescription', 0,
636 self.xmlElement('ID', 1, arg),
637 self.xmlElement('accepted', 1, info['pat']),
638 self.xmlElement('description', 1, description),
639 defaultXml
640 ))
641 for dep in self.dependencies:
642 argsAndDepsXml.append(
643 self.xmlElement('dependency', 0, self.xmlElement('ID', 1, dep))
644 )
645 return self.xmlElement('help', 0,
646 self.xmlElement('ID', 1, 'help'),
647 self.xmlElement('name', 1, self.getName()),
648 self.xmlElement('version', 1, str(self.getVersion())),
649 self.xmlElement('description', 1, self.getDescription()),
650 self.xmlElement('url', 1, self.getUrl()), \
651 *argsAndDepsXml
652 )
653
655 """Returns the UTC time for the time() return value when in ISO 8601
656 format: CCMM-MM-DDTHH:MM:SSZ
657 """
658 t = time.gmtime(when)
659 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % \
660 (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
661
663 """Returns XML report beginning with the header and input sections plus any
664 contents specified in the arguments.
665 """
666 argXmls = []
667 logXmls = []
668 hostname = socket.gethostname()
669 if hostname.find('.') < 0:
670 hostname = socket.getfqdn()
671
672 tags = [
673 self.xmlElement('gmt', 1, self._iso8601Time(time.time())),
674 self.xmlElement('hostname', 1, hostname),
675 self.xmlElement('name', 1, self.getName()),
676 self.xmlElement('version', 1, str(self.getVersion())),
677 self.xmlElement('workingDir', 1, self.getCwd()),
678 self.xmlElement('reporterPath', 1, sys.argv[0])
679 ]
680 args = self.args.keys()
681 args.sort()
682 for arg in args:
683 valuesXml = []
684 for value in self.argValues(arg):
685 valuesXml.append(self.xmlElement('value', 1, value))
686 if len(valuesXml) == 0:
687 valuesXml.append(self.xmlElement('value', 1, ''))
688 argXmls.append \
689 (self.xmlElement('arg', 0, self.xmlElement('name', 1, arg), *valuesXml))
690 tags.extend(argXmls)
691 for entry in self.log_entries:
692 logXmls.append(self.xmlElement(entry['type'], 0,
693 self.xmlElement('gmt', 1, self._iso8601Time(entry['time'])),
694 self.xmlElement('message', 1, entry['msg'])
695 ))
696 if len(logXmls) > 0:
697 tags.append(self.xmlElement('log', 0, *logXmls))
698 tags.extend(contents)
699 result = self.xmlElement('rep:report', 0, *tags)
700 result = re.sub('<rep:report', "<rep:report xmlns:rep='http://inca.sdsc.edu/dataModel/report_2.1'", result, 1)
701 return "<?xml version='1.0'?>\n" + result
702
704 """SIGALRM handler that throws an exception."""
705 raise SystemExit(1)
706