Fix the crappy error handling in utils.run() - it doesn't work
at all right now, because no exceptions are thrown for a timeout

Move CmdResult class to utils.py - it has no business being under
hosts/ ... nothing to do with it.

Signed-off-by: Martin Bligh <mbligh@google.com>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@995 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/server/hosts/__init__.py b/server/hosts/__init__.py
index f849375..f3c250b 100644
--- a/server/hosts/__init__.py
+++ b/server/hosts/__init__.py
@@ -25,6 +25,3 @@
 
 # bootloader classes
 from bootloader import Bootloader
-
-# command result class
-from base_classes import CmdResult
diff --git a/server/hosts/base_classes.py b/server/hosts/base_classes.py
index 9625224..4669dee 100644
--- a/server/hosts/base_classes.py
+++ b/server/hosts/base_classes.py
@@ -10,7 +10,6 @@
 
 	Host: a machine on which you can run programs
 	RemoteHost: a remote machine on which you can run programs
-	CmdResult: contain the results of a Host.run() command execution
 """
 
 __author__ = """
@@ -19,11 +18,8 @@
 stutsman@google.com (Ryan Stutsman)
 """
 
-
 import time
-import textwrap
-import bootloader
-import utils
+import bootloader, utils
 
 class Host(object):
 	"""
@@ -127,52 +123,3 @@
 
 	def __init__(self):
 		super(RemoteHost, self).__init__()
-
-
-class CmdResult(object):
-	"""
-	Command execution result.
-
-	Modified from the original Autoserv code, local_cmd.py:
-		Copyright jonmayer@google.com (Jonathan Mayer),
-		mbligh@google.com   (Martin J. Bligh)
-		Released under the GPL, v2
-
-	command: String containing the command line itself
-	exit_status: Integer exit code of the process
-	stdout: String containing stdout of the process
-	stderr: String containing stderr of the process
-	duration: Elapsed wall clock time running the process
-	aborted: Signal that caused the command to terminate (0 if none)
-	"""
-
-	def __init__(self):
-		super(CmdResult, self).__init__()
-		self.command = ""
-		self.exit_status = None
-		self.stdout = ""
-		self.stderr = ""
-		self.duration = 0
-		self.aborted= False
-
-
-	def __repr__(self):
-		wrapper= textwrap.TextWrapper(width=78, 
-			initial_indent="\n    ", subsequent_indent="    ")
-		
-		stdout= self.stdout.rstrip(" \n")
-		if stdout:
-			stdout= "\nStdout:\n%s" % (stdout,)
-		
-		stderr= self.stderr.rstrip(" \n")
-		if stderr:
-			stderr= "\nStderr:\n%s" % (stderr,)
-		
-		return ("* Command: %s\n"
-			"Exit status: %s\n"
-			"Duration: %s\n"
-			"Aborted: %s"
-			"%s"
-			"%s"
-			% (wrapper.fill(self.command), self.exit_status, 
-			self.duration, self.aborted, stdout, stderr))
diff --git a/server/hosts/ssh_host.py b/server/hosts/ssh_host.py
index 2edc6b3..4b18504 100644
--- a/server/hosts/ssh_host.py
+++ b/server/hosts/ssh_host.py
@@ -337,7 +337,7 @@
 		#~ print "running %s" % (command,)
 		result= utils.run(r'%s "%s"' % (self.ssh_command(),
 						utils.sh_escape(command)),
-				  timeout, ignore_status)
+						timeout, ignore_status)
 		return result
 
 
diff --git a/server/utils.py b/server/utils.py
index ba350ef..7de279f 100644
--- a/server/utils.py
+++ b/server/utils.py
@@ -13,8 +13,9 @@
 """
 
 import atexit, os, select, shutil, signal, StringIO, subprocess, tempfile
-import time, types, urllib, re, sys
+import time, types, urllib, re, sys, textwrap
 import hosts, errors
+from error import *
 
 # A dictionary of pid and a list of tmpdirs for that pid
 __tmp_dirs = {}
@@ -126,6 +127,24 @@
 			return tmpfile
 
 
+def __nuke_subprocess(subproc):
+       # the process has not terminated within timeout,
+       # kill it via an escalating series of signals.
+       signal_queue = [signal.SIGTERM, signal.SIGKILL]
+       for sig in signal_queue:
+	       try:
+		       os.kill(subproc.pid, sig)
+	       # The process may have died before we could kill it.
+	       except OSError:
+		       pass
+
+	       for i in range(5):
+		       rc = subproc.poll()
+		       if rc != None:
+			       return
+		       time.sleep(1)
+
+
 def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
 	if use_os_read:
 		data = os.read(pipe.fileno(), 1024)
@@ -161,30 +180,15 @@
 		pid, exit_status_indication = os.waitpid(subproc.pid,
 							 os.WNOHANG)
 		if pid:
-			break
+			return exit_status_indication
 		if timeout:
 			time_left = stop_time - time.time()
 
 	# the process has not terminated within timeout,
 	# kill it via an escalating series of signals.
 	if not pid:
-		signal_queue = [signal.SIGTERM, signal.SIGKILL]
-		for sig in signal_queue:
-			try:
-				os.kill(subproc.pid, sig)
-			# handle race condition in which
-			# process died before we could kill it.
-			except OSError:
-				pass
-
-			for i in range(5):
-				pid, exit_status_indication = (
-				    os.waitpid(subproc.pid, os.WNOHANG))
-				if pid:
-					return exit_status_indication
-				else:
-						time.sleep(1)
-	return exit_status_indication
+		__nuke_subprocess(subproc)
+	raise CmdError('Command not complete within %s seconds' % timeout)
 
 
 def run(command, timeout=None, ignore_status=False,
@@ -206,17 +210,13 @@
 		stderr_tee: likewise for stderr
 
 	Returns:
-		a hosts.CmdResult object
+		a CmdResult object
 
 	Raises:
 		AutoservRunError: the exit code of the command
 			execution was not 0
-
-	TODO(poirier): Should a timeout raise an exception? Should
-		exceptions be raised at all?
 	"""
-	result = hosts.CmdResult()
-	result.command = command
+	result = CmdResult(command)
 	sp = subprocess.Popen(command, stdout=subprocess.PIPE,
 			      stderr=subprocess.PIPE, close_fds=True,
 			      shell=True, executable="/bin/bash")
@@ -227,17 +227,10 @@
 		# We are holding ends to stdin, stdout pipes
 		# hence we need to be sure to close those fds no mater what
 		start_time = time.time()
-		exit_status_indication = _wait_for_command(
-		    sp, start_time, timeout,
-		    stdout_file, stderr_file,
-		    stdout_tee, stderr_tee)
+		result.exit_status = _wait_for_command(sp, start_time, timeout,
+			      stdout_file, stderr_file, stdout_tee, stderr_tee)
 
 		result.duration = time.time() - start_time
-		result.aborted = exit_status_indication & 127
-		if result.aborted:
-			result.exit_status = None
-		else:
-			result.exit_status = exit_status_indication / 256
 		# don't use os.read now, so we get all the rest of the output
 		_process_output(sp.stdout, stdout_file, stdout_tee,
 				use_os_read=False)
@@ -252,8 +245,7 @@
 	result.stderr = stderr_file.getvalue()
 
 	if not ignore_status and result.exit_status > 0:
-		raise errors.AutoservRunError("command execution error",
-			result)
+		raise errors.AutoservRunError("command execution error", result)
 
 	return result
 
@@ -416,3 +408,44 @@
 				return ret
 		else:
 			return default
+
+
+class CmdResult(object):
+	"""
+	Command execution result.
+
+	command:     String containing the command line itself
+	exit_status: Integer exit code of the process
+	stdout:      String containing stdout of the process
+	stderr:      String containing stderr of the process
+	duration:    Elapsed wall clock time running the process
+	"""
+
+	def __init__(self, command = None):
+		self.command = command
+		self.exit_status = None
+		self.stdout = ""
+		self.stderr = ""
+		self.duration = 0
+
+
+	def __repr__(self):
+		wrapper = textwrap.TextWrapper(width = 78, 
+					       initial_indent="\n    ",
+					       subsequent_indent="    ")
+		
+		stdout = self.stdout.rstrip()
+		if stdout:
+			stdout = "\nstdout:\n%s" % stdout
+		
+		stderr = self.stderr.rstrip()
+		if stderr:
+			stderr = "\nstderr:\n%s" % stderr
+		
+		return ("* Command: %s\n"
+			"Exit status: %s\n"
+			"Duration: %s\n"
+			"%s"
+			"%s"
+			% (wrapper.fill(self.command), self.exit_status, 
+			self.duration, stdout, stderr))