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