blob: a845f7faa0343442cb54c945d3652845ff7da4f8 [file] [log] [blame]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02001# osmo_gsm_tester: process management
2#
3# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Neels Hofmeyr <neels@hofmeyr.de>
6#
7# This program is free software: you can redistribute it and/or modify
Harald Welte27205342017-06-03 09:51:45 +02008# it under the terms of the GNU General Public License as
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02009# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Harald Welte27205342017-06-03 09:51:45 +020015# GNU General Public License for more details.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020016#
Harald Welte27205342017-06-03 09:51:45 +020017# You should have received a copy of the GNU General Public License
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020018# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
Neels Hofmeyr3531a192017-03-28 14:30:28 +020020import os
21import time
22import subprocess
23import signal
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020024from datetime import datetime
Neels Hofmeyr3531a192017-03-28 14:30:28 +020025
Pau Espin Pedrol9a4631c2018-03-28 19:17:34 +020026from . import log
27from .event_loop import MainLoop
Neels Hofmeyr3531a192017-03-28 14:30:28 +020028from .util import Dir
29
30class Process(log.Origin):
31
Neels Hofmeyr3531a192017-03-28 14:30:28 +020032 def __init__(self, name, run_dir, popen_args, **popen_kwargs):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020033 super().__init__(log.C_RUN, name)
Pau Espin Pedrol58603672018-08-09 13:45:55 +020034 self.process_obj = None
35 self.result = None
36 self.killed = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +020037 self.name_str = name
Neels Hofmeyr3531a192017-03-28 14:30:28 +020038 self.run_dir = run_dir
39 self.popen_args = popen_args
40 self.popen_kwargs = popen_kwargs
41 self.outputs = {}
42 if not isinstance(self.run_dir, Dir):
43 self.run_dir = Dir(os.path.abspath(str(self.run_dir)))
44
45 def set_env(self, key, value):
46 env = self.popen_kwargs.get('env') or {}
47 env[key] = value
48 self.popen_kwargs['env'] = env
49
50 def make_output_log(self, name):
51 '''
52 create a non-existing log output file in run_dir to pipe stdout and
53 stderr from this process to.
54 '''
55 path = self.run_dir.new_child(name)
56 f = open(path, 'w')
57 self.dbg(path)
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020058 f.write('(launched: %s)\n' % datetime.now().strftime(log.LONG_DATEFMT))
Neels Hofmeyr3531a192017-03-28 14:30:28 +020059 f.flush()
60 self.outputs[name] = (path, f)
61 return f
62
63 def launch(self):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020064 log.dbg('cd %r; %s %s' % (
65 os.path.abspath(str(self.run_dir)),
66 ' '.join(['%s=%r'%(k,v) for k,v in self.popen_kwargs.get('env', {}).items()]),
67 ' '.join(self.popen_args)))
Neels Hofmeyr3531a192017-03-28 14:30:28 +020068
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020069 self.process_obj = subprocess.Popen(
70 self.popen_args,
71 stdout=self.make_output_log('stdout'),
72 stderr=self.make_output_log('stderr'),
73 stdin=subprocess.PIPE,
74 shell=False,
75 cwd=self.run_dir.path,
76 **self.popen_kwargs)
77 self.set_name(self.name_str, pid=self.process_obj.pid)
78 self.log('Launched')
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079
Pau Espin Pedrolb1526b92018-05-22 20:32:30 +020080 def respawn(self):
81 self.dbg('respawn')
82 assert not self.is_running()
83 self.result = None
84 self.killed = None
85 self.launch()
86
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087 def _poll_termination(self, time_to_wait_for_term=5):
88 wait_step = 0.001
89 waited_time = 0
90 while True:
91 # poll returns None if proc is still running
92 self.result = self.process_obj.poll()
93 if self.result is not None:
94 return True
95 waited_time += wait_step
96 # make wait_step approach 1.0
97 wait_step = (1. + 5. * wait_step) / 6.
98 if waited_time >= time_to_wait_for_term:
99 break
100 time.sleep(wait_step)
101 return False
102
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200103 def send_signal(self, sig):
104 os.kill(self.process_obj.pid, sig)
105
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200106 def terminate(self):
107 if self.process_obj is None:
108 return
109 if self.result is not None:
110 return
111
112 while True:
113 # first try SIGINT to allow stdout+stderr flushing
114 self.log('Terminating (SIGINT)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200115 self.send_signal(signal.SIGINT)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200116 self.killed = signal.SIGINT
117 if self._poll_termination():
118 break
119
120 # SIGTERM maybe?
121 self.log('Terminating (SIGTERM)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200122 self.send_signal(signal.SIGTERM)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200123 self.killed = signal.SIGTERM
124 if self._poll_termination():
125 break
126
127 # out of patience
128 self.log('Terminating (SIGKILL)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200129 self.send_signal(signal.SIGKILL)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200130 self.killed = signal.SIGKILL
131 break;
132
133 self.process_obj.wait()
134 self.cleanup()
135
136 def cleanup(self):
Pau Espin Pedrol06ada452018-05-22 19:20:41 +0200137 self.dbg('Cleanup')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138 self.close_output_logs()
139 if self.result == 0:
140 self.log('Terminated: ok', rc=self.result)
141 elif self.killed:
142 self.log('Terminated', rc=self.result)
143 else:
144 self.err('Terminated: ERROR', rc=self.result)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200145 #self.log_stdout_tail()
146 self.log_stderr_tail()
147
148 def log_stdout_tail(self):
149 m = self.get_stdout_tail(prefix='| ')
150 if not m:
151 return
152 self.log('stdout:\n', m, '\n')
153
154 def log_stderr_tail(self):
155 m = self.get_stderr_tail(prefix='| ')
156 if not m:
157 return
158 self.log('stderr:\n', m, '\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200159
160 def close_output_logs(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161 for k, v in self.outputs.items():
162 path, f = v
163 if f:
164 f.flush()
165 f.close()
166 self.outputs[k] = (path, None)
167
168 def poll(self):
169 if self.process_obj is None:
170 return
171 if self.result is not None:
172 return
173 self.result = self.process_obj.poll()
174 if self.result is not None:
175 self.cleanup()
176
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200177 def is_running(self, poll_first=True):
178 if poll_first:
179 self.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200180 return self.process_obj is not None and self.result is None
181
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200182 def get_output(self, which):
183 v = self.outputs.get(which)
184 if not v:
185 return None
186 path, f = v
187 with open(path, 'r') as f2:
188 return f2.read()
189
190 def get_output_tail(self, which, tail=10, prefix=''):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200191 out = self.get_output(which)
192 if not out:
193 return None
194 out = out.splitlines()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195 tail = min(len(out), tail)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200196 return prefix + ('\n' + prefix).join(out[-tail:])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197
198 def get_stdout(self):
199 return self.get_output('stdout')
200
201 def get_stderr(self):
202 return self.get_output('stderr')
203
204 def get_stdout_tail(self, tail=10, prefix=''):
205 return self.get_output_tail('stdout', tail, prefix)
206
207 def get_stderr_tail(self, tail=10, prefix=''):
208 return self.get_output_tail('stderr', tail, prefix)
209
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200210 def terminated(self, poll_first=True):
211 if poll_first:
212 self.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213 return self.result is not None
214
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200215 def wait(self, timeout=300):
Pau Espin Pedrol9a4631c2018-03-28 19:17:34 +0200216 MainLoop.wait(self, self.terminated, timeout=timeout)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200217
218
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219class RemoteProcess(Process):
220
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200221 def __init__(self, name, run_dir, remote_user, remote_host, remote_cwd, popen_args, **popen_kwargs):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200222 super().__init__(name, run_dir, popen_args, **popen_kwargs)
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200223 self.remote_user = remote_user
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224 self.remote_host = remote_host
225 self.remote_cwd = remote_cwd
226
227 # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
228 # over the ssh link, we should probably run on the remote side,
229 # monitoring the process remotely.
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200230 if self.remote_cwd:
231 cd = 'cd "%s"; ' % self.remote_cwd
232 else:
233 cd = ''
Pau Espin Pedrol302c7562018-10-02 13:08:02 +0200234 # We need double -t to force tty and be able to forward signals to
235 # processes (SIGHUP) when we close ssh on the local side. As a result,
236 # stderr seems to be merged into stdout in ssh client.
237 self.popen_args = ['ssh', '-t', '-t', self.remote_user+'@'+self.remote_host,
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200238 '%s%s' % (cd,
239 ' '.join(self.popen_args))]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200241
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200242class NetNSProcess(Process):
243 NETNS_EXEC_BIN = 'osmo-gsm-tester_netns_exec.sh'
244 def __init__(self, name, run_dir, netns, popen_args, **popen_kwargs):
245 super().__init__(name, run_dir, popen_args, **popen_kwargs)
246 self.netns = netns
247
248 self.popen_args = ['sudo', self.NETNS_EXEC_BIN, self.netns] + list(popen_args)
249 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
250
251 # HACK: Since we run under sudo, only way to kill root-owned process is to kill as root...
252 # This function is overwritten from Process.
253 def send_signal(self, sig):
254 kill_cmd = ('kill', '-%d' % int(sig), str(self.process_obj.pid))
255 run_local_netns_sync(self.run_dir, self.name()+"-kill", self.netns, kill_cmd)
256
257
Pau Espin Pedrol8aca1f32018-10-25 18:31:50 +0200258def run_proc_sync(proc):
259 try:
260 proc.launch()
261 proc.wait()
262 except Exception as e:
263 proc.terminate()
264 raise e
265 if proc.result != 0:
266 log.ctx(proc)
267 raise log.Error('Exited in error')
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200268
269def run_local_sync(run_dir, name, popen_args):
270 run_dir =run_dir.new_dir(name)
271 proc = Process(name, run_dir, popen_args)
Pau Espin Pedrol8aca1f32018-10-25 18:31:50 +0200272 run_proc_sync(proc)
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200273
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200274def run_local_netns_sync(run_dir, name, netns, popen_args):
275 run_dir =run_dir.new_dir(name)
276 proc = NetNSProcess(name, run_dir, netns, popen_args)
277 run_proc_sync(proc)
278
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200279def run_remote_sync(run_dir, remote_user, remote_addr, name, popen_args, remote_cwd=None):
280 run_dir = run_dir.new_dir(name)
Pau Espin Pedrol8aca1f32018-10-25 18:31:50 +0200281 proc = RemoteProcess(name, run_dir, remote_user, remote_addr, remote_cwd, popen_args)
282 run_proc_sync(proc)
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200283
284def scp(run_dir, remote_user, remote_addr, name, local_path, remote_path):
285 run_local_sync(run_dir, name, ('scp', '-r', local_path, '%s@%s:%s' % (remote_user, remote_addr, remote_path)))
286
287def copy_inst_ssh(run_dir, inst, remote_dir, remote_user, remote_addr, remote_rundir_append, cfg_file_name):
288 remote_inst = Dir(remote_dir.child(os.path.basename(str(inst))))
289 remote_dir_str = str(remote_dir)
290 run_remote_sync(run_dir, remote_user, remote_addr, 'rm-remote-dir', ('test', '!', '-d', remote_dir_str, '||', 'rm', '-rf', remote_dir_str))
291 run_remote_sync(run_dir, remote_user, remote_addr, 'mk-remote-dir', ('mkdir', '-p', remote_dir_str))
292 scp(run_dir, remote_user, remote_addr, 'scp-inst-to-remote', str(inst), remote_dir_str)
293
294 remote_run_dir = remote_dir.child(remote_rundir_append)
295 run_remote_sync(run_dir, remote_user, remote_addr, 'mk-remote-run-dir', ('mkdir', '-p', remote_run_dir))
296
297 remote_config_file = remote_dir.child(os.path.basename(cfg_file_name))
298 scp(run_dir, remote_user, remote_addr, 'scp-cfg-to-remote', cfg_file_name, remote_config_file)
299 return remote_inst
300
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200301# vim: expandtab tabstop=4 shiftwidth=4