blob: 40d0cc1b91fa196e1b2d908301abae47caba41d9 [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 Pedrol79df7392018-11-12 18:15:30 +010080 def launch_sync(self):
81 '''
82 calls launch() method and block waiting for it to finish, serving the
83 mainloop meanwhile.
84 '''
85 try:
86 self.launch()
87 self.wait()
88 except Exception as e:
89 self.terminate()
90 raise e
91 if self.result != 0:
92 log.ctx(self)
93 raise log.Error('Exited in error')
94
Pau Espin Pedrolb1526b92018-05-22 20:32:30 +020095 def respawn(self):
96 self.dbg('respawn')
97 assert not self.is_running()
98 self.result = None
99 self.killed = None
100 self.launch()
101
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200102 def _poll_termination(self, time_to_wait_for_term=5):
103 wait_step = 0.001
104 waited_time = 0
105 while True:
106 # poll returns None if proc is still running
107 self.result = self.process_obj.poll()
108 if self.result is not None:
109 return True
110 waited_time += wait_step
111 # make wait_step approach 1.0
112 wait_step = (1. + 5. * wait_step) / 6.
113 if waited_time >= time_to_wait_for_term:
114 break
115 time.sleep(wait_step)
116 return False
117
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200118 def send_signal(self, sig):
119 os.kill(self.process_obj.pid, sig)
120
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 def terminate(self):
122 if self.process_obj is None:
123 return
124 if self.result is not None:
125 return
126
127 while True:
128 # first try SIGINT to allow stdout+stderr flushing
129 self.log('Terminating (SIGINT)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200130 self.send_signal(signal.SIGINT)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 self.killed = signal.SIGINT
132 if self._poll_termination():
133 break
134
135 # SIGTERM maybe?
136 self.log('Terminating (SIGTERM)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200137 self.send_signal(signal.SIGTERM)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138 self.killed = signal.SIGTERM
139 if self._poll_termination():
140 break
141
142 # out of patience
143 self.log('Terminating (SIGKILL)')
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200144 self.send_signal(signal.SIGKILL)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145 self.killed = signal.SIGKILL
146 break;
147
148 self.process_obj.wait()
149 self.cleanup()
150
151 def cleanup(self):
Pau Espin Pedrol06ada452018-05-22 19:20:41 +0200152 self.dbg('Cleanup')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153 self.close_output_logs()
154 if self.result == 0:
155 self.log('Terminated: ok', rc=self.result)
156 elif self.killed:
157 self.log('Terminated', rc=self.result)
158 else:
159 self.err('Terminated: ERROR', rc=self.result)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200160 #self.log_stdout_tail()
161 self.log_stderr_tail()
162
163 def log_stdout_tail(self):
164 m = self.get_stdout_tail(prefix='| ')
165 if not m:
166 return
167 self.log('stdout:\n', m, '\n')
168
169 def log_stderr_tail(self):
170 m = self.get_stderr_tail(prefix='| ')
171 if not m:
172 return
173 self.log('stderr:\n', m, '\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174
175 def close_output_logs(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176 for k, v in self.outputs.items():
177 path, f = v
178 if f:
179 f.flush()
180 f.close()
181 self.outputs[k] = (path, None)
182
183 def poll(self):
184 if self.process_obj is None:
185 return
186 if self.result is not None:
187 return
188 self.result = self.process_obj.poll()
189 if self.result is not None:
190 self.cleanup()
191
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200192 def is_running(self, poll_first=True):
193 if poll_first:
194 self.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200195 return self.process_obj is not None and self.result is None
196
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 def get_output(self, which):
198 v = self.outputs.get(which)
199 if not v:
200 return None
201 path, f = v
202 with open(path, 'r') as f2:
203 return f2.read()
204
205 def get_output_tail(self, which, tail=10, prefix=''):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200206 out = self.get_output(which)
207 if not out:
208 return None
209 out = out.splitlines()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210 tail = min(len(out), tail)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200211 return prefix + ('\n' + prefix).join(out[-tail:])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212
213 def get_stdout(self):
214 return self.get_output('stdout')
215
216 def get_stderr(self):
217 return self.get_output('stderr')
218
219 def get_stdout_tail(self, tail=10, prefix=''):
220 return self.get_output_tail('stdout', tail, prefix)
221
222 def get_stderr_tail(self, tail=10, prefix=''):
223 return self.get_output_tail('stderr', tail, prefix)
224
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200225 def terminated(self, poll_first=True):
226 if poll_first:
227 self.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228 return self.result is not None
229
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200230 def wait(self, timeout=300):
Pau Espin Pedrol9a4631c2018-03-28 19:17:34 +0200231 MainLoop.wait(self, self.terminated, timeout=timeout)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232
233
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234class RemoteProcess(Process):
235
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200236 def __init__(self, name, run_dir, remote_user, remote_host, remote_cwd, popen_args, **popen_kwargs):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200237 super().__init__(name, run_dir, popen_args, **popen_kwargs)
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200238 self.remote_user = remote_user
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200239 self.remote_host = remote_host
240 self.remote_cwd = remote_cwd
241
242 # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
243 # over the ssh link, we should probably run on the remote side,
244 # monitoring the process remotely.
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200245 if self.remote_cwd:
246 cd = 'cd "%s"; ' % self.remote_cwd
247 else:
248 cd = ''
Pau Espin Pedrol302c7562018-10-02 13:08:02 +0200249 # We need double -t to force tty and be able to forward signals to
250 # processes (SIGHUP) when we close ssh on the local side. As a result,
251 # stderr seems to be merged into stdout in ssh client.
252 self.popen_args = ['ssh', '-t', '-t', self.remote_user+'@'+self.remote_host,
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200253 '%s%s' % (cd,
254 ' '.join(self.popen_args))]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200256
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200257class NetNSProcess(Process):
258 NETNS_EXEC_BIN = 'osmo-gsm-tester_netns_exec.sh'
259 def __init__(self, name, run_dir, netns, popen_args, **popen_kwargs):
260 super().__init__(name, run_dir, popen_args, **popen_kwargs)
261 self.netns = netns
262
263 self.popen_args = ['sudo', self.NETNS_EXEC_BIN, self.netns] + list(popen_args)
264 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
265
266 # HACK: Since we run under sudo, only way to kill root-owned process is to kill as root...
267 # This function is overwritten from Process.
268 def send_signal(self, sig):
269 kill_cmd = ('kill', '-%d' % int(sig), str(self.process_obj.pid))
270 run_local_netns_sync(self.run_dir, self.name()+"-kill", self.netns, kill_cmd)
271
272
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200273def run_local_sync(run_dir, name, popen_args):
274 run_dir =run_dir.new_dir(name)
275 proc = Process(name, run_dir, popen_args)
Pau Espin Pedrol79df7392018-11-12 18:15:30 +0100276 proc.launch_sync()
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200277
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200278def run_local_netns_sync(run_dir, name, netns, popen_args):
279 run_dir =run_dir.new_dir(name)
280 proc = NetNSProcess(name, run_dir, netns, popen_args)
Pau Espin Pedrol79df7392018-11-12 18:15:30 +0100281 proc.launch_sync()
Pau Espin Pedrolfd4c1442018-10-25 17:37:23 +0200282
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200283def run_remote_sync(run_dir, remote_user, remote_addr, name, popen_args, remote_cwd=None):
284 run_dir = run_dir.new_dir(name)
Pau Espin Pedrol8aca1f32018-10-25 18:31:50 +0200285 proc = RemoteProcess(name, run_dir, remote_user, remote_addr, remote_cwd, popen_args)
Pau Espin Pedrol79df7392018-11-12 18:15:30 +0100286 proc.launch_sync()
Pau Espin Pedrole4358a92018-10-01 11:27:55 +0200287
288def scp(run_dir, remote_user, remote_addr, name, local_path, remote_path):
289 run_local_sync(run_dir, name, ('scp', '-r', local_path, '%s@%s:%s' % (remote_user, remote_addr, remote_path)))
290
291def copy_inst_ssh(run_dir, inst, remote_dir, remote_user, remote_addr, remote_rundir_append, cfg_file_name):
292 remote_inst = Dir(remote_dir.child(os.path.basename(str(inst))))
293 remote_dir_str = str(remote_dir)
294 run_remote_sync(run_dir, remote_user, remote_addr, 'rm-remote-dir', ('test', '!', '-d', remote_dir_str, '||', 'rm', '-rf', remote_dir_str))
295 run_remote_sync(run_dir, remote_user, remote_addr, 'mk-remote-dir', ('mkdir', '-p', remote_dir_str))
296 scp(run_dir, remote_user, remote_addr, 'scp-inst-to-remote', str(inst), remote_dir_str)
297
298 remote_run_dir = remote_dir.child(remote_rundir_append)
299 run_remote_sync(run_dir, remote_user, remote_addr, 'mk-remote-run-dir', ('mkdir', '-p', remote_run_dir))
300
301 remote_config_file = remote_dir.child(os.path.basename(cfg_file_name))
302 scp(run_dir, remote_user, remote_addr, 'scp-cfg-to-remote', cfg_file_name, remote_config_file)
303 return remote_inst
304
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200305# vim: expandtab tabstop=4 shiftwidth=4