Gnumed.pycommon.gmShellAPI

Package Gnumed :: Package pycommon :: Module gmShellAPI |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmShellAPI

      1  __doc__ = """GNUmed general tools."""
      2
      3  #===========================================================================
      4  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
      5  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
      6
      7
      8  # stdlib
      9  import os
     10  import sys
     11  import logging
     12  import subprocess
     13  import shlex
     14
     15  _log = logging.getLogger('gm.shell')
     16
     17  #===========================================================================
     18 -def is_cmd_in_path(cmd=None):
     19
     20          _log.debug('cmd: [%s]', cmd)
     21          dirname = os.path.dirname(cmd)
     22          _log.debug('dir: [%s]', dirname)
     23          if dirname != '':
     24                  _log.info('command with full or relative path, not searching in PATH for binary')
     25                  return (None, None)
     26
     27          #env_paths = str(os.environ['PATH'], encoding = sys.getfilesystemencoding(), errors = 'replace')
     28          env_paths = os.environ['PATH']
     29          _log.debug('${PATH}: %s', env_paths)
     30          for path in env_paths.split(os.pathsep):
     31                  candidate = os.path.join(path, cmd)
     32                  if os.access(candidate, os.X_OK):
     33                          _log.debug('found [%s]', candidate)
     34                          return (True, candidate)
     35                  else:
     36                          _log.debug('not found: %s', candidate)
     37
     38          _log.debug('command not found in PATH')
     39
     40          return (False, None)
     41
     42  #===========================================================================
     43 -def is_executable_by_wine(cmd=None):
     44
     45          if not cmd.startswith('wine'):
     46                  _log.debug('not a WINE call: %s', cmd)
     47                  return (False, None)
     48
     49          exe_path = cmd.encode(sys.getfilesystemencoding())
     50
     51          exe_path = exe_path[4:].strip().strip('"').strip()
     52          # [wine "/standard/unix/path/to/binary.exe"] ?
     53          if os.access(exe_path, os.R_OK):
     54                  _log.debug('WINE call with UNIX path: %s', exe_path)
     55                  return (True, cmd)
     56
     57          # detect [winepath]
     58          found, full_winepath_path = is_cmd_in_path(cmd = r'winepath')
     59          if not found:
     60                  _log.error('[winepath] not found, cannot check WINE call for Windows path conformance: %s', exe_path)
     61                  return (False, None)
     62
     63          # [wine "drive:\a\windows\path\to\binary.exe"] ?
     64          cmd_line = r'%s -u "%s"' % (
     65                  full_winepath_path.encode(sys.getfilesystemencoding()),
     66                  exe_path
     67          )
     68          _log.debug('converting Windows path to UNIX path: %s' % cmd_line)
     69          cmd_line = shlex.split(cmd_line)
     70          try:
     71                  winepath = subprocess.Popen (
     72                          cmd_line,
     73                          stdout = subprocess.PIPE,
     74                          stderr = subprocess.PIPE,
     75                          universal_newlines = True
     76                  )
     77          except OSError:
     78                  _log.exception('cannot run <winepath>')
     79                  return (False, None)
     80
     81          stdout, stderr = winepath.communicate()
     82          full_path = stdout.strip('\r\n')
     83          _log.debug('UNIX path: %s', full_path)
     84
     85          if winepath.returncode != 0:
     86                  _log.error('<winepath -u> returned [%s], failed to convert path', winepath.returncode)
     87                  return (False, None)
     88
     89          if os.access(full_path, os.R_OK):
     90                  _log.debug('WINE call with Windows path')
     91                  return (True, cmd)
     92
     93          _log.warning('Windows path [%s] not verifiable under UNIX: %s', exe_path, full_path)
     94          return (False, None)
     95
     96  #===========================================================================
     97 -def detect_external_binary(binary=None):
     98          """<binary> is the name of the executable with or without .exe/.bat"""
     99
    100          _log.debug('searching for [%s]', binary)
    101
    102          binary = binary.lstrip()
    103
    104          # is it a sufficiently qualified, directly usable, explicit path ?
    105          if os.access(binary, os.X_OK):
    106                  _log.debug('found: executable explicit path')
    107                  return (True, binary)
    108
    109          # can it be found in PATH ?
    110          found, full_path = is_cmd_in_path(cmd = binary)
    111          if found:
    112                  if os.access(full_path, os.X_OK):
    113                          _log.debug('found: executable in ${PATH}')
    114                          return (True, full_path)
    115
    116          # does it seem to be a call via WINE ?
    117          is_wine_call, full_path = is_executable_by_wine(cmd = binary)
    118          if is_wine_call:
    119                  _log.debug('found: is valid WINE call')
    120                  return (True, full_path)
    121
    122          # maybe we can be a bit smart about Windows ?
    123          if os.name == 'nt':
    124                  # try .exe (but not if already .bat or .exe)
    125                  if not (binary.endswith('.exe') or binary.endswith('.bat')):
    126                          exe_binary = binary + r'.exe'
    127                          _log.debug('re-testing as %s', exe_binary)
    128                          found_dot_exe_binary, full_path = detect_external_binary(binary = exe_binary)
    129                          if found_dot_exe_binary:
    130                                  return (True, full_path)
    131                          # not found with .exe, so try .bat:
    132                          bat_binary = binary + r'.bat'
    133                          _log.debug('re-testing as %s', bat_binary)
    134                          found_bat_binary, full_path = detect_external_binary(binary = bat_binary)
    135                          if found_bat_binary:
    136                                  return (True, full_path)
    137          else:
    138                  _log.debug('not running under Windows, not testing .exe/.bat')
    139
    140          return (False, None)
    141
    142  #===========================================================================
    143 -def find_first_binary(binaries=None):
    144          found = False
    145          binary = None
    146
    147          for cmd in binaries:
    148                  _log.debug('looking for [%s]', cmd)
    149                  if cmd is None:
    150                          continue
    151                  found, binary = detect_external_binary(binary = cmd)
    152                  if found:
    153                          break
    154
    155          return (found, binary)
    156
    157  #===========================================================================
    158 -def run_command_in_shell(command=None, blocking=False, acceptable_return_codes=None):
    159          """Runs a command in a subshell via standard-C system().
    160
    161          <command>
    162                  The shell command to run including command line options.
    163          <blocking>
    164                  This will make the code *block* until the shell command exits.
    165                  It will likely only work on UNIX shells where "cmd &" makes sense.
    166
    167          http://stackoverflow.com/questions/35817/how-to-escape-os-system-calls-in-python
    168          """
    169          if acceptable_return_codes is None:
    170                  acceptable_return_codes = [0]
    171
    172          _log.debug('shell command >>>%s<<<', command)
    173          _log.debug('blocking: %s', blocking)
    174          _log.debug('acceptable return codes: %s', str(acceptable_return_codes))
    175
    176          # FIXME: command should be checked for shell exploits
    177          command = command.strip()
    178
    179          if os.name == 'nt':
    180                  # http://stackoverflow.com/questions/893203/bat-files-nonblocking-run-launch
    181                  if blocking is False:
    182                          if not command.startswith('start '):
    183                                  command = 'start "GNUmed" /B "%s"' % command
    184  #               elif blocking is True:
    185  #                       if not command.startswith('start '):
    186  #                               command = 'start "GNUmed" /WAIT /B "%s"' % command
    187          else:
    188                  # what the following hack does is this: the user indicated
    189                  # whether she wants non-blocking external display of files
    190                  # - the real way to go about this is to have a non-blocking command
    191                  #   in the line in the mailcap file for the relevant mime types
    192                  # - as non-blocking may not be desirable when *not* displaying
    193                  #   files from within GNUmed the really right way would be to
    194                  #   add a "test" clause to the non-blocking mailcap entry which
    195                  #   yields true if and only if GNUmed is running
    196                  # - however, this is cumbersome at best and not supported in
    197                  #   some mailcap implementations
    198                  # - so we allow the user to attempt some control over the process
    199                  #   from within GNUmed by setting a configuration option
    200                  # - leaving it None means to use the mailcap default or whatever
    201                  #   was specified in the command itself
    202                  # - True means: tack " &" onto the shell command if necessary
    203                  # - False means: remove " &" from the shell command if its there
    204                  # - all this, of course, only works in shells which support
    205                  #   detaching jobs with " &" (so, most POSIX shells)
    206                  if blocking is True:
    207                          command = command.rstrip(' &')
    208                  elif blocking is False:
    209                          if not command.strip().endswith('&'):
    210                                  command += ' &'
    211
    212          _log.info('running shell command >>>%s<<<', command)
    213          # FIXME: use subprocess.Popen()
    214          ret_val = os.system(command.encode(sys.getfilesystemencoding()))
    215          _log.debug('os.system() returned: [%s]', ret_val)
    216
    217          exited_normally = False
    218
    219          if not hasattr(os, 'WIFEXITED'):
    220                  _log.error('platform does not support exit status differentiation')
    221                  if ret_val in acceptable_return_codes:
    222                          _log.info('os.system() return value contained in acceptable return codes')
    223                          _log.info('continuing and hoping for the best')
    224                          return True
    225                  return exited_normally
    226
    227          _log.debug('exited via exit(): %s', os.WIFEXITED(ret_val))
    228          if os.WIFEXITED(ret_val):
    229                  _log.debug('exit code: [%s]', os.WEXITSTATUS(ret_val))
    230                  exited_normally = (os.WEXITSTATUS(ret_val) in acceptable_return_codes)
    231                  _log.debug('normal exit: %s', exited_normally)
    232          _log.debug('dumped core: %s', os.WCOREDUMP(ret_val))
    233          _log.debug('stopped by signal: %s', os.WIFSIGNALED(ret_val))
    234          if os.WIFSIGNALED(ret_val):
    235                  try:
    236                          _log.debug('STOP signal was: [%s]', os.WSTOPSIG(ret_val))
    237                  except AttributeError:
    238                          _log.debug('platform does not support os.WSTOPSIG()')
    239                  try:
    240                          _log.debug('TERM signal was: [%s]', os.WTERMSIG(ret_val))
    241                  except AttributeError:
    242                          _log.debug('platform does not support os.WTERMSIG()')
    243
    244          return exited_normally
    245
    246  #===========================================================================
    247 -def run_first_available_in_shell(binaries=None, args=None, blocking=False, run_last_one_anyway=False, acceptable_return_codes=None):
    248
    249          found, binary = find_first_binary(binaries = binaries)
    250
    251          if not found:
    252                  _log.warning('cannot find any of: %s', binaries)
    253                  if run_last_one_anyway:
    254                          binary = binaries[-1]
    255                          _log.debug('falling back to trying to run [%s] anyway', binary)
    256                  else:
    257                          return False
    258
    259          return run_command_in_shell(command = '%s %s' % (binary, args), blocking = blocking, acceptable_return_codes = acceptable_return_codes)
    260
    261  #===========================================================================
    262 -def _log_output(level, stdout=None, stderr=None):
    263          lines2log = ['process output:']
    264          if stdout is not None:
    265                  lines2log.extend([ ' STDOUT: %s' % line for line in stdout.split('\n') ])
    266          if stderr is not None:
    267                  lines2log.extend([ ' STDERR: %s' % line for line in stderr.split('\n') ])
    268          _log.log(level, '\n'.join(lines2log))
    269
    270  #===========================================================================
    271 -def run_process(cmd_line=None, timeout=None, encoding='utf8', input_data=None, acceptable_return_codes=None, verbose=False):
    272          assert (cmd_line is not None), '<cmd_line> must not be None'
    273
    274          if acceptable_return_codes is None:
    275                  acceptable_return_codes = [0]
    276          _log.info('running: %s' % cmd_line)
    277          try:
    278                  if input_data is None:
    279                          proc_result = subprocess.run (
    280                                  args = cmd_line,
    281                                  stdin = subprocess.PIPE,
    282                                  stdout = subprocess.PIPE,
    283                                  stderr = subprocess.PIPE,
    284                                  timeout = timeout,
    285                                  encoding = encoding,
    286                                  errors = 'replace'
    287                          )
    288                  else:
    289                          proc_result = subprocess.run (
    290                                  args = cmd_line,
    291                                  input = input_data,
    292                                  stdout = subprocess.PIPE,
    293                                  stderr = subprocess.PIPE,
    294                                  timeout = timeout,
    295                                  encoding = encoding,
    296                                  errors = 'replace'
    297                          )
    298          except (subprocess.TimeoutExpired, FileNotFoundError):
    299                  _log.exception('there was a problem running external process')
    300                  return False, -1, ''
    301
    302          _log.info('exit code [%s]', proc_result.returncode)
    303          if verbose:
    304                  _log_output(logging.DEBUG, stdout = proc_result.stdout, stderr = proc_result.stderr)
    305          if proc_result.returncode not in acceptable_return_codes:
    306                  _log.error('there was a problem executing the external process')
    307                  _log.debug('expected one of: %s', acceptable_return_codes)
    308                  if not verbose:
    309                          _log_output(logging.ERROR, stdout = proc_result.stdout, stderr = proc_result.stderr)
    310                  return False, proc_result.returncode, ''
    311
    312          return True, proc_result.returncode, proc_result.stdout
    313
    314  #===========================================================================
    315  # main
    316  #---------------------------------------------------------------------------
    317  if __name__ == '__main__':
    318
    319          if len(sys.argv) < 2:
    320                  sys.exit()
    321
    322          if sys.argv[1] != 'test':
    323                  sys.exit()
    324
    325          logging.basicConfig(level = logging.DEBUG)
    326          #---------------------------------------------------------
    327 -        def test_detect_external_binary():
    328                  found, path = detect_external_binary(binary = sys.argv[2])
    329                  if found:
    330                          print("found as:", path)
    331                  else:
    332                          print(sys.argv[2], "not found")
    333          #---------------------------------------------------------
    334 -        def test_run_command_in_shell():
    335                  print("-------------------------------------")
    336                  print("running:", sys.argv[2])
    337                  if run_command_in_shell(command=sys.argv[2], blocking=False):
    338                          print("-------------------------------------")
    339                          print("success")
    340                  else:
    341                          print("-------------------------------------")
    342                          print("failure, consult log")
    343          #---------------------------------------------------------
    344 -        def test_is_cmd_in_path():
    345                  print(is_cmd_in_path(cmd = sys.argv[2]))
    346          #---------------------------------------------------------
    347 -        def test_is_executable_by_wine():
    348                  print(is_executable_by_wine(cmd = sys.argv[2]))
    349          #---------------------------------------------------------
    350          #test_run_command_in_shell()
    351          #test_detect_external_binary()
    352          test_is_cmd_in_path()
    353          #test_is_executable_by_wine()
    354
    355  #===========================================================================
    356