blob: ad2405df202e21fb61ac36f97c86d204031ca616 [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
103 def terminate(self):
104 if self.process_obj is None:
105 return
106 if self.result is not None:
107 return
108
109 while True:
110 # first try SIGINT to allow stdout+stderr flushing
111 self.log('Terminating (SIGINT)')
112 os.kill(self.process_obj.pid, signal.SIGINT)
113 self.killed = signal.SIGINT
114 if self._poll_termination():
115 break
116
117 # SIGTERM maybe?
118 self.log('Terminating (SIGTERM)')
119 self.process_obj.terminate()
120 self.killed = signal.SIGTERM
121 if self._poll_termination():
122 break
123
124 # out of patience
125 self.log('Terminating (SIGKILL)')
126 self.process_obj.kill()
127 self.killed = signal.SIGKILL
128 break;
129
130 self.process_obj.wait()
131 self.cleanup()
132
133 def cleanup(self):
Pau Espin Pedrol06ada452018-05-22 19:20:41 +0200134 self.dbg('Cleanup')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135 self.close_output_logs()
136 if self.result == 0:
137 self.log('Terminated: ok', rc=self.result)
138 elif self.killed:
139 self.log('Terminated', rc=self.result)
140 else:
141 self.err('Terminated: ERROR', rc=self.result)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200142 #self.log_stdout_tail()
143 self.log_stderr_tail()
144
145 def log_stdout_tail(self):
146 m = self.get_stdout_tail(prefix='| ')
147 if not m:
148 return
149 self.log('stdout:\n', m, '\n')
150
151 def log_stderr_tail(self):
152 m = self.get_stderr_tail(prefix='| ')
153 if not m:
154 return
155 self.log('stderr:\n', m, '\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156
157 def close_output_logs(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 for k, v in self.outputs.items():
159 path, f = v
160 if f:
161 f.flush()
162 f.close()
163 self.outputs[k] = (path, None)
164
165 def poll(self):
166 if self.process_obj is None:
167 return
168 if self.result is not None:
169 return
170 self.result = self.process_obj.poll()
171 if self.result is not None:
172 self.cleanup()
173
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200174 def is_running(self, poll_first=True):
175 if poll_first:
176 self.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200177 return self.process_obj is not None and self.result is None
178
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200179 def get_output(self, which):
180 v = self.outputs.get(which)
181 if not v:
182 return None
183 path, f = v
184 with open(path, 'r') as f2:
185 return f2.read()
186
187 def get_output_tail(self, which, tail=10, prefix=''):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200188 out = self.get_output(which)
189 if not out:
190 return None
191 out = out.splitlines()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200192 tail = min(len(out), tail)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200193 return prefix + ('\n' + prefix).join(out[-tail:])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200194
195 def get_stdout(self):
196 return self.get_output('stdout')
197
198 def get_stderr(self):
199 return self.get_output('stderr')
200
201 def get_stdout_tail(self, tail=10, prefix=''):
202 return self.get_output_tail('stdout', tail, prefix)
203
204 def get_stderr_tail(self, tail=10, prefix=''):
205 return self.get_output_tail('stderr', tail, prefix)
206
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200207 def terminated(self, poll_first=True):
208 if poll_first:
209 self.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210 return self.result is not None
211
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200212 def wait(self, timeout=300):
Pau Espin Pedrol9a4631c2018-03-28 19:17:34 +0200213 MainLoop.wait(self, self.terminated, timeout=timeout)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200214
215
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216class RemoteProcess(Process):
217
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200218 def __init__(self, name, run_dir, remote_user, remote_host, remote_cwd, popen_args, **popen_kwargs):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200219 super().__init__(name, run_dir, popen_args, **popen_kwargs)
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200220 self.remote_user = remote_user
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221 self.remote_host = remote_host
222 self.remote_cwd = remote_cwd
223
224 # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
225 # over the ssh link, we should probably run on the remote side,
226 # monitoring the process remotely.
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200227 if self.remote_cwd:
228 cd = 'cd "%s"; ' % self.remote_cwd
229 else:
230 cd = ''
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200231 self.popen_args = ['ssh', self.remote_user+'@'+self.remote_host,
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200232 '%s%s' % (cd,
233 ' '.join(self.popen_args))]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200235
236# vim: expandtab tabstop=4 shiftwidth=4