blob: c13ded0f83133782b5f648aaa3e67ffa16f9353c [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
32 process_obj = None
33 outputs = None
34 result = None
35 killed = None
36
37 def __init__(self, name, run_dir, popen_args, **popen_kwargs):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020038 super().__init__(log.C_RUN, name)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020039 self.name_str = name
Neels Hofmeyr3531a192017-03-28 14:30:28 +020040 self.run_dir = run_dir
41 self.popen_args = popen_args
42 self.popen_kwargs = popen_kwargs
43 self.outputs = {}
44 if not isinstance(self.run_dir, Dir):
45 self.run_dir = Dir(os.path.abspath(str(self.run_dir)))
46
47 def set_env(self, key, value):
48 env = self.popen_kwargs.get('env') or {}
49 env[key] = value
50 self.popen_kwargs['env'] = env
51
52 def make_output_log(self, name):
53 '''
54 create a non-existing log output file in run_dir to pipe stdout and
55 stderr from this process to.
56 '''
57 path = self.run_dir.new_child(name)
58 f = open(path, 'w')
59 self.dbg(path)
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020060 f.write('(launched: %s)\n' % datetime.now().strftime(log.LONG_DATEFMT))
Neels Hofmeyr3531a192017-03-28 14:30:28 +020061 f.flush()
62 self.outputs[name] = (path, f)
63 return f
64
65 def launch(self):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020066 log.dbg('cd %r; %s %s' % (
67 os.path.abspath(str(self.run_dir)),
68 ' '.join(['%s=%r'%(k,v) for k,v in self.popen_kwargs.get('env', {}).items()]),
69 ' '.join(self.popen_args)))
Neels Hofmeyr3531a192017-03-28 14:30:28 +020070
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020071 self.process_obj = subprocess.Popen(
72 self.popen_args,
73 stdout=self.make_output_log('stdout'),
74 stderr=self.make_output_log('stderr'),
75 stdin=subprocess.PIPE,
76 shell=False,
77 cwd=self.run_dir.path,
78 **self.popen_kwargs)
79 self.set_name(self.name_str, pid=self.process_obj.pid)
80 self.log('Launched')
Neels Hofmeyr3531a192017-03-28 14:30:28 +020081
Pau Espin Pedrolb1526b92018-05-22 20:32:30 +020082 def respawn(self):
83 self.dbg('respawn')
84 assert not self.is_running()
85 self.result = None
86 self.killed = None
87 self.launch()
88
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 def _poll_termination(self, time_to_wait_for_term=5):
90 wait_step = 0.001
91 waited_time = 0
92 while True:
93 # poll returns None if proc is still running
94 self.result = self.process_obj.poll()
95 if self.result is not None:
96 return True
97 waited_time += wait_step
98 # make wait_step approach 1.0
99 wait_step = (1. + 5. * wait_step) / 6.
100 if waited_time >= time_to_wait_for_term:
101 break
102 time.sleep(wait_step)
103 return False
104
105 def terminate(self):
106 if self.process_obj is None:
107 return
108 if self.result is not None:
109 return
110
111 while True:
112 # first try SIGINT to allow stdout+stderr flushing
113 self.log('Terminating (SIGINT)')
114 os.kill(self.process_obj.pid, signal.SIGINT)
115 self.killed = signal.SIGINT
116 if self._poll_termination():
117 break
118
119 # SIGTERM maybe?
120 self.log('Terminating (SIGTERM)')
121 self.process_obj.terminate()
122 self.killed = signal.SIGTERM
123 if self._poll_termination():
124 break
125
126 # out of patience
127 self.log('Terminating (SIGKILL)')
128 self.process_obj.kill()
129 self.killed = signal.SIGKILL
130 break;
131
132 self.process_obj.wait()
133 self.cleanup()
134
135 def cleanup(self):
Pau Espin Pedrol06ada452018-05-22 19:20:41 +0200136 self.dbg('Cleanup')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 self.close_output_logs()
138 if self.result == 0:
139 self.log('Terminated: ok', rc=self.result)
140 elif self.killed:
141 self.log('Terminated', rc=self.result)
142 else:
143 self.err('Terminated: ERROR', rc=self.result)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200144 #self.log_stdout_tail()
145 self.log_stderr_tail()
146
147 def log_stdout_tail(self):
148 m = self.get_stdout_tail(prefix='| ')
149 if not m:
150 return
151 self.log('stdout:\n', m, '\n')
152
153 def log_stderr_tail(self):
154 m = self.get_stderr_tail(prefix='| ')
155 if not m:
156 return
157 self.log('stderr:\n', m, '\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158
159 def close_output_logs(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200160 for k, v in self.outputs.items():
161 path, f = v
162 if f:
163 f.flush()
164 f.close()
165 self.outputs[k] = (path, None)
166
167 def poll(self):
168 if self.process_obj is None:
169 return
170 if self.result is not None:
171 return
172 self.result = self.process_obj.poll()
173 if self.result is not None:
174 self.cleanup()
175
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200176 def is_running(self, poll_first=True):
177 if poll_first:
178 self.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200179 return self.process_obj is not None and self.result is None
180
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200181 def get_output(self, which):
182 v = self.outputs.get(which)
183 if not v:
184 return None
185 path, f = v
186 with open(path, 'r') as f2:
187 return f2.read()
188
189 def get_output_tail(self, which, tail=10, prefix=''):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200190 out = self.get_output(which)
191 if not out:
192 return None
193 out = out.splitlines()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200194 tail = min(len(out), tail)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200195 return prefix + ('\n' + prefix).join(out[-tail:])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200196
197 def get_stdout(self):
198 return self.get_output('stdout')
199
200 def get_stderr(self):
201 return self.get_output('stderr')
202
203 def get_stdout_tail(self, tail=10, prefix=''):
204 return self.get_output_tail('stdout', tail, prefix)
205
206 def get_stderr_tail(self, tail=10, prefix=''):
207 return self.get_output_tail('stderr', tail, prefix)
208
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200209 def terminated(self, poll_first=True):
210 if poll_first:
211 self.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212 return self.result is not None
213
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200214 def wait(self, timeout=300):
Pau Espin Pedrol9a4631c2018-03-28 19:17:34 +0200215 MainLoop.wait(self, self.terminated, timeout=timeout)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200216
217
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200218class RemoteProcess(Process):
219
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200220 def __init__(self, name, run_dir, remote_user, remote_host, remote_cwd, popen_args, **popen_kwargs):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200221 super().__init__(name, run_dir, popen_args, **popen_kwargs)
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200222 self.remote_user = remote_user
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200223 self.remote_host = remote_host
224 self.remote_cwd = remote_cwd
225
226 # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
227 # over the ssh link, we should probably run on the remote side,
228 # monitoring the process remotely.
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200229 if self.remote_cwd:
230 cd = 'cd "%s"; ' % self.remote_cwd
231 else:
232 cd = ''
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200233 self.popen_args = ['ssh', self.remote_user+'@'+self.remote_host,
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200234 '%s%s' % (cd,
235 ' '.join(self.popen_args))]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200236 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200237
238# vim: expandtab tabstop=4 shiftwidth=4