Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
S2-group
GitHub Repository: S2-group/android-runner
Path: blob/master/AndroidRunner/PrematureStoppableRun.py
629 views
1
import logging
2
from .StopRunWebserver import StopRunWebserver
3
from .util import ConfigError, keyboardinterrupt_handler
4
import time
5
from http.server import BaseHTTPRequestHandler, HTTPServer
6
import multiprocessing as mp
7
import psutil
8
9
class PrematureStoppableRun(object):
10
""" Starts a run that is stopped prematurely when:
11
1. a certain regex is matched in the logcat of the given device.
12
2. a HTTP POST request is received by the local webserver.
13
3. the stop() method is called on the Experiment object instance.
14
15
If this does not happen the run continues and finishes as usual thus not stopping early/prematurely.
16
17
When an user chooses either the logcat_regex or post_request method he/she can also use the stop() function call.
18
19
A "run" in Android Runner basically consists of what happens between the start_profiling and stop_profiling functional calls.
20
In AR this is the interaction function. So we want to run this function and stop it when a regex is matched,
21
post request is received or function call is executed.
22
23
From a high level perspective it works as follows:
24
We run two processes (in addition to our main process) simultaneously:
25
1. A process running the interaction function (the AR "run")
26
2. A processes that either:
27
- runs a webserver (for the post_request option) or
28
- continuosly checks the logcat for the given regex (for the logcat_regex option).
29
In addition we have a queue which is shared among these two processes (as well as the main process).
30
We then block the main process, waiting for one of the two processes to write to the queue.
31
The process running the interaction function will write to the queue when the interation (and thus the run) has finished.
32
The process running either a webserver will write to the queue when a HTTP POST request is received or when the logcat matches the regex.
33
When the stop() method is called on the Experiment object instance it will also write to the queue.
34
After a process writes to the queue the given process is finished and the main process will continue as well.
35
We then terminate the "other" process that is left.
36
For 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.
37
"""
38
39
STOPPING_MECHANISM_HTTP_POST_REQUEST = "HTTP POST request"
40
STOPPING_MECHANISM_LOGCAT_REGEX = "matching regex"
41
STOPPING_MECHANISM_FUNCTION_CALL = "stop() function call"
42
43
def __init__(self, run_stopping_condition_config, queue, interaction_function, device, path, run, *args, **kwargs):
44
""" Creates a PrematureStoppableRun instance.
45
46
Parameters
47
----------
48
run_stopping_condition_config : dict
49
A dictionary containing the run stopping condition (post_request, logcat_regex, function),
50
the regex (in case of logcat_regex) and optional options (port number in case of post_request).
51
queue : multiprocessing.Queue
52
The queue that is shared among the main process and child processes.
53
interaction_function : function
54
The interaction function that represents the run.
55
device : AndroidRunner.Device
56
The device for the current run.
57
path : str
58
The path for the current run
59
run : int
60
The currents run count.
61
*args
62
Variable length argument list
63
**kwargs
64
Arbitrary keyword arguments.
65
"""
66
self.run_stopping_condition_config = run_stopping_condition_config
67
self.queue = queue
68
self.interaction_function = interaction_function
69
self.device = device
70
self.path = path
71
self.args = args
72
self.kwargs = kwargs
73
self.logger = logging.getLogger(self.__class__.__name__)
74
75
self.condition = next(iter(self.run_stopping_condition_config))
76
if self.condition not in ["function", "post_request", "logcat_regex"]:
77
raise ConfigError("Given run_stopping_condition is not accepted. Accepted values are function, post_request or logcat_regex")
78
79
self.regex = run_stopping_condition_config[self.condition].get("regex", None)
80
if self.condition == "logcat_regex" and self.regex == None:
81
raise ConfigError("A regex must be given when run_stopping_condition is set to logcat_regex.")
82
83
self.server_port = run_stopping_condition_config[self.condition].get("port", StopRunWebserver.DEFAULT_SERVER_PORT)
84
if not isinstance(self.server_port, int):
85
raise ConfigError("Provided server port for run_stopping_condition value must be an integer.")
86
87
@keyboardinterrupt_handler
88
def _mp_interaction(self, queue, interaction_function, device, path, run, *args, **kwargs):
89
""" Runs the provided interaction_function and when done writes to the central shared <queue>.
90
91
Parameters
92
----------
93
queue : multiprocessing.Queue
94
The queue that is shared among the main process and child processes.
95
interaction_function : function
96
The interaction function (run) that needs to be executed and which can be prematurely stopped.
97
device : AndroidRunner.Device
98
The device for the current run.
99
path : str
100
The path for the current run
101
run : int
102
The current run count.
103
*args
104
Variable length argument list
105
**kwargs
106
Arbitrary keyword arguments.
107
"""
108
interaction_function(device, path, run, *args, **kwargs)
109
queue.put("interaction")
110
111
@keyboardinterrupt_handler
112
def _mp_logcat_regex(self, queue, device, regex):
113
""" Keeps checking the logcat of the <device> until an
114
entry matching the <regex> is found. When done it writes
115
to the shared <queue> so main process knows it can stop other process(es).
116
117
Parameters
118
----------
119
queue : multiprocessing.Queue
120
The queue that is shared among the main process and child processes.
121
device : AndroidRunner.Device
122
The device for the current run.
123
regex : str
124
The regex that should be matched.
125
"""
126
while not device.logcat_regex(regex):
127
time.sleep(1)
128
queue.put(PrematureStoppableRun.STOPPING_MECHANISM_LOGCAT_REGEX)
129
130
@keyboardinterrupt_handler
131
def _mp_post_request(self, queue, server_port):
132
""" Starts a local webserver on <server_port> that stops when a HTTP POST
133
request is received. It then writes to the central shared <queue>.
134
135
Parameters
136
---------
137
queue : multiprocessing.Queue
138
The queue that is shared among the main process and child processes.
139
server_port : int
140
The port on which the local webserver is started.
141
"""
142
self.logger.info(f"Starting webserver on port {server_port}.")
143
webServer = HTTPServer(("", server_port), StopRunWebserver)
144
145
# We "serve_forever" but the server will stop itself when a HTTP POST request was received.
146
webServer.serve_forever()
147
queue.put(PrematureStoppableRun.STOPPING_MECHANISM_HTTP_POST_REQUEST)
148
149
def run(self):
150
""" Runs the interaction (run) process in a new process which can be prematurely stopped by
151
the stop() function call, a receiving HTTP POST request or found regex.
152
"""
153
procs = []
154
155
# Start either a local webserver or continuously check the devices logcat for a regex in a new process.
156
# When the condition is set to "function" we don't need to start another process, only the interaction process.
157
if self.condition == "post_request":
158
procs.append(mp.Process(target=self._mp_post_request, args=(self.queue, self.server_port,)))
159
elif self.condition == "logcat_regex":
160
procs.append(mp.Process(target=self._mp_logcat_regex, args=(self.queue, self.device, self.regex,)))
161
162
# Always run the interaction (run).
163
procs.append(mp.Process(target=self._mp_interaction, args=(self.queue, self.interaction_function, self.device, self.path, self.run, *self.args,), kwargs=self.kwargs))
164
165
for proc in procs:
166
proc.start()
167
168
# Wait till one of the created processes writes to the queue. It means that that process is finished.
169
res = self.queue.get()
170
171
if res != "interaction":
172
self.logger.info(f"Run was prematurely stopped by means of a(n) {res}.")
173
174
# Terminate all processes also the ones that are not finished (since it may have child processes we have to kill them too).
175
for proc in procs:
176
parent = psutil.Process(proc.pid)
177
178
# Kill its child proccesses.
179
for child in parent.children(recursive=True):
180
child.terminate()
181
182
proc.terminate()
183
184