123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- # -*- coding: utf-8 -*-
- """
- The MIT License
- Copyright (c) 2013 Helgi Þorbjörnsson
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- """
- import subprocess
- import time
- import os
- from os.path import isfile, split, join
- import tempfile
- import threading
- class Command(object):
- @classmethod
- def run(cls, command, timeout=None, cwd=None, env=None, debug=None):
- """
- Runs a given command on the system within a set time period, providing an easy way to access
- command output as it happens without waiting for the command to finish running.
- :type list
- :param command: Should be a list that contains the command that should be ran on the given
- system. The only whitespaces that can occur is for paths that use a backslash
- to escape it appropriately
- :type int
- :param timeout: Specificed in seconds. If a command outruns the timeout then the command and
- its child processes will be terminated. The default is to run
- :type string
- :param cwd: If cwd is set then the current directory will be changed to cwd before it is executed.
- Note that this directory is not considered when searching the executable, so you
- can’t specify the program’s path relative to cwd.
- :type dict
- :param env: A dict of any ENV variables that should be combined into the OS ENV that will help
- the command to run successfully. Note that more often than not the command run
- does not have the same ENV variables available as your shell by default and as such
- require some assistance.
- :type function
- :param debug: A function (also a class function) can be passed in here and all output, line by line,
- from the command being run will be passed to it as it gets outputted to stdout.
- This allows for things such as logging (using the built in python logging lib)
- what is happening on long running commands or redirect output of a tail -f call
- as lines get outputted without having to wait till the command finishes.
- :return returns :class:`Command.Response` that contains the exit code and the output from the command
- """
- # Merge together the system ENV details and the passed in ones if any
- environ = dict(os.environ)
- environ.update(env or {})
- # Check if the executable is executable and in fact exists
- which(command[0], environ)
- # Use tempfile to get past a limitation with subprocess.PIPE and 64kb.
- # Also to have a file to plug into the track generator
- outputtmp = tempfile.NamedTemporaryFile()
- try:
- # Verify debug is in fact a function we can call
- debug_output = False
- if debug and (callable(debug) or hasattr(debug, "__call__")):
- debug_output = True
- if debug_output:
- def track(thefile, shutdown=None):
- """Process the temp file until a valid line is found and return it"""
- thefile.seek(0, 2)
- while True:
- if shutdown and shutdown.isSet():
- break
- line = thefile.readline()
- if not line:
- time.sleep(0.00001)
- continue
- yield line
- def track_run(output, debug, shutdown_event):
- """Wrap track and pass information on the fly to the debug process"""
- for line in track(output, shutdown_event):
- debug(line.rstrip())
- # Run the track generator in a separate thread so we can run the command
- shutdown_event = threading.Event()
- thread = threading.Thread(
- target=track_run,
- args=(open(outputtmp.name, 'rb'), debug, shutdown_event),
- name='Monitoring'
- )
- thread.start()
- def target():
- # Run the actual command
- cls.process = subprocess.Popen(
- command,
- universal_newlines=True,
- shell=False,
- env=environ,
- cwd=cwd,
- preexec_fn=os.setsid,
- stdout=outputtmp,
- stderr=outputtmp
- )
- cls.process.communicate()
- # Deal with timeout
- thread = threading.Thread(target=target, name='Command Runner')
- thread.start()
- thread.join(timeout)
- if thread.is_alive():
- cls.process.terminate()
- thread.join()
- if thread.is_alive():
- cls.process.kill()
- # Prime the response
- response = Response
- response.command = command
- response.exit = cls.process.returncode
- # Fetch from the temp file
- outputtmp.seek(0, 0)
- response.output = outputtmp.read().strip()
- outputtmp.close()
- if response.exit < 0:
- raise CommandException("command ('%s') was terminated by signal: %s"
- % (' '.join(command), -response.exit),
- response.exit,
- response.output)
- if response.exit > 0:
- raise CommandException("command ('%s') exited with value: %s\n\n%s"
- % (' '.join(command), str(response.exit), response.output),
- response.exit,
- response.output)
- return response
- except OSError as error:
- # Use fake exit code since we can't get the accurate one from this
- raise CommandException("command failed: %s" % error, 1, error)
- except subprocess.CalledProcessError as error:
- raise CommandException(error.output, error.returncode, error.output)
- finally:
- if debug_output:
- shutdown_event.set()
- class CommandException(Exception):
- """
- Class for commanbd exceptions. Beside a specific error message it also stores the
- return code and the output of the command
- :type string
- :param Class specific message
- :type int
- :param exit_code: Exit code of the failed program (default: 1)
- :type string
- :param output: Any output associated with the failure from the program ran (default: None)
- """
- def __init__(self, message, exit_code=1, output=None):
- Exception.__init__(self, message)
- self.message = message
- self.exit = exit_code
- if output is None:
- output = message
- self.output = output
- def __str__(self):
- return repr(self.message)
- class Response(object):
- """Contain the response information for a given command"""
- exit = 0
- output = ''
- command = []
- def which(program, environ=None):
- """
- Find out if an executable exists in the supplied PATH.
- If so, the absolute path to the executable is returned.
- If not, an exception is raised.
- :type string
- :param program: Executable to be checked for
- :param dict
- :param environ: Any additional ENV variables required, specifically PATH
- :return string|:class:`command.CommandException` Returns the location if found, otherwise raises exception
- """
- def is_exe(path):
- """
- Helper method to check if a file exists and is executable
- """
- return isfile(path) and os.access(path, os.X_OK)
- if program is None:
- raise CommandException("Invalid program name passed")
- fpath, fname = split(program)
- if fpath:
- if is_exe(program):
- return program
- else:
- if environ is None:
- environ = os.environ
- for path in environ['PATH'].split(os.pathsep):
- exe_file = join(path, program)
- if is_exe(exe_file):
- return exe_file
- raise CommandException("Could not find %s" % program)
- def run(command, timeout=None, cwd=None, env=None, debug=None):
- """
- Runs a given command on the system within a set time period, providing an easy way to access
- command output as it happens without waiting for the command to finish running.
- :type list
- :param command: Should be a list that contains the command that should be ran on the given
- system. The only whitespaces that can occur is for paths that use a backslash
- to escape it appropriately
- :type int
- :param timeout: Specificed in seconds. If a command outruns the timeout then the command and
- its child processes will be terminated. The default is to run
- :type string
- :param cwd: If cwd is set then the current directory will be changed to cwd before it is executed.
- Note that this directory is not considered when searching the executable, so you
- can’t specify the program’s path relative to cwd.
- :type dict
- :param env: A dict of any ENV variables that should be combined into the OS ENV that will help
- the command to run successfully. Note that more often than not the command run
- does not have the same ENV variables available as your shell by default and as such
- require some assistance.
- :type function
- :param debug: A function (also a class function) can be passed in here and all output, line by line,
- from the command being run will be passed to it as it gets outputted to stdout.
- This allows for things such as logging (using the built in python logging lib)
- what is happening on long running commands or redirect output of a tail -f call
- as lines get outputted without having to wait till the command finishes.
- :return returns :class:`Command.Response` that contains the exit code and the output from the command
- """
- return Command.run(command, timeout=timeout, cwd=cwd, env=env, debug=debug)
|