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. Recognized params::
177
178 code
179 the code to compile; required
180
181 compiler
182 the compiler to invoke; defaults to cc
183
184 language
185 source file language--one of 'c', 'c++', 'fortran', or 'java';
186 defaults to 'c'.
187
188 out_switch
189 the switch to use to specify the compiler output file; default '-o '
190
191 switches
192 additional switches to pass to the compiler; defaults to ''
193
194 timeout
195 max seconds compilation/execution may take; returns a non-zero exit
196 status and any partial program output on time-out
197 """
198 code = params['code']
199 compiler = 'cc'
200 if params.has_key('compiler'):
201 compiler = params['compiler']
202 lang = 'c'
203 if params.has_key('language'):
204 lang = params['language']
205 extension = 'c'
206 if lang == 'c++':
207 extension = 'C'
208 elif lang == 'fortran':
209 extension = 'f'
210 elif lang == 'java':
211 extension = 'java'
212 prefix = 'src' + str(os.getpid())
213 if lang == 'java':
214 code = re.sub(r'class\s+\w+', 'class ' + prefix, code)
215 if os.environ['CLASSPATH'] != None:
216 os.environ['CLASSPATH'] += ':'
217 os.environ['CLASSPATH'] += '.'
218 path = prefix + '.' + extension
219 output = open(path, 'w')
220 if not output:
221 return None
222 output.write(code + '\n')
223 output.close()
224 out = '-o '
225 if params.has_key('out_switch'):
226 out = params['out_switch']
227 switches = ''
228 if params.has_key('switches'):
229 switches = params['switches']
230 timeout = None
231 if params.has_key('timeout'):
232 timeout = params['timeout']
233 cmd = None
234 if lang == 'java':
235 cmd = '('+compiler+' '+path+' '+switches+' && java '+prefix+')'
236 else:
237 cmd='('+compiler+' '+path+' '+out+prefix+' '+switches+' && ./'+prefix+')'
238 oldLd = None
239 if os.environ.has_key('LD_LIBRARY_PATH'):
240 oldLd = os.environ['LD_LIBRARY_PATH']
241 if re.search(r'-L\s*\S+', switches):
242 paths = re.findall(r'-L\s*(\S+)', switches)
243 os.environ['LD_LIBRARY_PATH'] = string.join(paths, ':')
244 if oldLd != None:
245 os.environ['LD_LIBRARY_PATH'] += ':' + oldLd
246 (status, output) = self.loggedCommandStatusOutput(cmd, timeout)
247 if oldLd != None:
248 os.environ['LD_LIBRARY_PATH'] = oldLd
249 elif os.environ.has_key('LD_LIBRARY_PATH'):
250 del os.environ['LD_LIBRARY_PATH']
251 self.loggedCommandOutput('/bin/rm -f ' + prefix + '*')
252 return (status, output)
253
255 """A convenience; calls setResult(0, msg) and printReport() before exiting
256 the reporter.
257 """
258 self.setResult(0, msg)
259 self.printReport()
260 sys.exit(0)
261
263 """Returns the body of the report."""
264 return self.body
265
267 """Returns the completion indicator of the reporter."""
268 return self.completed
269
271 """Returns the initial working directory of the reporter."""
272 return self.cwd
273
275 """Returns the initial working directory of the reporter."""
276 return self.description
277
279 """Returns the failure message of the reporter."""
280 return self.fail_message
281
283 """Returns the name that identifies this reporter."""
284 return self.name
285
287 """Returns the url which describes the reporter in more detail."""
288 return self.url
289
291 """Returns the version of the reporter."""
292 return self.version
293
294 - def log(self, type, *msgs):
295 """Appends each element of msgs to the list of type log messages stored
296 in the reporter. type must be one of 'debug', 'error', 'info', 'system',
297 or 'warn'."""
298 if not re.search(self.log_pat, type):
299 return
300 for msg in msgs:
301 if self.argValue('verbose') == '0':
302 sys.stderr.write(type + ': ' + msg + '\n')
303 self.log_entries.append({
304 'type' : type, 'time' : time.time(), 'msg' : msg
305 })
306
308 """A convenience; appends cmd to the 'system'-type log messages stored in
309 the reporter, then runs cmd and returns its combined stderr/stdout.
310 If timeout is specified and the command doesn't complete within timeout
311 seconds, aborts the execution of cmd and returns any partial output.
312 """
313 (status, output) = self.loggedCommandStatusOutput(cmd, timeout)
314 return output
315
317 """A convenience; appends cmd to the 'system'-type log messages stored in
318 the reporter, then runs cmd and returns a tuple that contains its exit
319 code and combined stderr/stdout. If timeout is specified and the
320 command doesn't complete within timeout seconds, aborts the execution
321 of cmd and returns a non-zero exit code and any partial output.
322 """
323 self.log('system', cmd)
324 if timeout == None:
325 (status, output) = commands.getstatusoutput(cmd)
326 return (status, output + "\n")
327
328
329 (readfd, writefd) = os.pipe();
330 childPid = os.fork()
331 if childPid == 0:
332 os.close(readfd)
333 os.dup2(writefd, 1)
334 os.dup2(writefd, 2)
335 os.setpgrp()
336 os.execl('/bin/sh', '/bin/sh', '-c', cmd)
337 os.exit(1)
338 os.close(writefd)
339 readFile = os.fdopen(readfd, 'r')
340 timedOut = False
341
342 oldHandler = signal.signal(signal.SIGALRM, self._timeoutException)
343 output = '';
344 signal.alarm(int(timeout))
345 try:
346 line = readFile.readline()
347 while line:
348 output += line
349 line = readFile.readline()
350 except SystemExit:
351 timedOut = True
352 signal.alarm(0)
353 if timedOut:
354 os.killpg(childPid, 9)
355 (childPid, status) = os.waitpid(childPid, 0)
356 status = os.WEXITSTATUS(status)
357 if timedOut:
358 status = 1
359 signal.signal(signal.SIGALRM, oldHandler)
360 readFile.close()
361 return (status, output)
362
364 """A convenience; prints report(verbose) to stdout."""
365 print self.report(verbose) + "\n"
366
368 """Processes argv which is a list of command-line arguments of the form
369 -name=value
370
371 The following options are predefined::
372
373 help
374 yes
375 Prints help information describing the reporter inputs, then
376 forces the reporter to exit. If the verbose level is 0, the
377 output will be text; otherwise, it will be Inca Report XML.
378 no (default)
379 Normal reporter execution.
380
381 log
382 0 (default)
383 log no messages
384 1
385 log error messages
386 2
387 log error and warning messages
388 3
389 log error, warning, and system messages
390 4
391 log error, warning, system, and info messages
392 5
393 log error, warning, system, info, and debug messages
394 debug
395 log only debug messages
396 error
397 log only error messages
398 info
399 log only info messages
400 system
401 log only system messages
402 warn
403 log only warning messages
404
405 verbose
406 0
407 print will only produce "completed" or "failed".
408 1 (default)
409 print will produce Inca Report XML.
410 2
411 print will produce Inca Report XML that includes help information.
412
413 version
414 yes
415 Prints the reporter version number and exits.
416 no (default)
417 Normal reporter execution.
418 """
419 if len(argv) == 1:
420
421
422 argv = argv[0].split('&')
423 elif len(argv) == 0 and os.environ.has_key('QUERY_STRING'):
424
425 argv = os.environ['QUERY_STRING'].split('&')
426
427 argValues = []
428 badArg = None
429 missing = []
430 patterns = []
431
432 for arg in argv:
433 pieces = arg.split('=')
434 name = re.sub('^--?', '', pieces[0])
435 if len(pieces) == 1:
436 value = 'yes'
437 else:
438 value = pieces[1]
439 if not self.args.has_key(name):
440 badArg = "unknown argument '" + name + "'"
441 elif not re.search(self.args[name]['pat'], value):
442 badArg = "'" + value + "' is not a valid value for -" + name
443 argValues.append(name + '=' + value)
444 self.argv = argValues
445 if badArg != None:
446 self.failPrintAndExit(badArg)
447
448 if self.argValue('help') != 'no':
449 if self.argValue('verbose') == '0':
450 description = self.getDescription()
451 version = self.getVersion()
452 if version == None:
453 version = "No version"
454 url = self.getUrl()
455 if url == None:
456 url = "No URL"
457 text = ''
458 usage = os.path.basename(sys.argv[0])
459 args = self.args.keys()
460 args.sort()
461 for arg in args:
462 argDefault = self.args[arg]['default']
463 argDescription = self.args[arg]['description']
464 text += ' -' + arg + '\n'
465 if argDescription != None:
466 text += '\t' + argDescription + '\n'
467 usage += ' -' + arg
468 if argDefault != None:
469 usage += '=' + str(argDefault)
470 print "NAME:\n " + self.getName() + "\n" + \
471 "VERSION:\n " + str(version) + "\n" + \
472 "URL:\n " + url + "\n" + \
473 "SYNOPSIS:\n " + usage + "\n" + text + "\n"
474 else:
475 print self._reportXml(self._helpXml()) + "\n"
476 sys.exit(0)
477 if self.argValue('version') != 'no':
478 print self.getName() + ' ' + str(self.getVersion()) + "\n"
479 sys.exit(0)
480
481 for arg in self.args.keys():
482 if self.argValue(arg) == None:
483 missing.append(arg)
484 if len(missing) == 1:
485 self.failPrintAndExit("Missing required argument '" + missing[0] + "'");
486 elif len(missing) > 0:
487 self.failPrintAndExit \
488 ("Missing required arguments '" + string.join(missing, "', '") + "'");
489
490 for arg in self.argValues('log'):
491 if re.match('[012345]$', arg):
492 allTypes = ['error', 'warn', 'system', 'info', 'debug']
493 arg = string.join(allTypes[0:int(arg)], '|')
494 patterns.append(arg)
495 if len(patterns) > 0:
496 self.log_pat = '^(' + string.join(patterns, '|') + ')$'
497
498 - def report(self, verbose=None):
499 """Returns report text or XML, depending on the value (0, 1, 2) of verbose.
500 Uses the value of the -verbose switch if verbose is None.
501 """
502 completed = self.getCompleted()
503 msg = self.getFailMessage()
504 if verbose == None:
505 verbose = self.argValue('verbose')
506 if verbose == '0':
507 if completed:
508 result = 'completed'
509 else:
510 result = 'failed'
511 if msg != None:
512 result += ': ' + msg
513 else:
514 if completed and self.getBody() == None:
515 self.setBody(self.reportBody())
516 if msg != None:
517 messageXml = self.xmlElement('errorMessage', 1, msg)
518 else:
519 messageXml = None
520 if completed:
521 completed = 'true'
522 else:
523 completed = 'false'
524 completedXml = self.xmlElement('completed', 1, completed)
525 if verbose == '2':
526 helpXml = self._helpXml()
527 else:
528 helpXml = None
529 result = self._reportXml(
530 self.xmlElement('body', 0, self.getBody()),
531 self.xmlElement('exitStatus', 0, completedXml, messageXml),
532 helpXml
533 )
534 return result
535
536 - def reportBody(self):
537 """Constructs and returns the XML contents of the report body. Child
538 classes should override the default implementation, which returns None.
539 """
540 return None
541
542 - def setBody(self, body):
543 """Sets the body of the report to body."""
544 self.body = body
545
547 """Sets the completion indicator of the reporter to completed."""
548 self.completed = completed
549
551 """Sets the initial working directory of the reporter to cwd."""
552 self.cwd = cwd
553
555 """Sets the description of the reporter to description."""
556 self.description = description
557
559 """Sets the failure message of the reporter to msg."""
560 self.fail_message = msg
561
563 """Sets the name that identifies this reporter to name."""
564 self.name = name
565
567 """A convenience; calls setCompleted(completed) and setFailMessage(msg)."""
568 self.setCompleted(completed)
569 self.setFailMessage(msg)
570
572 """Sets the url for the reporter to url."""
573 self.url = url
574
576 """Sets the version of the reporter to version. Recognizes and parses CVS
577 revision strings.
578 """
579 if version == None:
580 return
581 found = re.search('Revision: (.*) ', version)
582 if found:
583 self.version = re.group(1)
584 else:
585 self.version = version
586
588 """A convenience. Adds each element of paths to a list of temporary files
589 that will be deleted automatically when the reporter is destroyed.
590 """
591 self.temp_paths.extend(paths)
592
594 """Returns the XML element name surrounding contents. escape should be
595 true only for leaf elements; in this case, each special XML character
596 (<>&) in contents is replaced by the equivalent XML entity.
597 """
598 innards = ''
599 for content in contents:
600 if content == None:
601 continue
602 content = str(content)
603 if escape:
604 content = re.sub('&', '&', content)
605 content = re.sub('<', '<', content)
606 content = re.sub('>', '>', content)
607 content = re.sub('^<', '\n<', content, 1)
608 innards += content
609 innards = re.sub('(?m)^( *)<', r' \1<', innards)
610 innards = re.sub('>$', '>\n', innards, 1)
611 return '<' + name + '>' + innards + '</' + name + '>'
612
614 """Class destructor."""
615 if len(self.temp_paths) > 0:
616 commands.getoutput(
617 '/bin/rm -fr ' + string.join(self.temp_paths, ' ')
618 )
619
621 """Returns help information formatted as the body of an Inca report."""
622 argsAndDepsXml = []
623 args = self.args.keys()
624 args.sort()
625 for arg in args:
626 info = self.args[arg]
627 defaultXml = info['default']
628 if defaultXml != None:
629 defaultXml = self.xmlElement('default', 1, defaultXml)
630 description = info['description']
631 if description == None:
632 description = ''
633 argsAndDepsXml.append(self.xmlElement('argDescription', 0,
634 self.xmlElement('ID', 1, arg),
635 self.xmlElement('accepted', 1, info['pat']),
636 self.xmlElement('description', 1, description),
637 defaultXml
638 ))
639 for dep in self.dependencies:
640 argsAndDepsXml.append(
641 self.xmlElement('dependency', 0, self.xmlElement('ID', 1, dep))
642 )
643 return self.xmlElement('help', 0,
644 self.xmlElement('ID', 1, 'help'),
645 self.xmlElement('name', 1, self.getName()),
646 self.xmlElement('version', 1, str(self.getVersion())),
647 self.xmlElement('description', 1, self.getDescription()),
648 self.xmlElement('url', 1, self.getUrl()), \
649 *argsAndDepsXml
650 )
651
653 """Returns the UTC time for the time() return value when in ISO 8601
654 format: CCMM-MM-DDTHH:MM:SSZ
655 """
656 t = time.gmtime(when)
657 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % \
658 (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
659
661 """Returns XML report beginning with the header and input sections plus any
662 contents specified in the arguments.
663 """
664 argXmls = []
665 logXmls = []
666 hostname = socket.gethostname()
667 if hostname.find('.') < 0:
668 hostname = socket.getfqdn()
669
670 tags = [
671 self.xmlElement('gmt', 1, self._iso8601Time(time.time())),
672 self.xmlElement('hostname', 1, hostname),
673 self.xmlElement('name', 1, self.getName()),
674 self.xmlElement('version', 1, str(self.getVersion())),
675 self.xmlElement('workingDir', 1, self.getCwd()),
676 self.xmlElement('reporterPath', 1, sys.argv[0])
677 ]
678 args = self.args.keys()
679 args.sort()
680 for arg in args:
681 valuesXml = []
682 for value in self.argValues(arg):
683 valuesXml.append(self.xmlElement('value', 1, value))
684 if len(valuesXml) == 0:
685 valuesXml.append(self.xmlElement('value', 1, ''))
686 argXmls.append \
687 (self.xmlElement('arg', 0, self.xmlElement('name', 1, arg), *valuesXml))
688 tags.append(self.xmlElement( 'args', 0, *argXmls))
689 for entry in self.log_entries:
690 logXmls.append(self.xmlElement(entry['type'], 0,
691 self.xmlElement('gmt', 1, self._iso8601Time(entry['time'])),
692 self.xmlElement('message', 1, entry['msg'])
693 ))
694 if len(logXmls) > 0:
695 tags.append(self.xmlElement('log', 0, *logXmls))
696 tags.extend(contents)
697 result = self.xmlElement('rep:report', 0, *tags)
698 result = re.sub('<rep:report', "<rep:report xmlns:rep='http://inca.sdsc.edu/dataModel/report_2.1'", result, 1)
699 return "<?xml version='1.0'?>\n" + result
700
702 """SIGALRM handler that throws an exception."""
703 raise SystemExit(1)
704