A VCS repository archival tool
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

88 lines
3.0 KiB

  1. import logging
  2. import os
  3. import select
  4. import selectors
  5. import subprocess
  6. _logger = logging.getLogger(__name__)
  7. def run_with_log(args, *, check = True, input = None, **kwargs):
  8. '''
  9. Run a command using `subprocess.Popen(args, **kwargs)` and log all its stderr output via `logging`.
  10. `check` has the same semantics as on `subprocess.run`, i.e. raises an exception if the process exits non-zero.
  11. `input`, if specified, is a `bytes` or a binary file-like object that is fed to the subprocess via stdin.
  12. `stdin`, `stdout`, and `stderr` kwargs must not be used.
  13. Returns a tuple with the process's exit status, its stdout output, and its stderr output.
  14. '''
  15. badKwargs = {'stdin', 'stdout', 'stderr'}.intersection(set(kwargs))
  16. if badKwargs:
  17. raise ValueError(f'Disallowed kwargs: {", ".join(sorted(badKwargs))}')
  18. _logger.info(f'Running subprocess: {args!r}')
  19. if input is not None:
  20. kwargs['stdin'] = subprocess.PIPE
  21. p = subprocess.Popen(args, **kwargs, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
  22. sel = selectors.DefaultSelector()
  23. if input is not None:
  24. sel.register(p.stdin, selectors.EVENT_WRITE)
  25. sel.register(p.stdout, selectors.EVENT_READ)
  26. sel.register(p.stderr, selectors.EVENT_READ)
  27. stdout = []
  28. stderr = []
  29. stderrBuf = b''
  30. if input is not None:
  31. inputIsBytes = isinstance(input, bytes)
  32. if inputIsBytes:
  33. stdinView = memoryview(input)
  34. stdinOffset = 0
  35. stdinLength = len(input)
  36. PIPE_BUF = getattr(select, 'PIPE_BUF', 512)
  37. while sel.get_map():
  38. for key, _ in sel.select():
  39. if key.fileobj is p.stdin:
  40. try:
  41. if inputIsBytes:
  42. stdinOffset += os.write(key.fd, stdinView[stdinOffset : stdinOffset + PIPE_BUF])
  43. else:
  44. d = input.read(PIPE_BUF)
  45. if not d:
  46. sel.unregister(key.fileobj)
  47. key.fileobj.close()
  48. else:
  49. os.write(key.fd, d)
  50. except BrokenPipeError:
  51. sel.unregister(key.fileobj)
  52. key.fileobj.close()
  53. else:
  54. if inputIsBytes and stdinOffset >= stdinLength:
  55. sel.unregister(key.fileobj)
  56. key.fileobj.close()
  57. else:
  58. data = key.fileobj.read1()
  59. if not data:
  60. sel.unregister(key.fileobj)
  61. key.fileobj.close()
  62. continue
  63. if key.fileobj is p.stderr:
  64. stderr.append(data)
  65. stderrBuf += data
  66. *lines, stderrBuf = stderrBuf.replace(b'\r', b'\n').rsplit(b'\n', 1)
  67. if not lines:
  68. continue
  69. lines = lines[0].decode('utf-8').split('\n')
  70. for line in lines:
  71. _logger.info(line)
  72. else:
  73. stdout.append(data)
  74. if stderrBuf:
  75. _logger.info(stderrBuf.decode('utf-8'))
  76. p.wait()
  77. assert p.poll() is not None
  78. if input is not None and inputIsBytes and stdinOffset < len(input):
  79. _logger.warning(f'Could not write all input to the stdin pipe (wanted to write {len(input)} bytes, only wrote {stdinOffset})')
  80. _logger.info(f'Process exited with status {p.returncode}')
  81. if check and p.returncode != 0:
  82. raise subprocess.CalledProcessError(returncode = p.returncode, cmd = args)
  83. return (p.returncode, b''.join(stdout).decode('utf-8'), b''.join(stderr).decode('utf-8'))