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...')
245 """Kill process instance if it is alive.
247 if self._child and self._child.isalive():
248 run_task(['sudo', 'kill', '-15', str(self._child.pid)],
250 self._logger.debug('Wait for process to terminate')
253 if self.is_relinquished():
254 self._relinquish_thread.join()
257 'Log available at %s', self._logfile)
259 def is_relinquished(self):
260 """Returns True if process is relinquished.
262 If relinquished the process is no longer controllable and can
265 :returns: True if process is relinquished, else False.
267 return self._relinquish_thread
269 def is_running(self):
270 """Returns True if process is running.
272 :returns: True if process is running, else False
274 return self._child is not None
276 def _affinitize_pid(self, core, pid):
277 """Affinitize a process with ``pid`` to ``core``.
279 :param core: Core to affinitize process to.
280 :param pid: Process ID to affinitize.
284 run_task(['sudo', 'taskset', '-c', '-p', str(core),
288 def affinitize(self, core):
289 """Affinitize process to a specific ``core``.
291 :param core: Core to affinitize process to.
295 self._logger.info('Affinitizing process')
297 if self._child and self._child.isalive():
298 self._affinitize_pid(core, self._child.pid)
300 class ContinueReadPrintLoop(threading.Thread):
301 """Thread to read output from child and log.
303 Taken from: https://github.com/pexpect/pexpect/issues/90
305 def __init__(self, child):
307 threading.Thread.__init__(self)
312 self.child.read_nonblocking()
313 except (pexpect.EOF, pexpect.TIMEOUT):
316 def relinquish(self):
317 """Relinquish control of process.
319 Give up control of application in order to ensure logging
320 continues for the application. After relinquishing control it
321 will no longer be possible to :func:`expect` anything.
323 This works around an issue described here:
325 https://github.com/pexpect/pexpect/issues/90
327 It is hoped that future versions of pexpect will avoid this
330 self._relinquish_thread = self.ContinueReadPrintLoop(self._child)
331 self._relinquish_thread.start()
334 class CustomProcess(Process):
335 """An sample implementation of ``Process``.
337 This is essentially a more detailed version of the
338 ``run_interactive_task`` function that checks for process execution
339 and kills the process (assuming use of the context manager).
341 def __init__(self, cmd, timeout, logfile, expect, name):
342 """Initialise process state.
344 :param cmd: Command to execute.
345 :param timeout: Time to wait for ``expect``.
346 :param logfile: Path to logfile.
347 :param expect: String to expect indicating startup. This is a
348 regex and should be escaped as such.
349 :param name: Name of process to use in logs.
354 self._logfile = logfile
355 self._expect = expect
356 self._proc_name = name
357 self._timeout = timeout