1 # Copyright 2015 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
32 _MY_ENCODING = locale.getdefaultlocale()[1]
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})
75 logger.debug('%s%s', CMD_PREFIX, ' '.join(cmd))
78 proc = subprocess.Popen(
79 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
82 reads = [proc.stdout.fileno(), proc.stderr.fileno()]
83 ret = select.select(reads, [], [])
86 if file_d == proc.stdout.fileno():
87 line = proc.stdout.readline()
88 if settings.getValue('VERBOSITY') == 'debug':
89 sys.stdout.write(line.decode(_MY_ENCODING))
91 if file_d == proc.stderr.fileno():
92 line = proc.stderr.readline()
93 sys.stderr.write(line.decode(_MY_ENCODING))
96 if proc.poll() is not None:
102 ex = subprocess.CalledProcessError(proc.returncode, cmd, stderr)
105 return ('\n'.join(sout.decode(_MY_ENCODING).strip() for sout in stdout),
106 ('\n'.join(sout.decode(_MY_ENCODING).strip() for sout in stderr)))
108 def run_background_task(cmd, logger, msg):
109 """Run task in background and log when started.
111 Run given task using ``subprocess.Popen``. Log the command
112 used. Print stdout to screen if in verbose mode. Prints stderr
115 :param cmd: Exact command to be executed
116 :param logger: Logger to write details to
117 :param msg: Message to be shown to user
119 :returns: Process PID
122 logger.debug('%s%s', CMD_PREFIX, ' '.join(cmd))
124 proc = subprocess.Popen(cmd, stdout=_get_stdout(), bufsize=0)
129 def run_interactive_task(cmd, logger, msg):
130 """Run a task interactively and log when started.
132 Run given task using ``pexpect.spawn``. Log the command used.
133 Performs neither validation of the process - if the process
134 successfully started or is still running - nor killing of the
135 process. The user must do both.
137 :param cmd: Exact command to be executed
138 :param logger: Logger to write details to
139 :param msg: Message to be shown to user
141 :returns: ``pexpect.child`` object
144 logger.debug('%s%s', CMD_PREFIX, cmd)
145 child = pexpect.spawnu(cmd)
147 if settings.getValue('VERBOSITY') == 'debug':
148 child.logfile_read = sys.stdout
153 class Process(object):
154 """Control an instance of a long-running process.
156 This is basically a context-manager wrapper around the
157 ``run_interactive_task`` function above (with some extra helper
163 _logger = logging.getLogger(__name__)
166 _proc_name = 'unnamed process'
167 _relinquish_thread = None
172 """Start process instance using context manager.
177 def __exit__(self, type_, value, traceback):
178 """Shutdown process instance.
185 """Start process instance.
187 self._start_process()
188 if self._timeout > 0:
189 self._expect_process()
191 def _start_process(self):
192 """Start process instance.
194 cmd = ' '.join(settings.getValue('SHELL_CMD') +
195 ['"%s"' % ' '.join(self._cmd)])
197 self._child = run_interactive_task(cmd, self._logger,
198 'Starting %s...' % self._proc_name)
199 self._child.logfile = open(self._logfile, 'w')
201 def expect(self, msg, timeout=None):
202 """Expect string from process.
204 Expect string and die if not received.
206 :param msg: String to expect.
207 :param timeout: Time to wait for string.
211 self._expect_process(msg, timeout)
213 def _expect_process(self, msg=None, timeout=None):
214 """Expect string from process.
219 timeout = self._timeout
221 # we use exceptions rather than catching conditions in ``expect`` list
222 # as we want to fail catastrophically after handling; there is likely
223 # little we can do from within the scripts to fix issues such as
224 # hugepages not being mounted
226 self._child.expect([msg], timeout=timeout)
227 except pexpect.EOF as exc:
228 self._logger.critical(
229 'An error occurred. Please check the logs (%s) for more'
230 ' information. Exiting...', self._logfile)
232 except pexpect.TIMEOUT as exc:
233 self._logger.critical(
234 'Failed to execute in \'%d\' seconds. Please check the logs'
235 ' (%s) for more information. Exiting...',
236 timeout, self._logfile)
239 except (Exception, KeyboardInterrupt) as exc:
240 self._logger.critical('General exception raised. Exiting...')
244 def kill(self, signal='-15', sleep=2):
245 """Kill process instance if it is alive.
247 :param signal: signal to be sent to the process
248 :param sleep: delay in seconds after signal is sent
250 if self._child and self._child.isalive():
251 run_task(['sudo', 'kill', signal, str(self._child.pid)],
253 self._logger.debug('Wait for process to terminate')
256 if self.is_relinquished():
257 self._relinquish_thread.join()
260 'Log available at %s', self._logfile)
262 def is_relinquished(self):
263 """Returns True if process is relinquished.
265 If relinquished the process is no longer controllable and can
268 :returns: True if process is relinquished, else False.
270 return self._relinquish_thread
272 def is_running(self):
273 """Returns True if process is running.
275 :returns: True if process is running, else False
277 return self._child is not None
279 def _affinitize_pid(self, core, pid):
280 """Affinitize a process with ``pid`` to ``core``.
282 :param core: Core to affinitize process to.
283 :param pid: Process ID to affinitize.
287 run_task(['sudo', 'taskset', '-c', '-p', str(core),
291 def affinitize(self, core):
292 """Affinitize process to a specific ``core``.
294 :param core: Core to affinitize process to.
298 self._logger.info('Affinitizing process')
300 if self._child and self._child.isalive():
301 self._affinitize_pid(core, self._child.pid)
303 class ContinueReadPrintLoop(threading.Thread):
304 """Thread to read output from child and log.
306 Taken from: https://github.com/pexpect/pexpect/issues/90
308 def __init__(self, child):
310 threading.Thread.__init__(self)
315 self.child.read_nonblocking()
316 except (pexpect.EOF, pexpect.TIMEOUT):
319 def relinquish(self):
320 """Relinquish control of process.
322 Give up control of application in order to ensure logging
323 continues for the application. After relinquishing control it
324 will no longer be possible to :func:`expect` anything.
326 This works around an issue described here:
328 https://github.com/pexpect/pexpect/issues/90
330 It is hoped that future versions of pexpect will avoid this
333 self._relinquish_thread = self.ContinueReadPrintLoop(self._child)
334 self._relinquish_thread.start()
337 class CustomProcess(Process):
338 """An sample implementation of ``Process``.
340 This is essentially a more detailed version of the
341 ``run_interactive_task`` function that checks for process execution
342 and kills the process (assuming use of the context manager).
344 def __init__(self, cmd, timeout, logfile, expect, name):
345 """Initialise process state.
347 :param cmd: Command to execute.
348 :param timeout: Time to wait for ``expect``.
349 :param logfile: Path to logfile.
350 :param expect: String to expect indicating startup. This is a
351 regex and should be escaped as such.
352 :param name: Name of process to use in logs.
357 self._logfile = logfile
358 self._expect = expect
359 self._proc_name = name
360 self._timeout = timeout