Improved package.py error handling. (#2187)
- Detailed debug information is now handed back to the parent process from builds, for *any* type of exception. - previously this only worked for Spack ProcessErrors, but now it works for any type of error raised in a child. - Spack will print an error message and source code context for build errors by default. - It will print a stack trace when using `spack -d`, even when the error occurred in the child process.
This commit is contained in:
parent
1b7f9e24f4
commit
edfe2297fd
@ -51,10 +51,12 @@
|
|||||||
Skimming this module is a nice way to get acquainted with the types of
|
Skimming this module is a nice way to get acquainted with the types of
|
||||||
calls you can make from within the install() function.
|
calls you can make from within the install() function.
|
||||||
"""
|
"""
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
|
import multiprocessing
|
||||||
|
import traceback
|
||||||
|
import inspect
|
||||||
|
import shutil
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
import spack
|
import spack
|
||||||
@ -530,9 +532,29 @@ def child_execution(child_connection):
|
|||||||
try:
|
try:
|
||||||
setup_package(pkg, dirty=dirty)
|
setup_package(pkg, dirty=dirty)
|
||||||
function()
|
function()
|
||||||
child_connection.send([None, None, None])
|
child_connection.send(None)
|
||||||
except Exception as e:
|
except:
|
||||||
child_connection.send([type(e), e, None])
|
# catch ANYTHING that goes wrong in the child process
|
||||||
|
exc_type, exc, tb = sys.exc_info()
|
||||||
|
|
||||||
|
# Need to unwind the traceback in the child because traceback
|
||||||
|
# objects can't be sent to the parent.
|
||||||
|
tb_string = traceback.format_exc()
|
||||||
|
|
||||||
|
# build up some context from the offending package so we can
|
||||||
|
# show that, too.
|
||||||
|
package_context = get_package_context(tb)
|
||||||
|
|
||||||
|
build_log = None
|
||||||
|
if hasattr(pkg, 'log_path'):
|
||||||
|
build_log = pkg.log_path
|
||||||
|
|
||||||
|
# make a pickleable exception to send to parent.
|
||||||
|
msg = "%s: %s" % (str(exc_type.__name__), str(exc))
|
||||||
|
|
||||||
|
ce = ChildError(msg, tb_string, build_log, package_context)
|
||||||
|
child_connection.send(ce)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
child_connection.close()
|
child_connection.close()
|
||||||
|
|
||||||
@ -542,11 +564,126 @@ def child_execution(child_connection):
|
|||||||
args=(child_connection,)
|
args=(child_connection,)
|
||||||
)
|
)
|
||||||
p.start()
|
p.start()
|
||||||
exc_type, exception, traceback = parent_connection.recv()
|
child_exc = parent_connection.recv()
|
||||||
p.join()
|
p.join()
|
||||||
if exception is not None:
|
|
||||||
raise exception
|
if child_exc is not None:
|
||||||
|
raise child_exc
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_context(traceback):
|
||||||
|
"""Return some context for an error message when the build fails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
traceback -- A traceback from some exception raised during install.
|
||||||
|
|
||||||
|
This function inspects the stack to find where we failed in the
|
||||||
|
package file, and it adds detailed context to the long_message
|
||||||
|
from there.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def make_stack(tb, stack=None):
|
||||||
|
"""Tracebacks come out of the system in caller -> callee order. Return
|
||||||
|
an array in callee -> caller order so we can traverse it."""
|
||||||
|
if stack is None:
|
||||||
|
stack = []
|
||||||
|
if tb is not None:
|
||||||
|
make_stack(tb.tb_next, stack)
|
||||||
|
stack.append(tb)
|
||||||
|
return stack
|
||||||
|
|
||||||
|
stack = make_stack(traceback)
|
||||||
|
|
||||||
|
for tb in stack:
|
||||||
|
frame = tb.tb_frame
|
||||||
|
if 'self' in frame.f_locals:
|
||||||
|
# Find the first proper subclass of spack.PackageBase.
|
||||||
|
obj = frame.f_locals['self']
|
||||||
|
if isinstance(obj, spack.PackageBase):
|
||||||
|
break
|
||||||
|
|
||||||
|
# we found obj, the Package implementation we care about.
|
||||||
|
# point out the location in the install method where we failed.
|
||||||
|
lines = []
|
||||||
|
lines.append("%s:%d, in %s:" % (
|
||||||
|
inspect.getfile(frame.f_code), frame.f_lineno, frame.f_code.co_name
|
||||||
|
))
|
||||||
|
|
||||||
|
# Build a message showing context in the install method.
|
||||||
|
sourcelines, start = inspect.getsourcelines(frame)
|
||||||
|
for i, line in enumerate(sourcelines):
|
||||||
|
mark = ">> " if start + i == frame.f_lineno else " "
|
||||||
|
lines.append(" %s%-5d%s" % (mark, start + i, line.rstrip()))
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
class InstallError(spack.error.SpackError):
|
class InstallError(spack.error.SpackError):
|
||||||
"""Raised when a package fails to install"""
|
"""Raised by packages when a package fails to install"""
|
||||||
|
|
||||||
|
|
||||||
|
class ChildError(spack.error.SpackError):
|
||||||
|
"""Special exception class for wrapping exceptions from child processes
|
||||||
|
in Spack's build environment.
|
||||||
|
|
||||||
|
The main features of a ChildError are:
|
||||||
|
|
||||||
|
1. They're serializable, so when a child build fails, we can send one
|
||||||
|
of these to the parent and let the parent report what happened.
|
||||||
|
|
||||||
|
2. They have a ``traceback`` field containing a traceback generated
|
||||||
|
on the child immediately after failure. Spack will print this on
|
||||||
|
failure in lieu of trying to run sys.excepthook on the parent
|
||||||
|
process, so users will see the correct stack trace from a child.
|
||||||
|
|
||||||
|
3. They also contain package_context, which shows source code context
|
||||||
|
in the Package implementation where the error happened. To get
|
||||||
|
this, Spack searches the stack trace for the deepest frame where
|
||||||
|
``self`` is in scope and is an instance of PackageBase. This will
|
||||||
|
generally find a useful spot in the ``package.py`` file.
|
||||||
|
|
||||||
|
The long_message of a ChildError displays all this stuff to the user,
|
||||||
|
and SpackError handles displaying the special traceback if we're in
|
||||||
|
debug mode with spack -d.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, traceback_string, build_log, package_context):
|
||||||
|
super(ChildError, self).__init__(msg)
|
||||||
|
self.traceback = traceback_string
|
||||||
|
self.build_log = build_log
|
||||||
|
self.package_context = package_context
|
||||||
|
|
||||||
|
@property
|
||||||
|
def long_message(self):
|
||||||
|
msg = self._long_message if self._long_message else ''
|
||||||
|
|
||||||
|
if self.package_context:
|
||||||
|
if msg:
|
||||||
|
msg += "\n\n"
|
||||||
|
msg += '\n'.join(self.package_context)
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg += "\n\n"
|
||||||
|
|
||||||
|
if self.build_log:
|
||||||
|
msg += "See build log for details:\n"
|
||||||
|
msg += " %s" % self.build_log
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
"""__reduce__ is used to serialize (pickle) ChildErrors.
|
||||||
|
|
||||||
|
Return a function to reconstruct a ChildError, along with the
|
||||||
|
salient properties we'll need.
|
||||||
|
"""
|
||||||
|
return _make_child_error, (
|
||||||
|
self.message,
|
||||||
|
self.traceback,
|
||||||
|
self.build_log,
|
||||||
|
self.package_context)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_child_error(msg, traceback, build_log, package_context):
|
||||||
|
"""Used by __reduce__ in ChildError to reconstruct pickled errors."""
|
||||||
|
return ChildError(msg, traceback, build_log, package_context)
|
||||||
|
@ -39,19 +39,31 @@ def __init__(self, message, long_message=None):
|
|||||||
self.message = message
|
self.message = message
|
||||||
self._long_message = long_message
|
self._long_message = long_message
|
||||||
|
|
||||||
|
# for exceptions raised from child build processes, we save the
|
||||||
|
# traceback as a string and print it in the parent.
|
||||||
|
self.traceback = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def long_message(self):
|
def long_message(self):
|
||||||
return self._long_message
|
return self._long_message
|
||||||
|
|
||||||
def die(self):
|
def die(self):
|
||||||
|
# basic debug message
|
||||||
|
tty.error(self.message)
|
||||||
|
if self.long_message:
|
||||||
|
print(self.long_message)
|
||||||
|
|
||||||
|
# stack trace, etc. in debug mode.
|
||||||
if spack.debug:
|
if spack.debug:
|
||||||
sys.excepthook(*sys.exc_info())
|
if self.traceback:
|
||||||
os._exit(1)
|
# exception came from a build child, already got
|
||||||
else:
|
# traceback in child, so print it.
|
||||||
tty.error(self.message)
|
sys.stderr.write(self.traceback)
|
||||||
if self.long_message:
|
else:
|
||||||
print(self.long_message)
|
# run parent exception hook.
|
||||||
os._exit(1)
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
msg = self.message
|
msg = self.message
|
||||||
|
@ -65,7 +65,6 @@
|
|||||||
from spack.stage import Stage, ResourceStage, StageComposite
|
from spack.stage import Stage, ResourceStage, StageComposite
|
||||||
from spack.util.crypto import bit_length
|
from spack.util.crypto import bit_length
|
||||||
from spack.util.environment import dump_environment
|
from spack.util.environment import dump_environment
|
||||||
from spack.util.executable import ProcessError
|
|
||||||
from spack.version import *
|
from spack.version import *
|
||||||
|
|
||||||
"""Allowed URL schemes for spack packages."""
|
"""Allowed URL schemes for spack packages."""
|
||||||
@ -96,6 +95,8 @@ def __get__(self, instance, owner):
|
|||||||
# install phase, thus return a properly set wrapper
|
# install phase, thus return a properly set wrapper
|
||||||
phase = getattr(instance, self.name)
|
phase = getattr(instance, self.name)
|
||||||
|
|
||||||
|
print phase
|
||||||
|
|
||||||
@functools.wraps(phase)
|
@functools.wraps(phase)
|
||||||
def phase_wrapper(spec, prefix):
|
def phase_wrapper(spec, prefix):
|
||||||
# Check instance attributes at the beginning of a phase
|
# Check instance attributes at the beginning of a phase
|
||||||
@ -1213,60 +1214,57 @@ def build_process():
|
|||||||
|
|
||||||
self.stage.keep = keep_stage
|
self.stage.keep = keep_stage
|
||||||
|
|
||||||
try:
|
with contextlib.nested(self.stage, self._prefix_write_lock()):
|
||||||
with contextlib.nested(self.stage, self._prefix_write_lock()):
|
# Run the pre-install hook in the child process after
|
||||||
# Run the pre-install hook in the child process after
|
# the directory is created.
|
||||||
# the directory is created.
|
spack.hooks.pre_install(self)
|
||||||
spack.hooks.pre_install(self)
|
if fake:
|
||||||
if fake:
|
self.do_fake_install()
|
||||||
self.do_fake_install()
|
else:
|
||||||
else:
|
# Do the real install in the source directory.
|
||||||
# Do the real install in the source directory.
|
self.stage.chdir_to_source()
|
||||||
self.stage.chdir_to_source()
|
|
||||||
# Save the build environment in a file before building.
|
|
||||||
env_path = join_path(os.getcwd(), 'spack-build.env')
|
|
||||||
# Redirect I/O to a build log (and optionally to
|
|
||||||
# the terminal)
|
|
||||||
log_path = join_path(os.getcwd(), 'spack-build.out')
|
|
||||||
# FIXME : refactor this assignment
|
|
||||||
self.log_path = log_path
|
|
||||||
self.env_path = env_path
|
|
||||||
dump_environment(env_path)
|
|
||||||
# Spawn a daemon that reads from a pipe and redirects
|
|
||||||
# everything to log_path
|
|
||||||
redirection_context = log_output(
|
|
||||||
log_path, verbose,
|
|
||||||
sys.stdout.isatty(),
|
|
||||||
True
|
|
||||||
)
|
|
||||||
with redirection_context as log_redirection:
|
|
||||||
for phase_name, phase in zip(self.phases, self._InstallPhase_phases): # NOQA: ignore=E501
|
|
||||||
tty.msg(
|
|
||||||
'Executing phase : \'{0}\''.format(phase_name) # NOQA: ignore=E501
|
|
||||||
)
|
|
||||||
# Redirect stdout and stderr to daemon pipe
|
|
||||||
with log_redirection:
|
|
||||||
getattr(self, phase)(
|
|
||||||
self.spec, self.prefix)
|
|
||||||
self.log()
|
|
||||||
# Run post install hooks before build stage is removed.
|
|
||||||
spack.hooks.post_install(self)
|
|
||||||
|
|
||||||
# Stop timer.
|
# Save the build environment in a file before building.
|
||||||
self._total_time = time.time() - start_time
|
env_path = join_path(os.getcwd(), 'spack-build.env')
|
||||||
build_time = self._total_time - self._fetch_time
|
|
||||||
|
|
||||||
tty.msg("Successfully installed %s" % self.name,
|
# Redirect I/O to a build log (and optionally to
|
||||||
"Fetch: %s. Build: %s. Total: %s." %
|
# the terminal)
|
||||||
(_hms(self._fetch_time), _hms(build_time),
|
log_path = join_path(os.getcwd(), 'spack-build.out')
|
||||||
_hms(self._total_time)))
|
|
||||||
print_pkg(self.prefix)
|
|
||||||
|
|
||||||
except ProcessError as e:
|
# FIXME : refactor this assignment
|
||||||
# Annotate ProcessErrors with the location of
|
self.log_path = log_path
|
||||||
# the build log
|
self.env_path = env_path
|
||||||
e.build_log = log_path
|
dump_environment(env_path)
|
||||||
raise e
|
|
||||||
|
# Spawn a daemon that reads from a pipe and redirects
|
||||||
|
# everything to log_path
|
||||||
|
redirection_context = log_output(
|
||||||
|
log_path, verbose,
|
||||||
|
sys.stdout.isatty(),
|
||||||
|
True
|
||||||
|
)
|
||||||
|
with redirection_context as log_redirection:
|
||||||
|
for phase_name, phase in zip(self.phases, self._InstallPhase_phases): # NOQA: ignore=E501
|
||||||
|
tty.msg(
|
||||||
|
'Executing phase : \'{0}\''.format(phase_name) # NOQA: ignore=E501
|
||||||
|
)
|
||||||
|
# Redirect stdout and stderr to daemon pipe
|
||||||
|
with log_redirection:
|
||||||
|
getattr(self, phase)(
|
||||||
|
self.spec, self.prefix)
|
||||||
|
self.log()
|
||||||
|
# Run post install hooks before build stage is removed.
|
||||||
|
spack.hooks.post_install(self)
|
||||||
|
|
||||||
|
# Stop timer.
|
||||||
|
self._total_time = time.time() - start_time
|
||||||
|
build_time = self._total_time - self._fetch_time
|
||||||
|
|
||||||
|
tty.msg("Successfully installed %s" % self.name,
|
||||||
|
"Fetch: %s. Build: %s. Total: %s." %
|
||||||
|
(_hms(self._fetch_time), _hms(build_time),
|
||||||
|
_hms(self._total_time)))
|
||||||
|
print_pkg(self.prefix)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the install prefix and fork the build process.
|
# Create the install prefix and fork the build process.
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import inspect
|
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
import spack
|
import spack
|
||||||
@ -237,94 +236,4 @@ def which(name, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class ProcessError(spack.error.SpackError):
|
class ProcessError(spack.error.SpackError):
|
||||||
|
"""ProcessErrors are raised when Executables exit with an error code."""
|
||||||
def __init__(self, msg, long_message=None):
|
|
||||||
# These are used for detailed debugging information for
|
|
||||||
# package builds. They're built up gradually as the exception
|
|
||||||
# propagates.
|
|
||||||
self.package_context = _get_package_context()
|
|
||||||
self.build_log = None
|
|
||||||
|
|
||||||
super(ProcessError, self).__init__(msg, long_message)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def long_message(self):
|
|
||||||
|
|
||||||
msg = self._long_message if self._long_message else ''
|
|
||||||
|
|
||||||
if self.package_context:
|
|
||||||
if msg:
|
|
||||||
msg += "\n\n"
|
|
||||||
msg += '\n'.join(self.package_context)
|
|
||||||
|
|
||||||
if msg:
|
|
||||||
msg += "\n\n"
|
|
||||||
|
|
||||||
if self.build_log:
|
|
||||||
msg += "See build log for details:\n"
|
|
||||||
msg += " %s" % self.build_log
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
# We need this constructor because we are trying to move a ProcessError
|
|
||||||
# across processes. This means that we have to preserve the original
|
|
||||||
# package context and build log
|
|
||||||
return _make_process_error, (
|
|
||||||
self.message,
|
|
||||||
self._long_message,
|
|
||||||
self.package_context,
|
|
||||||
self.build_log
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_process_error(msg, long_message, pkg_context, build_log):
|
|
||||||
a = ProcessError(msg, long_message)
|
|
||||||
a.package_context = pkg_context
|
|
||||||
a.build_log = build_log
|
|
||||||
return a
|
|
||||||
|
|
||||||
|
|
||||||
def _get_package_context():
|
|
||||||
"""Return some context for an error message when the build fails.
|
|
||||||
|
|
||||||
This should be called within a ProcessError when the exception is
|
|
||||||
thrown.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
process_error -- A ProcessError raised during install()
|
|
||||||
|
|
||||||
This function inspects the stack to find where we failed in the
|
|
||||||
package file, and it adds detailed context to the long_message
|
|
||||||
from there.
|
|
||||||
|
|
||||||
"""
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# Walk up the stack
|
|
||||||
for f in inspect.stack():
|
|
||||||
frame = f[0]
|
|
||||||
|
|
||||||
# Find a frame with 'self' in the local variables.
|
|
||||||
if 'self' not in frame.f_locals:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Look only at a frame in a subclass of spack.Package
|
|
||||||
obj = frame.f_locals['self']
|
|
||||||
if type(obj) != spack.PackageBase and isinstance(obj, spack.PackageBase): # NOQA: ignore=E501
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Didn't find anything
|
|
||||||
return lines
|
|
||||||
|
|
||||||
# Build a message showing where in install we failed.
|
|
||||||
lines.append("%s:%d, in %s:" % (
|
|
||||||
inspect.getfile(frame.f_code), frame.f_lineno, frame.f_code.co_name
|
|
||||||
))
|
|
||||||
|
|
||||||
sourcelines, start = inspect.getsourcelines(frame)
|
|
||||||
for i, line in enumerate(sourcelines):
|
|
||||||
mark = ">> " if start + i == frame.f_lineno else " "
|
|
||||||
lines.append(" %s%-5d%s" % (mark, start + i, line.rstrip()))
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
Loading…
Reference in New Issue
Block a user