Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
b3144ae
Add a new shell_out that works with AST-commands
May 20, 2025
50eb803
fix the headers and move tests to the tests folder
May 21, 2025
5792afd
fix pylint errors in /benchkit/shell/
May 21, 2025
5cde1db
fix a mistake in the imports of the visitor
May 21, 2025
0c79d2c
fixes a bug where output logging was incomplete, along with minor bug…
May 21, 2025
0ed91a5
fix the ordering for the thead
May 21, 2025
940ef7e
fix a bug where input would not always get flushed to the command
May 21, 2025
65b7814
switch logging to commandhooks
aaronbog May 24, 2025
f3cdc9b
adds async version and removes useless print statements
May 26, 2025
5c8d908
releases the file handels of the Popen process to avoid warnings in t…
May 26, 2025
59ccd6b
start on the test framework
May 26, 2025
ee8ed4c
make the command use the correct pid
May 27, 2025
0fe606c
ads ability for ssh IO handles to be None
May 27, 2025
bab21ed
rework basic tests
May 27, 2025
188f707
Merge branch 'commands-as-AST' into testRework
May 27, 2025
92e256c
expand the test casses and do small cleanup tasks
May 28, 2025
1f37cd4
apply fixes to bugs
May 28, 2025
8219706
Merge branch 'commands-as-AST' into testRework
May 28, 2025
e24c296
iprove documentation of the tests
May 28, 2025
f150883
increased timeout time for tests due to them being to slow for wsl
May 29, 2025
25c5aec
fixes isort flow
May 29, 2025
5bd2fc4
redone formating to pass CI
May 29, 2025
597abe2
do more changes for CI
May 29, 2025
7cb0c81
add basic tests that go along with the ast-shell. (#2)
aaronbog May 29, 2025
353ded1
improve on the complaints given by python linter
May 30, 2025
d63f346
fix a bug where readline would apear empty and did not follow convention
May 30, 2025
8a8b833
add the functionality to change the shell implementation to the new one
May 30, 2025
9dab378
merged commands-as-AST into the testbranch
May 30, 2025
aeec29f
added a test for the return codes
May 30, 2025
26c3458
clean up code, remove useless print and make it conform to the checker
May 30, 2025
a57cecb
rework the printing of commands and outputs to be more conform to the…
May 30, 2025
fba05f6
remove leftover debugging lines
May 30, 2025
5aca124
changes outputs to isolated IOStreams for performance overhead
Jun 2, 2025
5351201
move hooks to thier own folder to reduce clutter
Jun 2, 2025
a820e49
fix formating errors for CI
Jun 2, 2025
b97e2bf
fixes formating error for the output of commands when logging is enabled
Jun 2, 2025
145bafb
swith the interactive shell to use the new one
Jun 2, 2025
572bfcc
remove prints
Jun 3, 2025
9482e58
changes to accomodate better input
Jun 5, 2025
5ac9407
change things, non working for reference
Jun 5, 2025
c219637
rework the command outputs and convert shell to the new system
Jun 6, 2025
a49e401
finish implementation of new structs
Jun 7, 2025
160ff16
finish implementation of new structs
Jun 7, 2025
38e7ea0
minor fixes for commandProcesses
Jun 7, 2025
3c837ed
clean up the code and fix typing issues
Jun 7, 2025
fc3e400
main flake errors fixed
Jun 7, 2025
3fb9969
minor typing fix
Jun 9, 2025
32fe803
add tests for the command execution basics
Jun 10, 2025
4211cff
make the environment empty by deafault
Jun 10, 2025
f5d484e
Merge branch 'commands-as-AST' into testRework
Jun 10, 2025
1f737cf
finish the basic execute tests
Jun 10, 2025
1ac3781
rename and add start of std_input test
Jun 10, 2025
7a0164c
added aditional tests
Jun 11, 2025
45926ec
added demo code to other_tests
Jun 12, 2025
5871374
fixed backwards compatibility
Jun 12, 2025
1213c4c
Merge branch 'commands-as-AST' into testRework
Jun 12, 2025
d05c61d
fix a bug where processes dont clean up nicely
Jun 14, 2025
3dbe3fb
fix the range for all ignored return codes
Jun 12, 2025
431515a
add some more usefull hooks
Jun 16, 2025
6efe641
remove ast from execution branch
Jun 17, 2025
6e46ded
reorganize the files of the new shell module
Jun 24, 2025
7c8603e
give naming option to IOHooks
Jun 30, 2025
a91040a
add the missing None
Jun 30, 2025
c4e9acc
do the minimal changes to be threaded instead of Processes
Jun 30, 2025
ee52840
move pipe to logical location
Jun 30, 2025
18e6981
intoduce a hook that logs all other interations
Jun 30, 2025
a5e97a5
implemented the closing of pipes and added better naming
Jul 1, 2025
55de5ee
thread fixes
Jul 3, 2025
caab225
remove logs
Jul 3, 2025
f315a45
removed debugging tools from the branch
Jul 3, 2025
904d22f
removed useless file
Jul 3, 2025
0af4552
removed file unasosiated with this branch
Jul 3, 2025
d80a814
remove wrongfull import
Jul 3, 2025
71d02b3
remove useless line
Jul 3, 2025
61061ed
undo threading
Jul 9, 2025
88238a7
fixed bug in readline
Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchkit/communication/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def shell(
timeout=timeout,
output_is_log=output_is_log,
ignore_ret_codes=ignore_ret_codes,
ignore_any_error_code=ignore_any_error_code,
)

return output
Expand Down
106 changes: 106 additions & 0 deletions benchkit/shell/command_execution/command_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (C) 2025 Vrije Universiteit Brussel. All rights reserved.
# SPDX-License-Identifier: MIT

from __future__ import annotations # Otherwise Queue comlains about typing

import itertools
from multiprocessing import Queue
from subprocess import CalledProcessError, Popen, TimeoutExpired
from threading import Thread
from typing import Iterable, Optional, Tuple

from benchkit.shell.command_execution.io.output import Output


class CommandProcess:
"""Encaptulation of the Popen process with functions to use it in an asyncronous way"""
def __init__(
self,
popen_object: Popen[bytes],
output: Output,
timeout: Optional[int],
success_code: int = 0,
ignore_exit_codes: Optional[Iterable[int]] = None,
):

self.__popen_object: Popen[bytes] = popen_object
self.__output: Output = output
self.__timeout: Optional[int] = timeout
self.__retcode_queue: Queue[Tuple[int, Optional[Exception]]] = Queue()
self.success_code: int = success_code
self.ignore_exit_codes: Iterable[int] = (
(success_code,) if ignore_exit_codes is None else ignore_exit_codes
)

# add the success_code to the return codes to ignore
if self.success_code not in self.ignore_exit_codes:
self.ignore_exit_codes = itertools.chain([success_code], self.ignore_exit_codes)

self.retcode: Optional[int] = None
self.error: Optional[Exception] = None
self.process: Thread = self.__wait_async()

@staticmethod
def __wait_func(
subprocess: Popen[bytes],
queue: Queue[Tuple[int, Optional[Exception]]],
timeout: Optional[int],
ignore_exit_codes: Iterable[int],
) -> None:
try:
retcode = subprocess.wait(timeout)
if retcode not in ignore_exit_codes:
queue.put(
(
retcode,
CalledProcessError(
retcode,
subprocess.args,
),
)
)
else:
queue.put((retcode, None))
except TimeoutExpired as exc:
# TODO: we can add some form of logging here to warn the user if something went wrong
subprocess.terminate()
subprocess.wait(1)
queue.put((-1, exc))

def __wait_async(self) -> Thread:
waiting_thread = Thread(
target=self.__wait_func,
args=(
self.__popen_object,
self.__retcode_queue,
self.__timeout,
self.ignore_exit_codes,
),
)
waiting_thread.start()
return waiting_thread

def get_output(self) -> Output:
"""get the Output object related to this process
can be used as input for other processes"""
return self.__output

def get_return_code(self) -> int:
"""halt until the process has a return code
if the return code is not ignored
or the waittime was exceded throw an error instead"""
if self.error is not None:
raise self.error
if self.retcode:
return self.retcode
self.process.join()
self.retcode, self.error = self.__retcode_queue.get()
if self.error is not None:
raise self.error
return self.retcode

# TODO: check how this interacts with ssh
# THIS DOES NOT SEND IT TO THE RIGHT ONE -> move abstraction higher
def signal(self, signalcode: int) -> None:
self.__popen_object.send_signal(signalcode)
self.__popen_object.wait(1)
104 changes: 104 additions & 0 deletions benchkit/shell/command_execution/execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright (C) 2024 Vrije Universiteit Brussel. All rights reserved.
# SPDX-License-Identifier: MIT

# Otherwise os.PathLike[Any] complains
from __future__ import annotations

import pathlib
import subprocess
from typing import Dict, Iterable, List, Optional

from benchkit.shell.command_execution.command_process import CommandProcess
from benchkit.shell.command_execution.io.hooks.hook import (
IOHook,
IOWriterHook,
OutputHook,
)
from benchkit.shell.command_execution.io.stream import (
EmptyIOStream,
ReadableIOStream,
WritableIOStream,
)
from benchkit.shell.command_execution.io.output import popen_get_output


def execute_command(
# needed for starting the command
command: List[str],
# This dir can only be a path on the local machine
current_dir: Optional[pathlib.Path] = None,
# Do we want to add os.environ to this?
environment: Optional[Dict[str, str]] = None,
# needed for construction and evaluation of output
timeout: Optional[int] = None,
ignore_ret_codes: Optional[Iterable[int]] = None,
success_value: int = 0,
# working with the IOStreams of the command
std_input: Optional[ReadableIOStream] = None,
ordered_input_hooks: Optional[List[IOHook]] = None,
ordered_output_hooks: Optional[List[OutputHook]] = None,
) -> CommandProcess:

if environment is None:
environment = {}

process = subprocess.Popen(
command,
cwd=current_dir,
env=environment,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
)
try:
# 1) manipulate the input stream using the ordered input hooks
if ordered_input_hooks is not None:
if std_input is None:
std_input = EmptyIOStream()
for inhook in ordered_input_hooks:
inhook.start_hook_function(std_input)
std_input = inhook.get_outgoing_io_stream()

# 2) Write the input to the command
# hookfunction to write the ReadableIOStream given as input to stdin
def pasalong(input_stream: ReadableIOStream, _: WritableIOStream) -> None:
if process.stdin is not None:
outline = input_stream.read(1)
while outline:
process.stdin.write(outline)
process.stdin.flush()
outline = input_stream.read(1)
process.stdin.close()

if std_input is not None:
hook = IOWriterHook(pasalong)
hook.start_hook_function(std_input)
if process.stdin is not None:
process.stdin.close()

# 3) manipulate teh output stream using the orderd output hooks
command_output = popen_get_output(process.stdout, process.stderr)

if ordered_output_hooks is not None:
for outhook in ordered_output_hooks:
command_output = outhook.attatch(command_output)

# close all the main thread file descriptors
if process.stdout is not None:
process.stdout.close()
if process.stderr is not None:
process.stderr.close()
if process.stdin is not None:
process.stdin.close()

# 4) construct the object we can use to monitor the process
return CommandProcess(
process, command_output, timeout, success_value, ignore_ret_codes
)

except Exception:
# make sure the process is terminated for cleanup
# TODO: this needs some test cases
process.terminate()
process.wait()
raise
107 changes: 107 additions & 0 deletions benchkit/shell/command_execution/io/hooks/basic_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright (C) 2025 Vrije Universiteit Brussel. All rights reserved.
# SPDX-License-Identifier: MIT

from __future__ import annotations # Otherwise Queue comlains about typing

from multiprocessing import Queue
from typing import Any
from pathlib import Path

from benchkit.shell.command_execution.io.hooks.hook import (
IOReaderHook,
IOResultHook,
IOWriterHook,
OutputHook,
)
from benchkit.shell.command_execution.io.stream import (
ReadableIOStream,
StringIOStream,
WritableIOStream,
try_converting_bystring_to_readable_characters,
)


def create_voiding_result_hook() -> IOResultHook:
def hook_function(
input_object: ReadableIOStream, _: WritableIOStream, result_queue: Queue[Any]
):
# we do not write to the out stream thus this is "voiding"
outlines: bytes = b""
outline = input_object.read(10)
while outline:
outlines += outline
outline = input_object.read(10)
result_queue.put(outlines)

return IOResultHook(hook_function)

def stream_prepend_hook(stream:StringIOStream):
def hook_function(
input_object: ReadableIOStream, output_object: WritableIOStream,
):
outline = stream.read(10)
while outline:
output_object.write(outline)
outline = input_object.read(10)
outline = input_object.read(10)
while outline:
output_object.write(outline)
outline = input_object.read(10)

return IOWriterHook(hook_function)

def write_to_file_hook(path:Path,mode:str="a"):
def hook_function(
input_object: ReadableIOStream,
):
with path.open(mode=f'{mode}b', buffering=0) as file:
outline = input_object.read(10)
while outline:
file.write(outline)
outline = input_object.read(10)
file.flush()

return IOReaderHook(hook_function)



def create_stream_line_logger_hook(formating_string: str) -> IOReaderHook:
def hook_function_line(input_object: ReadableIOStream):
byt = input_object.read_line()
while byt:
print(
formating_string.format(f"{try_converting_bystring_to_readable_characters(byt)}"),
end="",
)
byt = input_object.read_line()

return IOReaderHook(hook_function_line)


# TODO: Voiding can be done be done better but this will do for now
# problem: if there are hooks on the output they will wait for input still
# can be resolved by making use of EmptyIOStream
# Needs to be done on a higher level than hooks
def void_input(input_object: ReadableIOStream, _: WritableIOStream):
outline = input_object.read(10)
while outline:
outline = input_object.read(10)


def logger_line_hook(outformat: str, errformat: str):
return OutputHook(
create_stream_line_logger_hook(outformat),
create_stream_line_logger_hook(errformat),
)


def void_hook():
return OutputHook(IOWriterHook(void_input), IOWriterHook(void_input))


def std_out_result_void_err():
output_hook_object = create_voiding_result_hook()

voiding_result_hook = OutputHook(output_hook_object, IOWriterHook(void_input))

return (output_hook_object, voiding_result_hook)
Loading