Better error and subprocess handling

Removed a potential deadlock, added logging for all
raised exceptions, made _to_error more solid and
removed another potential encoding issue when
creating diffs
This commit is contained in:
Gina Häußge 2016-11-25 14:58:09 +01:00
parent 8beedce98f
commit 528e99898f

View file

@ -8,6 +8,8 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import errno
import sys
import traceback
import time
def _log_call(*lines):
_log(lines, prefix=">", stream="call")
@ -61,39 +63,54 @@ def _execute(command, **kwargs):
try:
p = sarge.run(command, **kwargs)
p.wait_events()
except Exception as e:
import traceback
exception_lines = traceback.format_exc()
error = "Error while trying to run command {}: {}\n{}".format(joined_command, str(e), exception_lines)
return None, "", error
while len(p.commands) == 0:
# somewhat ugly... we can't use wait_events because
# the events might not be all set if an exception
# by sarge is triggered within the async process
# thread
time.sleep(0.01)
# by now we should have a command, let's wait for its
# process to have been prepared
p.commands[0].process_ready.wait()
if not p.commands[0].process:
# the process might have been set to None in case of any exception
print("Error while trying to run command {}".format(joined_command), file=sys.stderr)
return None, [], []
except:
print("Error while trying to run command {}".format(joined_command), file=sys.stderr)
traceback.print_exc(file=sys.stderr)
return None, [], []
all_stdout = []
all_stderr = []
try:
while p.returncode is None:
while p.commands[0].poll() is None:
lines = p.stderr.readlines(timeout=0.5)
if lines:
lines = map(lambda x: _to_unicode(x, errors="replace"), lines)
_log_stderr(*lines)
all_stderr += list(lines)
lines = p.stdout.readlines(timeout=0.5)
if lines:
lines = map(lambda x: _to_unicode(x, errors="replace"), lines)
_log_stdout(*lines)
all_stdout += list(lines)
p.commands[0].poll()
finally:
p.close()
lines = p.stderr.readlines()
if lines:
lines = map(lambda x: _to_unicode(x, errors="replace"), lines)
_log_stderr(*lines)
all_stderr += lines
lines = p.stdout.readlines()
if lines:
lines = map(lambda x: _to_unicode(x, errors="replace"), lines)
_log_stdout(*lines)
all_stdout += lines
@ -107,27 +124,31 @@ def _get_git_executables():
return GITS
def _git(args, cwd, verbose=False, git_executable=None):
def _git(args, cwd, git_executable=None):
if git_executable is not None:
commands = [git_executable]
else:
commands = _get_git_executables()
for c in commands:
command = [c] + args
try:
return _execute([c] + args, cwd=cwd)
return _execute(command, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if e.errno == errno.ENOENT:
continue
if verbose:
print("unable to run %s" % args[0])
print(e)
return None, None, None
print("Error while trying to run command {}".format(" ".join(command)), file=sys.stderr)
traceback.print_exc(file=sys.stderr)
return None, [], []
except:
print("Error while trying to run command {}".format(" ".join(command)), file=sys.stderr)
traceback.print_exc(file=sys.stderr)
return None, [], []
else:
if verbose:
print("unable to find command, tried %s" % (commands,))
return None, None, None
print("Unable to find git command, tried {}".format(", ".join(commands)), file=sys.stderr)
return None, [], []
def _python(args, cwd, python_executable, sudo=False):
@ -137,31 +158,40 @@ def _python(args, cwd, python_executable, sudo=False):
try:
return _execute(command, cwd=cwd)
except:
return None, None, None
import traceback
print("Error while trying to run command {}".format(" ".join(command)), file=sys.stderr)
traceback.print_exc(file=sys.stderr)
return None, [], []
def _to_error(*lines):
return u"".join(map(lambda x: _to_unicode(x, errors="replace"), lines))
if len(lines) == 1:
if isinstance(lines[0], (list, tuple)):
lines = lines[0]
elif not isinstance(lines[0], (str, unicode)):
lines = [repr(lines[0]),]
return u"\n".join(map(lambda x: _to_unicode(x, errors="replace"), lines))
def _rescue_changes(git_executable, folder):
print(">>> Running: git diff --shortstat")
returncode, stdout, stderr = _git(["diff", "--shortstat"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git diff\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
if stdout and "".join(stdout).strip():
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git diff\" failed with returncode {}".format(returncode))
if stdout and u"".join(stdout).strip():
# we got changes in the working tree, maybe from the user, so we'll now rescue those into a patch
import time
import os
timestamp = time.strftime("%Y%m%d%H%M")
patch = os.path.join(folder, "%s-preupdate.patch" % timestamp)
patch = os.path.join(folder, "{}-preupdate.patch".format(timestamp))
print(">>> Running: git diff and saving output to %s" % timestamp)
print(">>> Running: git diff and saving output to {}".format(patch))
returncode, stdout, stderr = _git(["diff"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, installation directory was dirty and state could not be persisted as a patch to %s" % patch)
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, installation directory was dirty and state could not be persisted as a patch to {}".format(patch))
with open(patch, "wb") as f:
import codecs
with codecs.open(patch, "w", encoding="utf-8", errors="replace") as f:
for line in stdout:
f.write(line)
@ -174,46 +204,45 @@ def update_source(git_executable, folder, target, force=False, branch=None):
if _rescue_changes(git_executable, folder):
print(">>> Running: git reset --hard")
returncode, stdout, stderr = _git(["reset", "--hard"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git reset --hard\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git reset --hard\" failed with returncode {}".format(returncode))
print(">>> Running: git clean -f -d -e *-preupdate.patch")
returncode, stdout, stderr = _git(["clean", "-f", "-d", "-e", "*-preupdate.patch"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git clean -f\" failed with returcode %d: %s" % (returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git clean -f\" failed with returncode {}".format(returncode))
print(">>> Running: git fetch")
returncode, stdout, stderr = _git(["fetch"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git fetch\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
print(stdout)
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git fetch\" failed with returncode {}".format(returncode))
if branch is not None and branch.strip() != "":
print(">>> Running: git checkout {}".format(branch))
returncode, stdout, stderr = _git(["checkout", branch], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git checkout\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git checkout\" failed with returncode {}".format(returncode))
print(">>> Running: git pull")
returncode, stdout, stderr = _git(["pull"], folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Could not update, \"git pull\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"git pull\" failed with returncode {}".format(returncode))
if force:
reset_command = ["reset", "--hard"]
reset_command += [target]
print(">>> Running: git %s" % " ".join(reset_command))
print(">>> Running: git {}".format(" ".join(reset_command)))
returncode, stdout, stderr = _git(reset_command, folder, git_executable=git_executable)
if returncode != 0:
raise RuntimeError("Error while updating, \"git %s\" failed with returncode %d: %s" % (" ".join(reset_command), returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Error while updating, \"git {}\" failed with returncode {}".format(" ".join(reset_command), returncode))
def install_source(python_executable, folder, user=False, sudo=False):
print(">>> Running: python setup.py clean")
returncode, stdout, stderr = _python(["setup.py", "clean"], folder, python_executable)
if returncode != 0:
print("\"python setup.py clean\" failed with returncode %d: %s" % (returncode, stdout))
if returncode is None or returncode != 0:
print("\"python setup.py clean\" failed with returncode {}".format(returncode))
print("Continuing anyways")
print(">>> Running: python setup.py install")
@ -221,15 +250,14 @@ def install_source(python_executable, folder, user=False, sudo=False):
if user:
args.append("--user")
returncode, stdout, stderr = _python(args, folder, python_executable, sudo=sudo)
if returncode != 0:
raise RuntimeError("Could not update, \"python setup.py install\" failed with returncode %d: %s" % (returncode, _to_error(*stdout)))
if returncode is None or returncode != 0:
raise RuntimeError("Could not update, \"python setup.py install\" failed with returncode {}".format(returncode))
def parse_arguments():
import argparse
boolean_trues = ["true", "yes", "1"]
boolean_falses = ["false", "no", "0"]
parser = argparse.ArgumentParser(prog="update-octoprint.py")