Path: blob/master/AndroidRunner/PrematureStoppableRun.py
629 views
import logging1from .StopRunWebserver import StopRunWebserver2from .util import ConfigError, keyboardinterrupt_handler3import time4from http.server import BaseHTTPRequestHandler, HTTPServer5import multiprocessing as mp6import psutil78class PrematureStoppableRun(object):9""" Starts a run that is stopped prematurely when:101. a certain regex is matched in the logcat of the given device.112. a HTTP POST request is received by the local webserver.123. the stop() method is called on the Experiment object instance.1314If this does not happen the run continues and finishes as usual thus not stopping early/prematurely.1516When an user chooses either the logcat_regex or post_request method he/she can also use the stop() function call.1718A "run" in Android Runner basically consists of what happens between the start_profiling and stop_profiling functional calls.19In AR this is the interaction function. So we want to run this function and stop it when a regex is matched,20post request is received or function call is executed.2122From a high level perspective it works as follows:23We run two processes (in addition to our main process) simultaneously:241. A process running the interaction function (the AR "run")252. A processes that either:26- runs a webserver (for the post_request option) or27- continuosly checks the logcat for the given regex (for the logcat_regex option).28In addition we have a queue which is shared among these two processes (as well as the main process).29We then block the main process, waiting for one of the two processes to write to the queue.30The process running the interaction function will write to the queue when the interation (and thus the run) has finished.31The process running either a webserver will write to the queue when a HTTP POST request is received or when the logcat matches the regex.32When the stop() method is called on the Experiment object instance it will also write to the queue.33After a process writes to the queue the given process is finished and the main process will continue as well.34We then terminate the "other" process that is left.35For example: when HTTP POST request was received or logcat regex was matched the interaction process will get terminated thus stopping the run and vice versa.36"""3738STOPPING_MECHANISM_HTTP_POST_REQUEST = "HTTP POST request"39STOPPING_MECHANISM_LOGCAT_REGEX = "matching regex"40STOPPING_MECHANISM_FUNCTION_CALL = "stop() function call"4142def __init__(self, run_stopping_condition_config, queue, interaction_function, device, path, run, *args, **kwargs):43""" Creates a PrematureStoppableRun instance.4445Parameters46----------47run_stopping_condition_config : dict48A dictionary containing the run stopping condition (post_request, logcat_regex, function),49the regex (in case of logcat_regex) and optional options (port number in case of post_request).50queue : multiprocessing.Queue51The queue that is shared among the main process and child processes.52interaction_function : function53The interaction function that represents the run.54device : AndroidRunner.Device55The device for the current run.56path : str57The path for the current run58run : int59The currents run count.60*args61Variable length argument list62**kwargs63Arbitrary keyword arguments.64"""65self.run_stopping_condition_config = run_stopping_condition_config66self.queue = queue67self.interaction_function = interaction_function68self.device = device69self.path = path70self.args = args71self.kwargs = kwargs72self.logger = logging.getLogger(self.__class__.__name__)7374self.condition = next(iter(self.run_stopping_condition_config))75if self.condition not in ["function", "post_request", "logcat_regex"]:76raise ConfigError("Given run_stopping_condition is not accepted. Accepted values are function, post_request or logcat_regex")7778self.regex = run_stopping_condition_config[self.condition].get("regex", None)79if self.condition == "logcat_regex" and self.regex == None:80raise ConfigError("A regex must be given when run_stopping_condition is set to logcat_regex.")8182self.server_port = run_stopping_condition_config[self.condition].get("port", StopRunWebserver.DEFAULT_SERVER_PORT)83if not isinstance(self.server_port, int):84raise ConfigError("Provided server port for run_stopping_condition value must be an integer.")8586@keyboardinterrupt_handler87def _mp_interaction(self, queue, interaction_function, device, path, run, *args, **kwargs):88""" Runs the provided interaction_function and when done writes to the central shared <queue>.8990Parameters91----------92queue : multiprocessing.Queue93The queue that is shared among the main process and child processes.94interaction_function : function95The interaction function (run) that needs to be executed and which can be prematurely stopped.96device : AndroidRunner.Device97The device for the current run.98path : str99The path for the current run100run : int101The current run count.102*args103Variable length argument list104**kwargs105Arbitrary keyword arguments.106"""107interaction_function(device, path, run, *args, **kwargs)108queue.put("interaction")109110@keyboardinterrupt_handler111def _mp_logcat_regex(self, queue, device, regex):112""" Keeps checking the logcat of the <device> until an113entry matching the <regex> is found. When done it writes114to the shared <queue> so main process knows it can stop other process(es).115116Parameters117----------118queue : multiprocessing.Queue119The queue that is shared among the main process and child processes.120device : AndroidRunner.Device121The device for the current run.122regex : str123The regex that should be matched.124"""125while not device.logcat_regex(regex):126time.sleep(1)127queue.put(PrematureStoppableRun.STOPPING_MECHANISM_LOGCAT_REGEX)128129@keyboardinterrupt_handler130def _mp_post_request(self, queue, server_port):131""" Starts a local webserver on <server_port> that stops when a HTTP POST132request is received. It then writes to the central shared <queue>.133134Parameters135---------136queue : multiprocessing.Queue137The queue that is shared among the main process and child processes.138server_port : int139The port on which the local webserver is started.140"""141self.logger.info(f"Starting webserver on port {server_port}.")142webServer = HTTPServer(("", server_port), StopRunWebserver)143144# We "serve_forever" but the server will stop itself when a HTTP POST request was received.145webServer.serve_forever()146queue.put(PrematureStoppableRun.STOPPING_MECHANISM_HTTP_POST_REQUEST)147148def run(self):149""" Runs the interaction (run) process in a new process which can be prematurely stopped by150the stop() function call, a receiving HTTP POST request or found regex.151"""152procs = []153154# Start either a local webserver or continuously check the devices logcat for a regex in a new process.155# When the condition is set to "function" we don't need to start another process, only the interaction process.156if self.condition == "post_request":157procs.append(mp.Process(target=self._mp_post_request, args=(self.queue, self.server_port,)))158elif self.condition == "logcat_regex":159procs.append(mp.Process(target=self._mp_logcat_regex, args=(self.queue, self.device, self.regex,)))160161# Always run the interaction (run).162procs.append(mp.Process(target=self._mp_interaction, args=(self.queue, self.interaction_function, self.device, self.path, self.run, *self.args,), kwargs=self.kwargs))163164for proc in procs:165proc.start()166167# Wait till one of the created processes writes to the queue. It means that that process is finished.168res = self.queue.get()169170if res != "interaction":171self.logger.info(f"Run was prematurely stopped by means of a(n) {res}.")172173# Terminate all processes also the ones that are not finished (since it may have child processes we have to kill them too).174for proc in procs:175parent = psutil.Process(proc.pid)176177# Kill its child proccesses.178for child in parent.children(recursive=True):179child.terminate()180181proc.terminate()182183184