blob: a687de6a627fada3a967f006cff5955f9260874a [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
8# it under the terms of the GNU Affero General Public License as
9# 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
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# 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
24
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +020025from . import log, test
Neels Hofmeyr3531a192017-03-28 14:30:28 +020026from .util import Dir
27
28class Process(log.Origin):
29
30 process_obj = None
31 outputs = None
32 result = None
33 killed = None
34
35 def __init__(self, name, run_dir, popen_args, **popen_kwargs):
36 self.name_str = name
37 self.set_name(name)
38 self.set_log_category(log.C_RUN)
39 self.run_dir = run_dir
40 self.popen_args = popen_args
41 self.popen_kwargs = popen_kwargs
42 self.outputs = {}
43 if not isinstance(self.run_dir, Dir):
44 self.run_dir = Dir(os.path.abspath(str(self.run_dir)))
45
46 def set_env(self, key, value):
47 env = self.popen_kwargs.get('env') or {}
48 env[key] = value
49 self.popen_kwargs['env'] = env
50
51 def make_output_log(self, name):
52 '''
53 create a non-existing log output file in run_dir to pipe stdout and
54 stderr from this process to.
55 '''
56 path = self.run_dir.new_child(name)
57 f = open(path, 'w')
58 self.dbg(path)
59 f.write('(launched: %s)\n' % time.strftime(log.LONG_DATEFMT))
60 f.flush()
61 self.outputs[name] = (path, f)
62 return f
63
64 def launch(self):
65 with self:
66
67 self.dbg('cd %r; %s %s' % (
68 os.path.abspath(str(self.run_dir)),
69 ' '.join(['%s=%r'%(k,v) for k,v in self.popen_kwargs.get('env', {}).items()]),
70 ' '.join(self.popen_args)))
71
72 self.process_obj = subprocess.Popen(
73 self.popen_args,
74 stdout=self.make_output_log('stdout'),
75 stderr=self.make_output_log('stderr'),
Neels Hofmeyracf0c932017-05-06 16:05:33 +020076 stdin=subprocess.PIPE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 shell=False,
78 cwd=self.run_dir.path,
79 **self.popen_kwargs)
80 self.set_name(self.name_str, pid=self.process_obj.pid)
81 self.log('Launched')
82
83 def _poll_termination(self, time_to_wait_for_term=5):
84 wait_step = 0.001
85 waited_time = 0
86 while True:
87 # poll returns None if proc is still running
88 self.result = self.process_obj.poll()
89 if self.result is not None:
90 return True
91 waited_time += wait_step
92 # make wait_step approach 1.0
93 wait_step = (1. + 5. * wait_step) / 6.
94 if waited_time >= time_to_wait_for_term:
95 break
96 time.sleep(wait_step)
97 return False
98
99 def terminate(self):
100 if self.process_obj is None:
101 return
102 if self.result is not None:
103 return
104
105 while True:
106 # first try SIGINT to allow stdout+stderr flushing
107 self.log('Terminating (SIGINT)')
108 os.kill(self.process_obj.pid, signal.SIGINT)
109 self.killed = signal.SIGINT
110 if self._poll_termination():
111 break
112
113 # SIGTERM maybe?
114 self.log('Terminating (SIGTERM)')
115 self.process_obj.terminate()
116 self.killed = signal.SIGTERM
117 if self._poll_termination():
118 break
119
120 # out of patience
121 self.log('Terminating (SIGKILL)')
122 self.process_obj.kill()
123 self.killed = signal.SIGKILL
124 break;
125
126 self.process_obj.wait()
127 self.cleanup()
128
129 def cleanup(self):
130 self.close_output_logs()
131 if self.result == 0:
132 self.log('Terminated: ok', rc=self.result)
133 elif self.killed:
134 self.log('Terminated', rc=self.result)
135 else:
136 self.err('Terminated: ERROR', rc=self.result)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200137 #self.log_stdout_tail()
138 self.log_stderr_tail()
139
140 def log_stdout_tail(self):
141 m = self.get_stdout_tail(prefix='| ')
142 if not m:
143 return
144 self.log('stdout:\n', m, '\n')
145
146 def log_stderr_tail(self):
147 m = self.get_stderr_tail(prefix='| ')
148 if not m:
149 return
150 self.log('stderr:\n', m, '\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200151
152 def close_output_logs(self):
153 self.dbg('Cleanup')
154 for k, v in self.outputs.items():
155 path, f = v
156 if f:
157 f.flush()
158 f.close()
159 self.outputs[k] = (path, None)
160
161 def poll(self):
162 if self.process_obj is None:
163 return
164 if self.result is not None:
165 return
166 self.result = self.process_obj.poll()
167 if self.result is not None:
168 self.cleanup()
169
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200170 def is_running(self, poll_first=True):
171 if poll_first:
172 self.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200173 return self.process_obj is not None and self.result is None
174
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175 def get_output(self, which):
176 v = self.outputs.get(which)
177 if not v:
178 return None
179 path, f = v
180 with open(path, 'r') as f2:
181 return f2.read()
182
183 def get_output_tail(self, which, tail=10, prefix=''):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200184 out = self.get_output(which)
185 if not out:
186 return None
187 out = out.splitlines()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188 tail = min(len(out), tail)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200189 return prefix + ('\n' + prefix).join(out[-tail:])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190
191 def get_stdout(self):
192 return self.get_output('stdout')
193
194 def get_stderr(self):
195 return self.get_output('stderr')
196
197 def get_stdout_tail(self, tail=10, prefix=''):
198 return self.get_output_tail('stdout', tail, prefix)
199
200 def get_stderr_tail(self, tail=10, prefix=''):
201 return self.get_output_tail('stderr', tail, prefix)
202
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200203 def terminated(self, poll_first=True):
204 if poll_first:
205 self.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206 return self.result is not None
207
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200208 def wait(self, timeout=300):
209 test.wait(self.terminated, timeout=timeout)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200210
211
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212class RemoteProcess(Process):
213
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200214 def __init__(self, name, run_dir, remote_user, remote_host, remote_cwd, popen_args, **popen_kwargs):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200215 super().__init__(name, run_dir, popen_args, **popen_kwargs)
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200216 self.remote_user = remote_user
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217 self.remote_host = remote_host
218 self.remote_cwd = remote_cwd
219
220 # hacky: instead of just prepending ssh, i.e. piping stdout and stderr
221 # over the ssh link, we should probably run on the remote side,
222 # monitoring the process remotely.
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200223 if self.remote_cwd:
224 cd = 'cd "%s"; ' % self.remote_cwd
225 else:
226 cd = ''
Pau Espin Pedrol3895fec2017-04-28 16:13:03 +0200227 self.popen_args = ['ssh', self.remote_user+'@'+self.remote_host,
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200228 '%s%s' % (cd,
229 ' '.join(self.popen_args))]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 self.dbg(self.popen_args, dir=self.run_dir, conf=self.popen_kwargs)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200231
232# vim: expandtab tabstop=4 shiftwidth=4