1 # Copyright 2015-2017 Intel Corporation.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 """Task management helper functions and classes.
28 from conf import settings
29 from tools import systeminfo
35 """Get stdout value for ``subprocess`` calls.
39 if settings.getValue('VERBOSITY') != 'debug':
40 stdout = open(os.devnull, 'wb')
45 def run_task(cmd, logger, msg=None, check_error=False):
46 """Run task, report errors and log overall status.
48 Run given task using ``subprocess.Popen``. Log the commands
49 used and any errors generated. Prints stdout to screen if
50 in verbose mode and returns it regardless. Prints stderr to
53 :param cmd: Exact command to be executed
54 :param logger: Logger to write details to
55 :param msg: Message to be shown to user
56 :param check_error: Throw exception on error
58 :returns: (stdout, stderr)
60 def handle_error(exception):
61 """Handle errors by logging and optionally raising an exception.
64 'Unable to execute %(cmd)s. Exception: %(exception)s',
65 {'cmd': ' '.join(cmd), 'exception': exception})
71 my_encoding = locale.getdefaultlocale()[1]
76 # pylint: disable=too-many-nested-blocks
77 logger.debug('%s%s', CMD_PREFIX, ' '.join(cmd))
79 proc = subprocess.Popen(map(os.path.expanduser, cmd),
80 stdout=subprocess.PIPE,
81 stderr=subprocess.PIPE, bufsize=0)
84 reads = [proc.stdout.fileno(), proc.stderr.fileno()]
85 ret = select.select(reads, [], [])
88 if file_d == proc.stdout.fileno():
90 line = proc.stdout.readline()
93 if settings.getValue('VERBOSITY') == 'debug':
94 sys.stdout.write(line.decode(my_encoding))
96 if file_d == proc.stderr.fileno():
98 line = proc.stderr.readline()
101 sys.stderr.write(line.decode(my_encoding))
104 if proc.poll() is not None:
107 except OSError as ex:
111 ex = subprocess.CalledProcessError(proc.returncode, cmd, stderr)
114 return ('\n'.join(sout.decode(my_encoding).strip() for sout in stdout),
115 ('\n'.join(sout.decode(my_encoding).strip() for sout in stderr)))
117 def update_pids(pid):
118 """update list of running pids, so they can be terminated at the end
121 pids = settings.getValue('_EXECUTED_PIDS')
123 except AttributeError:
125 settings.setValue('_EXECUTED_PIDS', pids)
127 def run_background_task(cmd, logger, msg):
128 """Run task in background and log when started.
130 Run given task using ``subprocess.Popen``. Log the command
131 used. Print stdout to screen if in verbose mode. Prints stderr
134 :param cmd: Exact command to be executed
135 :param logger: Logger to write details to
136 :param msg: Message to be shown to user
138 :returns: Process PID
141 logger.debug('%s%s', CMD_PREFIX, ' '.join(cmd))
143 proc = subprocess.Popen(map(os.path.expanduser, cmd), stdout=_get_stdout(), bufsize=0)
145 update_pids(proc.pid)
150 def run_interactive_task(cmd, logger, msg):
151 """Run a task interactively and log when started.
153 Run given task using ``pexpect.spawn``. Log the command used.
154 Performs neither validation of the process - if the process
155 successfully started or is still running - nor killing of the
156 process. The user must do both.
158 :param cmd: Exact command to be executed
159 :param logger: Logger to write details to
160 :param msg: Message to be shown to user
162 :returns: ``pexpect.child`` object
165 logger.debug('%s%s', CMD_PREFIX, cmd)
166 child = pexpect.spawnu(cmd)
168 if settings.getValue('VERBOSITY') == 'debug':
169 child.logfile_read = sys.stdout
173 def terminate_task_subtree(pid, signal='-15', sleep=10, logger=None):
174 """Terminate given process and all its children
176 Function will sent given signal to the process. In case
177 that process will not terminate within given sleep interval
178 and signal was not SIGKILL, then process will be killed by SIGKILL.
179 After that function will check if all children of the process
180 are terminated and if not the same terminating procedure is applied
181 on any living child (only one level of children is considered).
183 :param pid: Process ID to terminate
184 :param signal: Signal to be sent to the process
185 :param sleep: Maximum delay in seconds after signal is sent
186 :param logger: Logger to write details to
189 children = subprocess.check_output("pgrep -P " + str(pid), shell=True).decode().rstrip('\n').split()
190 except subprocess.CalledProcessError:
193 terminate_task(pid, signal, sleep, logger)
195 # just for case children were kept alive
196 for child in children:
197 terminate_task(child, signal, sleep, logger)
199 def terminate_task(pid, signal='-15', sleep=10, logger=None):
200 """Terminate process with given pid
202 Function will sent given signal to the process. In case
203 that process will not terminate within given sleep interval
204 and signal was not SIGKILL, then process will be killed by SIGKILL.
206 :param pid: Process ID to terminate
207 :param signal: Signal to be sent to the process
208 :param sleep: Maximum delay in seconds after signal is sent
209 :param logger: Logger to write details to
211 if systeminfo.pid_isalive(pid):
212 run_task(['sudo', 'kill', signal, str(pid)], logger)
213 logger.debug('Wait for process %s to terminate after signal %s', pid, signal)
214 for dummy in range(sleep):
216 if not systeminfo.pid_isalive(pid):
219 if signal.lstrip('-').upper() not in ('9', 'KILL', 'SIGKILL') and systeminfo.pid_isalive(pid):
220 terminate_task(pid, '-9', sleep, logger)
222 pids = settings.getValue('_EXECUTED_PIDS')
225 settings.setValue('_EXECUTED_PIDS', pids)
227 def terminate_all_tasks(logger):
228 """Terminate all processes executed by vsperf, just for case they were not
229 terminated by standard means.
231 pids = settings.getValue('_EXECUTED_PIDS')
233 logger.debug('Following processes will be terminated: %s', pids)
235 terminate_task_subtree(pid, logger=logger)
236 settings.setValue('_EXECUTED_PIDS', [])
238 class Process(object):
239 """Control an instance of a long-running process.
241 This is basically a context-manager wrapper around the
242 ``run_interactive_task`` function above (with some extra helper
248 _logger = logging.getLogger(__name__)
251 _proc_name = 'unnamed process'
252 _relinquish_thread = None
257 """Start process instance using context manager.
262 def __exit__(self, type_, value, traceback):
263 """Shutdown process instance.
270 """Start process instance.
272 self._start_process()
273 if self._timeout > 0:
274 self._expect_process()
276 def _start_process(self):
277 """Start process instance.
279 cmd = ' '.join(settings.getValue('SHELL_CMD') +
280 ['"%s"' % ' '.join(self._cmd)])
282 self._child = run_interactive_task(cmd, self._logger,
283 'Starting %s...' % self._proc_name)
284 self._child.logfile = open(self._logfile, 'w')
286 def expect(self, msg, timeout=None):
287 """Expect string from process.
289 Expect string and die if not received.
291 :param msg: String to expect.
292 :param timeout: Time to wait for string.
296 self._expect_process(msg, timeout)
298 def _expect_process(self, msg=None, timeout=None):
299 """Expect string from process.
304 timeout = self._timeout
306 # we use exceptions rather than catching conditions in ``expect`` list
307 # as we want to fail catastrophically after handling; there is likely
308 # little we can do from within the scripts to fix issues such as
309 # hugepages not being mounted
311 self._child.expect([msg], timeout=timeout)
312 except pexpect.EOF as exc:
313 self._logger.critical(
314 'An error occurred. Please check the logs (%s) for more'
315 ' information. Exiting...', self._logfile)
317 except pexpect.TIMEOUT as exc:
318 self._logger.critical(
319 'Failed to execute in \'%d\' seconds. Please check the logs'
320 ' (%s) for more information. Exiting...',
321 timeout, self._logfile)
324 except (Exception, KeyboardInterrupt) as exc:
325 self._logger.critical('General exception raised. Exiting...')
329 def kill(self, signal='-15', sleep=10):
330 """Kill process instance if it is alive.
332 :param signal: signal to be sent to the process
333 :param sleep: delay in seconds after signal is sent
335 if self.is_running():
336 terminate_task_subtree(self._child.pid, signal, sleep, self._logger)
338 if self.is_relinquished():
339 self._relinquish_thread.join()
342 'Log available at %s', self._logfile)
344 def is_relinquished(self):
345 """Returns True if process is relinquished.
347 If relinquished the process is no longer controllable and can
350 :returns: True if process is relinquished, else False.
352 return self._relinquish_thread
354 def is_running(self):
355 """Returns True if process is running.
357 :returns: True if process is running, else False
359 return self._child and self._child.isalive()
361 def _affinitize_pid(self, core, pid):
362 """Affinitize a process with ``pid`` to ``core``.
364 :param core: Core to affinitize process to.
365 :param pid: Process ID to affinitize.
369 run_task(['sudo', 'taskset', '-c', '-p', str(core),
373 def affinitize(self, core):
374 """Affinitize process to a specific ``core``.
376 :param core: Core to affinitize process to.
380 self._logger.info('Affinitizing process')
382 if self.is_running():
383 self._affinitize_pid(core, self._child.pid)
385 class ContinueReadPrintLoop(threading.Thread):
386 """Thread to read output from child and log.
388 Taken from: https://github.com/pexpect/pexpect/issues/90
390 def __init__(self, child):
392 threading.Thread.__init__(self)
397 self.child.read_nonblocking()
398 except (pexpect.EOF, pexpect.TIMEOUT):
401 def relinquish(self):
402 """Relinquish control of process.
404 Give up control of application in order to ensure logging
405 continues for the application. After relinquishing control it
406 will no longer be possible to :func:`expect` anything.
408 This works around an issue described here:
410 https://github.com/pexpect/pexpect/issues/90
412 It is hoped that future versions of pexpect will avoid this
415 self._relinquish_thread = self.ContinueReadPrintLoop(self._child)
416 self._relinquish_thread.start()
419 class CustomProcess(Process):
420 """An sample implementation of ``Process``.
422 This is essentially a more detailed version of the
423 ``run_interactive_task`` function that checks for process execution
424 and kills the process (assuming use of the context manager).
426 def __init__(self, cmd, timeout, logfile, expect, name):
427 """Initialise process state.
429 :param cmd: Command to execute.
430 :param timeout: Time to wait for ``expect``.
431 :param logfile: Path to logfile.
432 :param expect: String to expect indicating startup. This is a
433 regex and should be escaped as such.
434 :param name: Name of process to use in logs.
439 self._logfile = logfile
440 self._expect = expect
441 self._proc_name = name
442 self._timeout = timeout