2023-01-11 11:04:27 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
2023-01-10 19:12:12 +01:00
|
|
|
from pathlib import Path
|
|
|
|
import sys
|
|
|
|
import threading
|
|
|
|
import subprocess
|
|
|
|
from typing import AnyStr, IO, Callable
|
|
|
|
|
|
|
|
|
2023-01-10 22:13:56 +01:00
|
|
|
def _chunk_transfer(chunk_file: Path, eof: bool):
|
2023-01-10 20:18:28 +01:00
|
|
|
print(f'Transferring chunk {chunk_file} to ... (This is the default method, it has no effect)')
|
2023-01-10 22:13:56 +01:00
|
|
|
if eof:
|
|
|
|
print(f'The last chunk has been transferred.')
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
|
2023-01-10 22:13:56 +01:00
|
|
|
def _rotate_chunk(chunk_file: Path, eof: bool, chunk_transfer_fun: Callable, chunk_transfer_args: tuple):
|
2023-01-10 20:18:28 +01:00
|
|
|
print(f'Transferring chunk {chunk_file}')
|
2023-01-10 22:13:56 +01:00
|
|
|
chunk_transfer_fun(chunk_file, eof, *chunk_transfer_args)
|
2023-01-10 19:12:12 +01:00
|
|
|
|
2023-01-10 20:18:28 +01:00
|
|
|
print(f'Removing chunk {chunk_file}')
|
|
|
|
chunk_file.unlink(missing_ok=False)
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
|
2023-01-10 20:18:28 +01:00
|
|
|
def _save_chunk(chunk: bytes, chunk_file: Path):
|
|
|
|
print(f'Saving chunk {chunk_file}')
|
2023-01-10 19:12:12 +01:00
|
|
|
# Fails if file does already exist.
|
2023-01-10 20:18:28 +01:00
|
|
|
with open(chunk_file, 'xb') as f:
|
2023-01-10 19:12:12 +01:00
|
|
|
f.write(chunk)
|
|
|
|
|
|
|
|
|
2023-01-11 11:04:27 +01:00
|
|
|
def _save_output(out_pipe: IO[AnyStr], stdout_dir: Path):
|
2023-01-10 19:12:12 +01:00
|
|
|
stdout_dir.mkdir(parents=True, exist_ok=False)
|
|
|
|
|
|
|
|
b: bytes
|
|
|
|
ct: int = 1
|
2023-01-11 11:04:27 +01:00
|
|
|
for b in out_pipe:
|
2023-01-10 19:12:12 +01:00
|
|
|
stdout_dir.joinpath(str(ct)).write_bytes(b)
|
|
|
|
ct += 1
|
|
|
|
|
|
|
|
# TODO: Has this any effect?
|
2023-01-11 11:04:27 +01:00
|
|
|
# out_pipe.close()
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
|
2023-01-11 11:04:27 +01:00
|
|
|
def _save_output_rotating_chunks(out_pipe: IO[AnyStr], chunk_file_tmpl: Path, chunk_size,
|
2023-01-10 19:12:12 +01:00
|
|
|
chunk_transfer_fun: Callable, chunk_transfer_args: tuple):
|
2023-01-10 22:13:56 +01:00
|
|
|
chunk_file_tmpl.parent.mkdir(parents=True, exist_ok=True)
|
2023-01-10 19:12:12 +01:00
|
|
|
|
2023-01-10 22:13:56 +01:00
|
|
|
ct: int = 1
|
2023-01-10 19:12:12 +01:00
|
|
|
remaining_bytes = chunk_size
|
|
|
|
chunk: bytes = b''
|
|
|
|
while True:
|
|
|
|
# https://docs.python.org/3/library/io.html#io.RawIOBase.read
|
|
|
|
# If 0 bytes are returned, and size was not 0, this indicates end of file.
|
|
|
|
# If the object is in non-blocking mode and no bytes are available, None is returned.
|
2023-01-11 11:04:27 +01:00
|
|
|
b = out_pipe.read(remaining_bytes)
|
2023-01-10 19:12:12 +01:00
|
|
|
if len(b) == 0:
|
2023-01-10 22:13:56 +01:00
|
|
|
# EOF reached.
|
|
|
|
chunk_file = chunk_file_tmpl.parent.joinpath(f'{chunk_file_tmpl.name}.{ct}')
|
|
|
|
|
2023-01-10 20:18:28 +01:00
|
|
|
_save_chunk(chunk, chunk_file)
|
2023-01-10 22:13:56 +01:00
|
|
|
_rotate_chunk(chunk_file, True, chunk_transfer_fun, chunk_transfer_args)
|
|
|
|
|
2023-01-10 19:12:12 +01:00
|
|
|
break
|
|
|
|
chunk += b
|
|
|
|
|
|
|
|
chunk_len = len(chunk)
|
|
|
|
if chunk_len == chunk_size:
|
2023-01-10 22:13:56 +01:00
|
|
|
# Next chunk is full.
|
|
|
|
chunk_file = chunk_file_tmpl.parent.joinpath(f'{chunk_file_tmpl.name}.{ct}')
|
|
|
|
|
2023-01-10 20:18:28 +01:00
|
|
|
_save_chunk(chunk, chunk_file)
|
2023-01-10 22:13:56 +01:00
|
|
|
_rotate_chunk(chunk_file, False, chunk_transfer_fun, chunk_transfer_args)
|
|
|
|
|
2023-01-10 19:12:12 +01:00
|
|
|
chunk = b''
|
|
|
|
remaining_bytes = chunk_size
|
2023-01-10 22:13:56 +01:00
|
|
|
ct += 1
|
2023-01-10 19:12:12 +01:00
|
|
|
elif chunk_len < chunk_size:
|
|
|
|
remaining_bytes = chunk_size - chunk_len
|
|
|
|
else:
|
|
|
|
raise ValueError('Invalid state')
|
|
|
|
|
|
|
|
# TODO: Has this any effect?
|
2023-01-11 11:04:27 +01:00
|
|
|
# out_pipe.close()
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
|
2023-01-11 11:04:27 +01:00
|
|
|
def _print_stderr(bin_pipe: IO[AnyStr]):
|
|
|
|
b: bytes
|
|
|
|
for b in bin_pipe:
|
|
|
|
sys.stderr.write(f'[STDERR] {b.decode("UTF-8")}')
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
# TODO: Has this any effect?
|
2023-01-11 11:04:27 +01:00
|
|
|
# bin_pipe.close()
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
def execute_print_transfer_chunks(command: list[str],
|
2023-01-10 22:13:56 +01:00
|
|
|
chunk_file_tmpl: Path,
|
2023-01-10 19:12:12 +01:00
|
|
|
chunk_transfer_fun: Callable = _chunk_transfer,
|
|
|
|
chunk_transfer_args: tuple = None,
|
|
|
|
chunk_size=1024 * 1024,
|
|
|
|
) -> int:
|
|
|
|
"""
|
2023-01-10 20:18:28 +01:00
|
|
|
Executes the given command.
|
|
|
|
|
|
|
|
Until the command has finished:
|
2023-01-10 19:12:12 +01:00
|
|
|
|
2023-01-10 22:13:56 +01:00
|
|
|
- saves a small part of the commands stdout to chunk_file = f'{chunk_file_tmpl.name}.{chunk_number}'
|
|
|
|
- calls chunk_transfer_fun with arguments (chunk_file, eof_reached, *chunk_transfer args)
|
|
|
|
- eof_reached is True for the last chunk
|
|
|
|
- the last chunk may be smaller than chunk_size
|
2023-01-10 20:18:28 +01:00
|
|
|
- deletes chunk_file
|
2023-01-10 19:12:12 +01:00
|
|
|
|
2023-01-10 20:18:28 +01:00
|
|
|
During command execution: Forwards stderr output of the command to stderr.
|
2023-01-10 19:12:12 +01:00
|
|
|
|
2023-01-10 22:13:56 +01:00
|
|
|
:param chunk_file_tmpl: Chunks are saved as this file before they are transferred.
|
2023-01-10 20:18:28 +01:00
|
|
|
:param command: Command to execute, e.g. ['cat', '/some/large/file']
|
2023-01-10 19:12:12 +01:00
|
|
|
:param chunk_transfer_fun:
|
|
|
|
:param chunk_transfer_args:
|
|
|
|
:param chunk_size: Defaults to 1MB (1024*1024).
|
2023-01-10 20:18:28 +01:00
|
|
|
:return: returncode of executed command
|
2023-01-10 19:12:12 +01:00
|
|
|
"""
|
|
|
|
if chunk_transfer_args is None:
|
2023-01-10 22:13:56 +01:00
|
|
|
chunk_transfer_args = tuple()
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
process = subprocess.Popen(
|
|
|
|
command,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
close_fds=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
t_out = threading.Thread(
|
|
|
|
target=_save_output_rotating_chunks,
|
2023-01-10 22:13:56 +01:00
|
|
|
args=(process.stdout, chunk_file_tmpl, chunk_size, chunk_transfer_fun, chunk_transfer_args))
|
2023-01-10 19:12:12 +01:00
|
|
|
t_err = threading.Thread(
|
2023-01-11 11:04:27 +01:00
|
|
|
target=_print_stderr, args=(process.stderr,))
|
2023-01-10 19:12:12 +01:00
|
|
|
|
|
|
|
for t in (t_out, t_err):
|
|
|
|
t.daemon = True
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
returncode = process.wait()
|
|
|
|
for t in (t_out, t_err):
|
|
|
|
t.join()
|
|
|
|
|
|
|
|
return returncode
|