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