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(map(os.path.expanduser, cmd),
79 stdout=subprocess.PIPE,
80 stderr=subprocess.PIPE, bufsize=0)
83 reads = [proc.stdout.fileno(), proc.stderr.fileno()]
84 ret = select.select(reads, [], [])
87 if file_d == proc.stdout.fileno():
88 line = proc.stdout.readline()
89 if settings.getValue('VERBOSITY') == 'debug':
90 sys.stdout.write(line.decode(_MY_ENCODING))
92 if file_d == proc.stderr.fileno():
93 line = proc.stderr.readline()
94 sys.stderr.write(line.decode(_MY_ENCODING))
97 if proc.poll() is not None:
103 ex = subprocess.CalledProcessError(proc.returncode, cmd, stderr)
106 return ('\n'.join(sout.decode(_MY_ENCODING).strip() for sout in stdout),
107 ('\n'.join(sout.decode(_MY_ENCODING).strip() for sout in stderr)))
109 def run_background_task(cmd, logger, msg):
110 """Run task in background and log when started.
112 Run given task using ``subprocess.Popen``. Log the command
113 used. Print stdout to screen if in verbose mode. Prints stderr
116 :param cmd: Exact command to be executed
117 :param logger: Logger to write details to
118 :param msg: Message to be shown to user
120 :returns: Process PID
123 logger.debug('%s%s', CMD_PREFIX, ' '.join(cmd))
125 proc = subprocess.Popen(map(os.path.expanduser, cmd), stdout=_get_stdout(), bufsize=0)
130 def run_interactive_task(cmd, logger, msg):
131 """Run a task interactively and log when started.
133 Run given task using ``pexpect.spawn``. Log the command used.
134 Performs neither validation of the process - if the process
135 successfully started or is still running - nor killing of the
136 process. The user must do both.
138 :param cmd: Exact command to be executed
139 :param logger: Logger to write details to
140 :param msg: Message to be shown to user
142 :returns: ``pexpect.child`` object
145 logger.debug('%s%s', CMD_PREFIX, cmd)
146 child = pexpect.spawnu(cmd)
148 if settings.getValue('VERBOSITY') == 'debug':
149 child.logfile_read = sys.stdout
154 class Process(object):
155 """Control an instance of a long-running process.
157 This is basically a context-manager wrapper around the
158 ``run_interactive_task`` function above (with some extra helper
164 _logger = logging.getLogger(__name__)
167 _proc_name = 'unnamed process'
168 _relinquish_thread = None
173 """Start process instance using context manager.
178 def __exit__(self, type_, value, traceback):
179 """Shutdown process instance.
186 """Start process instance.
188 self._start_process()
189 if self._timeout > 0:
190 self._expect_process()
192 def _start_process(self):
193 """Start process instance.
195 cmd = ' '.join(settings.getValue('SHELL_CMD') +
196 ['"%s"' % ' '.join(self._cmd)])
198 self._child = run_interactive_task(cmd, self._logger,
199 'Starting %s...' % self._proc_name)
200 self._child.logfile = open(self._logfile, 'w')
202 def expect(self, msg, timeout=None):
203 """Expect string from process.
205 Expect string and die if not received.
207 :param msg: String to expect.
208 :param timeout: Time to wait for string.
212 self._expect_process(msg, timeout)
214 def _expect_process(self, msg=None, timeout=None):
215 """Expect string from process.
220 timeout = self._timeout
222 # we use exceptions rather than catching conditions in ``expect`` list
223 # as we want to fail catastrophically after handling; there is likely
224 # little we can do from within the scripts to fix issues such as
225 # hugepages not being mounted
227 self._child.expect([msg], timeout=timeout)
228 except pexpect.EOF as exc:
229 self._logger.critical(
230 'An error occurred. Please check the logs (%s) for more'
231 ' information. Exiting...', self._logfile)
233 except pexpect.TIMEOUT as exc:
234 self._logger.critical(
235 'Failed to execute in \'%d\' seconds. Please check the logs'
236 ' (%s) for more information. Exiting...',
237 timeout, self._logfile)
240 except (Exception, KeyboardInterrupt) as exc:
241 self._logger.critical('General exception raised. Exiting...')
245 def kill(self, signal='-15', sleep=2):
246 """Kill process instance if it is alive.
248 :param signal: signal to be sent to the process
249 :param sleep: delay in seconds after signal is sent
251 if self._child and self._child.isalive():
252 run_task(['sudo', 'kill', signal, str(self._child.pid)],
254 self._logger.debug('Wait for process to terminate')
257 if self.is_relinquished():
258 self._relinquish_thread.join()
261 'Log available at %s', self._logfile)
263 def is_relinquished(self):
264 """Returns True if process is relinquished.
266 If relinquished the process is no longer controllable and can
269 :returns: True if process is relinquished, else False.
271 return self._relinquish_thread
273 def is_running(self):
274 """Returns True if process is running.
276 :returns: True if process is running, else False
278 return self._child is not None
280 def _affinitize_pid(self, core, pid):
281 """Affinitize a process with ``pid`` to ``core``.
283 :param core: Core to affinitize process to.
284 :param pid: Process ID to affinitize.
288 run_task(['sudo', 'taskset', '-c', '-p', str(core),
292 def affinitize(self, core):
293 """Affinitize process to a specific ``core``.
295 :param core: Core to affinitize process to.
299 self._logger.info('Affinitizing process')
301 if self._child and self._child.isalive():
302 self._affinitize_pid(core, self._child.pid)
304 class ContinueReadPrintLoop(threading.Thread):
305 """Thread to read output from child and log.
307 Taken from: https://github.com/pexpect/pexpect/issues/90
309 def __init__(self, child):
311 threading.Thread.__init__(self)
316 self.child.read_nonblocking()
317 except (pexpect.EOF, pexpect.TIMEOUT):
320 def relinquish(self):
321 """Relinquish control of process.
323 Give up control of application in order to ensure logging
324 continues for the application. After relinquishing control it
325 will no longer be possible to :func:`expect` anything.
327 This works around an issue described here:
329 https://github.com/pexpect/pexpect/issues/90
331 It is hoped that future versions of pexpect will avoid this
334 self._relinquish_thread = self.ContinueReadPrintLoop(self._child)
335 self._relinquish_thread.start()
338 class CustomProcess(Process):
339 """An sample implementation of ``Process``.
341 This is essentially a more detailed version of the
342 ``run_interactive_task`` function that checks for process execution
343 and kills the process (assuming use of the context manager).
345 def __init__(self, cmd, timeout, logfile, expect, name):
346 """Initialise process state.
348 :param cmd: Command to execute.
349 :param timeout: Time to wait for ``expect``.
350 :param logfile: Path to logfile.
351 :param expect: String to expect indicating startup. This is a
352 regex and should be escaped as such.
353 :param name: Name of process to use in logs.
358 self._logfile = logfile
359 self._expect = expect
360 self._proc_name = name
361 self._timeout = timeout