-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbaserunner.py
240 lines (202 loc) · 7.56 KB
/
baserunner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import abc
import argparse
import datetime
import glob
import os
import shutil
import subprocess
import sys
import time
from typing import List
class Runner(abc.ABC):
'''
Abstract base class for runner class.
'''
TIMEOUT = 6*3600
'''Default timeout'''
def __init__(self, path: str, args: List[str],
timeout: int, artifact_dir: str):
"""
Parameters:
path The path of (PJSIP) program to run
args Arguments to be given to the (PJSIP) program
timeout Maximum run time in seconds after which program will be killed
and dump will be generated
artifact_dir Output directory to copy artifacts (program and dump file)
"""
self.path = os.path.abspath(path)
'''Path to program'''
if not os.path.exists(self.path):
raise Exception(f'Program not found: {self.path}')
self.args = args
'''Arguments for the program'''
self.timeout = timeout
'''Maximum running time (secs) before we kill the program'''
self.artifact_dir = artifact_dir
'''Output directory to copy artifacts (program and dump file)'''
if self.artifact_dir:
self.artifact_dir = os.path.abspath(self.artifact_dir)
self.info(f'will write artifacts to {self.artifact_dir} on crash')
self.popen : subprocess.Popen = None
'''Popen object when running the program, will be set later'''
self.info(f'running. cmd="{self.path}", args={self.args}, timeout={self.timeout}')
@classmethod
def info(cls, msg, box=False):
t = datetime.datetime.now()
if box:
print('\n' + '#'*60)
print('##')
print(('## ' if box else '') + t.strftime('%H:%M:%S') + ' cirunner: ' + msg)
if box:
print('##')
print('#'*60)
sys.stdout.flush()
@classmethod
def err(cls, msg, box=False):
t = datetime.datetime.now()
if box:
sys.stderr.write('\n' + '#'*60 + '\n')
sys.stderr.write('##\n')
sys.stderr.write(('## ' if box else '') + t.strftime('%H:%M:%S') + ' cirunner: ' + msg + '\n')
if box:
sys.stderr.write('##\n')
sys.stderr.write('#'*60 + '\n')
sys.stderr.flush()
@classmethod
@abc.abstractmethod
def get_dump_dir(cls) -> str:
"""
Returns directory where dump file will be saved
"""
pass
@abc.abstractmethod
def get_dump_path(self) -> str:
"""
Get the path of core dump file
"""
pass
@classmethod
@abc.abstractmethod
def get_dump_pattern(cls) -> str:
"""
Get file pattern to find dump files
"""
pass
@classmethod
@abc.abstractmethod
def install(cls):
"""
Install crash handler for this machine
"""
pass
def detect_crash(self) -> bool:
"""
Determine whether process has crashed or just exited normally.
Returns True if it had crashed.
"""
dump_path = self.get_dump_path()
return os.path.exists(dump_path)
@abc.abstractmethod
def process_crash(self):
"""
Process dump file.
"""
pass
@abc.abstractmethod
def terminate(self):
"""
Terminate a process and generate dump file
"""
pass
def warmup(self):
"""
This will be called before run()
"""
pass
def get_additional_artifacts(self) -> List[str]:
"""
Return list of files to be uploaded as additional artifacts when
the program crashed and artifact_dir is set.
"""
return []
def run(self):
"""
Run the program, monitor dump file when crash happens, and terminate
the program if it runs for longer than permitted.
"""
# TODO:
# 1. It looks like when the program crashes, some of the output may not
# be written to stdout/stderr. This is also reported by:
# https://stackoverflow.com/q/77117927/7975037
self.warmup()
self.popen = subprocess.Popen([self.path] + self.args,
shell=False,
bufsize=0,
universal_newlines=True)
self.info(f'program launched, pid={self.popen.pid}')
try:
self.popen.wait(self.timeout)
except subprocess.TimeoutExpired as e:
self.info('Execution timeout, terminating process..', box=True)
self.terminate()
time.sleep(1)
if not self.popen.returncode:
self.popen.returncode = 1234567
if self.popen.returncode != 0:
self.info(f'exit code {self.popen.returncode}, waiting until crash dump is written')
for _ in range(30):
if self.detect_crash():
break
time.sleep(1)
if not self.detect_crash():
self.err('ERROR: UNABLE TO FIND CRASH DUMP FILE!')
dump_dir = self.get_dump_dir()
pat = self.get_dump_pattern()
files = glob.glob(os.path.join(dump_dir, pat))
self.err(f'ls {dump_dir}/{pat}: ' + ' '.join(files[:20]))
else:
self.info(f'crash dump found: {self.get_dump_path()}')
time.sleep(5)
if self.detect_crash():
self.info('Processing crash info..', box=True)
if self.artifact_dir:
try:
self.info(f'Copying artifacts to {self.artifact_dir}..',)
if not os.path.exists(self.artifact_dir):
os.makedirs(self.artifact_dir)
self.info(f' Copying {self.path}..',)
shutil.copy(self.path, self.artifact_dir)
self.info(f' Copying {self.get_dump_path()}..',)
shutil.copy(self.get_dump_path(), self.artifact_dir)
files = self.get_additional_artifacts()
for file in files:
self.info(f' Copying {file}..',)
shutil.copy(file, self.artifact_dir)
except Exception as e:
self.err(' Caught exception: ' + str(e))
self.process_crash()
# Propagate program's return code as our return code
self.info(f'Exiting with exit code {self.popen.returncode}')
sys.exit(self.popen.returncode)
def main(cls: Runner):
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--install', action='store_true',
help='Install crash handler on this machine')
parser.add_argument('-t', '--timeout', type=int,
default=Runner.TIMEOUT,
help='Max running time in seconds before terminated')
parser.add_argument('-o', '--output',
help='Output directory to copy artifacts (program and core dump)')
parser.add_argument('prog', help='Program to run', nargs='?')
parser.add_argument('args', nargs='*',
help='Arguments for the program (use -- to separate from cirunner\'s arguments)')
args = parser.parse_args()
kwargs = {}
kwargs['timeout'] = args.timeout
kwargs['artifact_dir'] = args.output
if args.install:
cls.install()
if args.prog:
ci_runner = cls(args.prog, args.args, **kwargs)
ci_runner.run()
# will not reach here