# -*- mode: tcl; indent-tabs-mode: nil -*-
# ----------------------------------------------------------------------
# COMPONENT: task - represents the executable part of a tool
#
# This object is an executable version of a Rappture xml file.
# A tool is a task plus its graphical user interface. Each task
# resides in an installation directory with other tool resources
# (libraries, examples, etc.). Each task is defined by its inputs
# and outputs, and understands the context in which it executes
# (via exec, submit, mx, etc.).
# ======================================================================
# AUTHOR: Michael McLennan, Purdue University
# Copyright (c) 2004-2014 HUBzero Foundation, LLC
#
# See the file "license.terms" for information on usage and
# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# ======================================================================
package require BLT
itcl::class Rappture::Task {
public variable logger ""
public variable jobstats Rappture::Task::MiddlewareTime
public variable resultdir "@default"
constructor {xmlobj installdir args} { # defined below }
destructor { # defined below }
public method installdir {} { return $_installdir }
public method run {args}
public method abort {}
public method reset {}
public method xml {args}
public method save {xmlobj {name ""}}
protected method _mkdir {dir}
protected method _output {data}
protected method _log {args}
private variable _xmlobj "" ;# XML object with inputs/outputs
private variable _origxml "" ;# copy of original XML (for reset)
private variable _lastrun "" ;# name of last run file
private variable _installdir "" ;# installation directory for this tool
private variable _outputcb "" ;# callback for tool output
private common job ;# array var used for blt::bgexec jobs
private common jobnum 0 ;# counter for unique job number
# get global resources for this tool session
public proc resources {{option ""}}
public common _resources
public proc setAppName {name} { set _resources(-appname) $name }
public proc setHubName {name} { set _resources(-hubname) $name }
public proc setHubURL {name} { set _resources(-huburl) $name }
public proc setSession {name} { set _resources(-session) $name }
public proc setJobPrt {name} { set _resources(-jobprotocol) $name }
public proc setResultDir {name} { set _resources(-resultdir) $name }
# default method for -jobstats control
public proc MiddlewareTime {args}
}
# must use this name -- plugs into Rappture::resources::load
proc task_init_resources {} {
Rappture::resources::register \
application_name Rappture::Task::setAppName \
application_id Rappture::Task::setAppId \
hub_name Rappture::Task::setHubName \
hub_url Rappture::Task::setHubURL \
session_token Rappture::Task::setSession \
job_protocol Rappture::Task::setJobPrt \
results_directory Rappture::Task::setResultDir
}
# ----------------------------------------------------------------------
# CONSTRUCTOR
# ----------------------------------------------------------------------
itcl::body Rappture::Task::constructor {xmlobj installdir args} {
if {![Rappture::library isvalid $xmlobj]} {
error "bad value \"$xmlobj\": should be Rappture::Library"
}
set _xmlobj $xmlobj
# stash a copy of the original XML for later "reset" operations
set _origxml [Rappture::LibraryObj ::#auto ""]
$_origxml copy "" from $_xmlobj ""
if {![file exists $installdir]} {
error "directory \"$installdir\" doesn't exist"
}
set _installdir $installdir
eval configure $args
}
# ----------------------------------------------------------------------
# DESTRUCTOR
# ----------------------------------------------------------------------
itcl::body Rappture::Task::destructor {} {
itcl::delete object $_origxml
}
# ----------------------------------------------------------------------
# USAGE: resources ?-option?
#
# Clients use this to query information about the tool.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::resources {{option ""}} {
if {$option == ""} {
return [array get _resources]
}
if {[info exists _resources($option)]} {
return $_resources($option)
}
return ""
}
# ----------------------------------------------------------------------
# USAGE: run ? ...? ?-output ?
#
# This method causes the tool to run. A "driver.xml" file is created
# as the input for the run. That file is fed to the executable
# according to the string, and the job is executed.
#
# Any " " arguments are used to override the current
# settings from the GUI. This is useful, for example, when filling
# in missing simulation results from the analyzer.
#
# If the -output argument is included, then the next arg is a
# callback command for output messages. Any output that comes in
# while the tool is running is sent back to the caller, so the user
# can see progress running the tool.
#
# Returns a list of the form {status result}, where status is an
# integer status code (0=success) and result is the output from the
# simulator. Successful output is something like {0 run1293921.xml},
# where 0=success and run1293921.xml is the name of the file containing
# results.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::run {args} {
global env errorInfo
#
# Make sure that we save the proper application name.
# Actually, the best place to get this information is
# straight from the "installtool" script, but just in
# case we have an older tool, we should insert the
# tool name from the resources config file.
#
if {[info exists _resources(-appname)]
&& $_resources(-appname) ne ""
&& [$_xmlobj get tool.name] eq ""} {
$_xmlobj put tool.name $_resources(-appname)
}
# if there are any args, use them to override parameters
set _outputcb ""
foreach {path val} $args {
if {$path == "-output"} {
set _outputcb $val
} else {
$_xmlobj put $path.current $val
}
}
foreach item {control output error} { set job($item) "" }
# write out the driver.xml file for the tool
set file "driver[pid].xml"
set status [catch {
set fid [open $file w]
puts $fid ""
puts $fid [$_xmlobj xml]
close $fid
} result]
# Set limits for cpu time
set limit [$_xmlobj get tool.limits.cputime]
if { $limit == "unlimited" } {
set limit 43200; # 12 hours
} else {
if { [scan $limit "%d" dum] != 1 } {
set limit 14400; # 4 hours by default
} elseif { $limit > 43200 } {
set limit 43200; # limit to 12 hrs.
} elseif { $limit < 10 } {
set limit 10; # lower bound is 10 seconds.
}
}
Rappture::rlimit set cputime $limit
# execute the tool using the path from the tool description
if {$status == 0} {
set cmd [$_xmlobj get tool.command]
regsub -all @tool $cmd $_installdir cmd
regsub -all @driver $cmd $file cmd
regsub -all {\\} $cmd {\\\\} cmd
set cmd [string trimleft $cmd " "]
if { $cmd == "" } {
puts stderr "cmd is empty"
return [list 1 "Command is empty.\n\nThere is no command specified by\n\n \n \n\nin the tool.xml file."]
}
switch -glob -- [resources -jobprotocol] {
"submit*" {
# if job_protocol is "submit", then use use submit command
set cmd "submit --local $cmd"
}
"mx" {
# metachory submission
set cmd "mx $cmd"
}
"exec" {
# default -- nothing special
}
}
$_xmlobj put tool.execute $cmd
# starting job...
set _lastrun ""
_log run started
Rappture::rusage mark
if {0 == [string compare -nocase -length 5 $cmd "ECHO "] } {
set status 0;
set job(output) [string range $cmd 5 end]
} else {
set status [catch {
set ::Rappture::Task::job(control) ""
eval blt::bgexec \
::Rappture::Task::job(control) \
-keepnewline yes \
-killsignal SIGTERM \
-onoutput [list [itcl::code $this _output]] \
-output ::Rappture::Task::job(output) \
-error ::Rappture::Task::job(error) \
$cmd
} result]
if { $status != 0 } {
# We're here because the exec-ed program failed
set logmesg $result
if { $::Rappture::Task::job(control) ne "" } {
foreach { token pid code mesg } \
$::Rappture::Task::job(control) break
if { $token == "EXITED" } {
# This means that the program exited normally but
# returned a non-zero exitcode. Consider this an
# invalid result from the program. Append the stderr
# from the program to the message.
set logmesg "Program finished: exit code is $code"
set result "$logmesg\n\n$::Rappture::Task::job(error)"
} elseif { $token == "abort" } {
# The user pressed the abort button.
set logmesg "Program terminated by user."
set result "$logmesg\n\n$::Rappture::Task::job(output)"
} else {
# Abnormal termination
set logmesg "Abnormal program termination: $mesg"
set result "$logmesg\n\n$::Rappture::Task::job(output)"
}
}
_log run failed [list $logmesg]
return [list $status $result]
}
}
# ...job is finished
array set times [Rappture::rusage measure]
if {[resources -jobprotocol] ne "submit"} {
set id [$_xmlobj get tool.id]
set vers [$_xmlobj get tool.version.application.revision]
set simulation simulation
if { $id ne "" && $vers ne "" } {
set pid [pid]
set simulation ${pid}_${id}_r${vers}
}
# need to save job info? then invoke the callback
if {[string length $jobstats] > 0} {
uplevel #0 $jobstats [list job [incr jobnum] \
event $simulation start $times(start) \
walltime $times(walltime) cputime $times(cputime) \
status $status]
}
#
# Scan through stderr channel and look for statements that
# represent grid jobs that were executed. The statements
# look like this:
#
# MiddlewareTime: job=1 event=simulation start=3.001094 ...
#
set subjobs 0
while {[regexp -indices {(^|\n)MiddlewareTime:( +[a-z]+=[^ \n]+)+(\n|$)} $job(error) match]} {
foreach {p0 p1} $match break
if {[string index $job(error) $p0] == "\n"} { incr p0 }
catch {unset data}
array set data {
job 1
event simulation
start 0
walltime 0
cputime 0
status 0
}
foreach arg [lrange [string range $job(error) $p0 $p1] 1 end] {
foreach {key val} [split $arg =] break
set data($key) $val
}
set data(job) [expr {$jobnum+$data(job)}]
set data(event) "subsimulation"
set data(start) [expr {$times(start)+$data(start)}]
set details ""
foreach key {job event start walltime cputime status} {
# add required keys in a particular order
lappend details $key $data($key)
unset data($key)
}
foreach key [array names data] {
# add anything else that the client gave -- venue, etc.
lappend details $key $data($key)
}
if {[string length $jobstats] > 0} {
uplevel #0 $jobstats $details
}
incr subjobs
# done -- remove this statement
set job(error) [string replace $job(error) $p0 $p1]
}
incr jobnum $subjobs
}
} else {
set job(error) "$result\n$errorInfo"
}
if {$status == 0} {
file delete -force -- $file
}
# see if the job was aborted
if {[regexp {^KILLED} $job(control)]} {
_log run aborted
return [list 0 "ABORT"]
}
#
# If successful, return the output, which should include
# a reference to the run.xml file containing results.
#
if {$status == 0} {
set result [string trim $job(output)]
if {[regexp {=RAPPTURE-RUN=>([^\n]+)} $result match file]} {
set _lastrun $file
set status [catch {Rappture::library $file} result]
if {$status == 0} {
# add cputime info to run.xml file
$result put output.walltime $times(walltime)
$result put output.cputime $times(cputime)
if {[info exists env(SESSION)]} {
$result put output.session $env(SESSION)
}
} else {
global errorInfo
set result "$result\n$errorInfo"
}
file delete -force -- $file
} else {
set status 1
set result "Can't find result file in output.\nDid you call Rappture
::result in your simulator?"
}
} elseif {$job(output) ne "" || $job(error) ne ""} {
set result [string trim "$job(output)\n$job(error)"]
}
# log final status for the run
if {$status == 0} {
_log run finished
} else {
_log run failed [list $result]
}
return [list $status $result]
}
# ----------------------------------------------------------------------
# USAGE: _mkdir
#
# Used internally to create the in the file system.
# The parent directory is also created, as needed.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::_mkdir {dir} {
set parent [file dirname $dir]
if {$parent ne "." && $parent ne "/"} {
if {![file exists $parent]} {
_mkdir $parent
}
}
file mkdir $dir
}
# ----------------------------------------------------------------------
# USAGE: abort
#
# Clients use this during a "run" to abort the current job.
# Kills the job and forces the "run" method to return.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::abort {} {
_log run abort
set job(control) "abort"
}
# ----------------------------------------------------------------------
# USAGE: reset
#
# Resets all input values to their defaults. Sometimes used just
# before a run to reset to a clean state.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::reset {} {
$_xmlobj copy "" from $_origxml ""
foreach path [Rappture::entities -as path $_xmlobj input] {
if {[$_xmlobj element -as type $path.default] ne ""} {
set defval [$_xmlobj get $path.default]
$_xmlobj put $path.current $defval
}
}
}
# ----------------------------------------------------------------------
# USAGE: xml ? ...?
# USAGE: xml object
#
# Used by clients to manipulate the underlying XML data for this
# tool. The can be any operation supported by a
# Rappture::library object. Clients can also request the XML object
# directly by using the "object" subcommand.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::xml {args} {
if {"object" == $args} {
return $_xmlobj
}
return [eval $_xmlobj $args]
}
# ----------------------------------------------------------------------
# USAGE: save ??
#
# Used by clients to save the contents of an representing
# a run out to the given file. If is not specified, then
# it uses the -resultsdir and other settings to do what Rappture
# would normally do with the output.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::save {xmlobj {filename ""}} {
if {$filename eq ""} {
# if there's a results_directory defined in the resources
# file, then move the run.xml file there for storage
set rdir ""
if {$resultdir eq "@default"} {
if {[info exists _resources(-resultdir)]} {
set rdir $_resources(-resultdir)
} else {
set rdir "."
}
} elseif {$resultdir ne ""} {
set rdir $resultdir
}
# use the runfile name generated by the last run
if {$_lastrun ne ""} {
set filename [file join $rdir $_lastrun]
} else {
set filename [file join $rdir run.xml]
}
}
# add any last-minute metadata
$xmlobj put output.time [clock format [clock seconds]]
$xmlobj put tool.version.rappture.version $::Rappture::version
$xmlobj put tool.version.rappture.revision $::Rappture::build
if {[info exists ::tcl_platform(user)]} {
$xmlobj put output.user $::tcl_platform(user)
}
# save the output
set rdir [file dirname $filename]
if {![file exists $rdir]} {
_mkdir $rdir
}
set fid [open $filename w]
puts $fid ""
puts $fid [$xmlobj xml]
close $fid
_log output saved in $filename
}
# ----------------------------------------------------------------------
# USAGE: _output
#
# Used internally to send each bit of output coming from the
# tool onto the caller, so the user can see progress.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::_output {data} {
if {[string length $_outputcb] > 0} {
uplevel #0 $_outputcb [list $data]
}
}
# ----------------------------------------------------------------------
# USAGE: _log ...
#
# Used internally to log interesting events during the run. If the
# -logger option is set (to Rappture::Logger::log, or something like
# that), then the arguments to this method are passed along to the
# logger and written out to a log file. Logging is off by default,
# so this method does nothing unless -logger is set.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::_log {args} {
if {[string length $logger] > 0} {
uplevel #0 $logger [list $args]
}
}
# ----------------------------------------------------------------------
# USAGE: MiddlewareTime ...
#
# Used as the default method for reporting job status information.
# Implements the old HUBzero method of reporting job status info to
# stderr, which can then be picked up by the tool session container.
# Most tools use the "submit" command, which talks directly to a
# database to log job information, so this isn't really needed. But
# it doesn't hurt to have this and it can be useful in some cases.
# ----------------------------------------------------------------------
itcl::body Rappture::Task::MiddlewareTime {args} {
set line "MiddlewareTime:"
foreach {key val} $args {
append line " $key=$val"
}
puts stderr $line
}