Source code for openwpm.utilities.multiprocess_utils
import logging
import os
import sys
import traceback
import multiprocess as mp
import psutil
[docs]
def parse_traceback_for_sentry(tb):
"""Parse traceback to send Sentry-compatible strings
Sentry appears to limit each `extra` string to 500 characters. This splits
the traceback string across N chunks of at most 500 characters each.
Chunks are split at newlines for readability. Traceback lines over 500
characters are still truncated.
Parameters
----------
tb : list of strings
Traceback formatted such that each list item is a new line.
"""
out = dict()
out_str = ""
counter = 0
for i in range(len(tb)):
out_str += tb[i][0 : min(500, len(tb[i]))]
if i != len(tb) - 1 and len(out_str) + len(tb[i + 1]) < 500:
continue
out["traceback-%d" % counter] = out_str
out_str = ""
counter += 1
return out
[docs]
class Process(mp.Process):
"""Wrapper Process class that includes exception logging"""
def __init__(self, *args, **kwargs):
mp.Process.__init__(self, *args, **kwargs)
self.logger = logging.getLogger("openwpm")
[docs]
def run(self):
# Enable coverage collection in child processes when COVERAGE_PROCESS_START is set
if "COVERAGE_PROCESS_START" in os.environ:
import coverage
coverage.process_startup()
try:
self.run_impl()
except Exception as e:
tb = traceback.format_exception(*sys.exc_info())
extra = parse_traceback_for_sentry(tb)
extra["exception"] = tb[-1]
self.logger.error("Exception in child process.", exc_info=True, extra=extra)
raise e
finally:
# Save coverage data before the process exits, since
# multiprocess.Process uses os._exit() which skips atexit handlers.
if "COVERAGE_PROCESS_START" in os.environ:
import coverage
cov = coverage.Coverage.current()
if cov is not None:
cov.stop()
cov.save()
[docs]
def run_impl(self):
"""Override this method in subclasses instead of run().
The base implementation calls the target function (if provided),
matching the default multiprocess.Process behavior.
"""
mp.Process.run(self)
[docs]
def kill_process_and_children(
parent_process: psutil.Process, logger, timeout: int = 20
) -> None:
"""Attempts to recursively kill the entire process tree under
a given parent process"""
try:
for child in parent_process.children():
kill_process_and_children(child, logger)
parent_process.kill()
parent_process.wait(timeout=timeout)
except psutil.NoSuchProcess:
logger.debug("Process %i has already exited", parent_process.pid)
pass
except psutil.TimeoutExpired:
logger.debug(
"Timeout while waiting for process %i to terminate" % parent_process.pid
)
pass