-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add PersistentSubShell
-feature
#596
Conversation
Multiple commands can be executed in the same >>> from datalad_next.shell import shell
>>> with shell(['ssh', 'localhost']) as ssh:
... r = ssh(b'ls -l /etc/passwd')
... print(tuple(r))
... print(r.returncode)
... r = ssh(b'ls -l /etc/fstab')
... print(tuple(r))
... print(r.returncode)
...
(b'-rw-r--r-- 1 root root 2773 14. Nov 10:05 /etc/passwd\n',)
0
(b'-rw-r--r-- 1 root root 1534 15. Sep 2022 /etc/fstab')
0 If a command in the shell exits with a non-zero return-code a >>> with shell(['ssh', 'localhost']) as ssh:
... r = ssh(b'ls no-such-file')
... print(tuple(r))
...
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
File "/usr/lib64/python3.10/_collections_abc.py", line 330, in __next__
return self.send(None)
File "/home/cristian/Develop/datalad-next/datalad_next/shell/response_generators.py", line 155, in send
self.check_result()
File "/home/cristian/Develop/datalad-next/datalad_next/shell/response_generators.py", line 52, in check_result
raise CommandError(
datalad.runner.exception.CommandError: CommandError: 'ls no-such-file ; x=$?; echo -e -n "----datalad-end-marker-3633463527-rekram-dne-dalatad----\n"; echo $x
' failed with exitcode 2 [err: 'Pseudo-terminal will not be allocated because stdin is not a terminal.
ls: Zugriff auf 'no-such-file' nicht möglich: Datei oder Verzeichnis nicht gefunden']
>>> The commands that are executed in the shell can read their >>> with shell(['ssh', 'localhost']) as ssh:
... r = ssh(b'hexdump -n 10 -C', stdin=iter([b'0123456789']))
... print(tuple(r))
... r = ssh(b'hexdump -n 5 -C', stdin=iter([b'abcde']))
... print(tuple(r))
...
(b'00000000 30 31 32 33 34 35 36 37 38 39 |0123456789|\n0000000a\n',)
(b'00000000 61 62 63 64 65 |abcde|\n00000005\n',) A
The PR contains a few useful response generator classes. Among them are
>>> with shell(['powershell', '-Command', '-'], zero_command_rg_class=VariableLengthResponseGeneratorPowerShell) as pwsh:
... r = pwsh(b'Get-ChildItem -Path \\')
... print(b''.join(r).decode())
...
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 12/15/2023 11:38 AM DLTMP
d-r--- 12/15/2023 11:09 AM Program Files
d-r--- 9/21/2023 6:54 AM Program Files (x86)
d----- 12/6/2023 11:56 AM TMP
d-r--- 9/1/2023 10:29 AM Users
da---- 1/4/2024 11:04 AM Windows
-a---- 5/12/2023 4:28 AM 112080 appverifUI.dll
-a---- 5/12/2023 4:29 AM 66160 vfcompat.dll
Further response generators are: For POSIX-shells there is a |
55c682a
to
90b5547
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #596 +/- ##
==========================================
+ Coverage 93.05% 93.24% +0.18%
==========================================
Files 171 178 +7
Lines 11945 12478 +533
Branches 1806 1890 +84
==========================================
+ Hits 11116 11635 +519
- Misses 642 651 +9
- Partials 187 192 +5 ☔ View full report in Codecov by Sentry. |
90b5547
to
f19e680
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I played with it and it works for me.
Here are some first thoughts.
I think we should carefully decide what features go into the base class implementation. From my POV there is nothing inherently "SSH" about this functionality and also nothing inherently "POSIX".
The present up/download/delete implementation assumes a POSIX environment, but this is certainly not without alternative. This suggests that this feature should be in a separate class.
f19e680
to
c08cfff
Compare
This commit addresses the bug described in: <datalad#596 (comment)>. The reason for the `AttributeError` was that un-caught `StopIteration` exceptions prevented an initialization of the `returncode`-attribute. This is fixed by initializing the attribute in the constructor of `ShellCommandResponseGenerator`. The commit adds a regression test that ensures that the correct error is signaled.
I see it the same way.
I interpreted issue #572 as assuming a POSIX environment on the server.
Yes, I agree. Independent of the server environment, it would be better to factor out individual operation implementations and allow for flexible "mix and match". Will do that now. The module |
This commit addresses reviewer comment <datalad#596 (comment)>. It exposes `chunk_size` in `shell_connection` and documents its significance for the size of the `stderr`-queue that is accessible via the `stderr_deque`-attribute of a `ShellCommandResponseGenerator`-object.
This commit addresses reviewer comment: <datalad#596 (comment)> It renames `ShellCommandExecutor.execute_command` to `ShellCommandExecutor.__call__`. With that change the following code becomes possible: >>> with shell_connection(['bash']) as shell: ... r = shell(b'uname')
This commit addresses reviewer comment: <datalad#596 (comment)>. It renames `datalad_next.utils.shell_connection.shell_connection` to `datalad_next.utils.shell.shell`. It also renames `datalad_next.utils.tests.test_shell_connection` to `datalad_next.utils.tests.test_shell`.
Just to ensure that I interpret this correctly. We would like to support Supported client environments:
Server side"Server side" refers to the environment in which the connected shell is executed. This environment mainly impacts two things. Firstly, how commands are augmented to allow output and return code detection. Secondly, which commands are sent to perform certain operations. My understanding was that we focus on Posix-server side, i.e. we connect to shells in a Posix environment (in fact the command-augmentation and the initial command that is used to skip login-messages are Posix-specific). So the following is the current state of support: Supported server environments:
I interpret the comment in such a way that we want to support Windows and OSX server (or at least create the possibility to support then in the future) too. Is that correct? ping @mih |
This commit addresses the bug described in: <datalad#596 (comment)>. The reason for the `AttributeError` was that un-caught `StopIteration` exceptions prevented an initialization of the `returncode`-attribute. This is fixed by initializing the attribute in the constructor of `ShellCommandResponseGenerator`. The commit adds a regression test that ensures that the correct error is signaled.
This commit addresses reviewer comment <datalad#596 (comment)>. It exposes `chunk_size` in `shell_connection` and documents its significance for the size of the `stderr`-queue that is accessible via the `stderr_deque`-attribute of a `ShellCommandResponseGenerator`-object.
This commit addresses reviewer comment: <datalad#596 (comment)> It renames `ShellCommandExecutor.execute_command` to `ShellCommandExecutor.__call__`. With that change the following code becomes possible: >>> with shell_connection(['bash']) as shell: ... r = shell(b'uname')
This commit addresses reviewer comment: <datalad#596 (comment)>. It renames `datalad_next.utils.shell_connection.shell_connection` to `datalad_next.utils.shell.shell`. It also renames `datalad_next.utils.tests.test_shell_connection` to `datalad_next.utils.tests.test_shell`.
ad0b555
to
de9a949
Compare
Notes on a conversation with @christian-monch in another channel. This PR is considered done. The issue of client code having to take care of the features of the shell that is being connected to is not to be addressed within this PR. A review needs to pay attention to properly labeling of implementations (e.g., check that the This PR is self-contained, i.e. has the feature implementation and tests for it. However, the feature is added only, and no existing alternative implementations are consolidated on it. A reviewer needs to go through these existing implementations and determine whether they can be replaced with this feature, and if not, why not (candidates to look at: uncurl, (ssh) test helpers (and potentially others), url_operations). |
from .operations import posix | ||
from .operations.posix import ( | ||
DownloadResponseGenerator, | ||
DownloadResponseGeneratorPosix, | ||
) | ||
from .response_generators import ( | ||
FixedLengthResponseGenerator, | ||
FixedLengthResponseGeneratorPosix, | ||
FixedLengthResponseGeneratorPowerShell, | ||
ShellCommandResponseGenerator, | ||
VariableLengthResponseGenerator, | ||
VariableLengthResponseGeneratorPosix, | ||
VariableLengthResponseGeneratorPowerShell, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a common naming/selection strategy?
Client code must choose the current implementation for there use case, but it seems a simple label for a shell-setup is not sufficient. We have a posix
operations module, but no one for response generators.
Why not have one posix
module and it has all pieces needed?
Along the same lines, I think we should also avoid that client code has to switch class names per use case. Now thye need to select a module to import from and what to import also. This means that client code also needs to maintain a mapping of the API. If we would provide modules with the exact same API, client code could be simplified to just get the label of the "platform/shell-type" module to work with.
I also want to point out that the promise is that everything imported from datalad_next.shell
is stable (or needs a minor/major release to change). We import the entire posix
module, which not only has the pieces that aim to be a "public" API. I think this calls for a dedicated module that exposes the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into replacing the implementation of assert_ssh_access
with this functionality. Here is some code that I came up with, based on the existing documentation
ssh_bash_call = [
ssh_bin,
'-i', seckey,
'-p', port,
f'{login}@{host}',
'bash',
]
from more_itertools import consume
with shell(ssh_bash_call) as ssh:
consume(ssh(f'mkdir -p {path}'))
consume(ssh(f'touch {path}/datalad-tests-probe'))
Two main issues confused me. The top-level documentation makes the impression that all commands need to be byte-strings (so no f-string formatting is possible). But fortunately this is not true.
It took me a moment to understand the implications of each line of the doc example shell()
.
print(b''.join(result))
Is not just a demo. But the command associated with result
does not actually run without it.
I think that ShellCommandExecutor.__call__
should not have this behavior. myshell(something)
appears to execute a command, but it really gives a handler that needs more boilerplate code to actually do things. At minimum something like
consume(myshell(something))
I believe __call__()
should better do that. Maybe have a check=
like subprocess.run()
.
Strangely, when I run a command that fails, I get a CommandError
immediately. The docs suggest that there should be no exception, but I have to inspect a returncode
manually.
In some sense I am glad that I do not have to implement a custom error handler for each execution, but I am also not sure if I am doing things correctly.
Overall, I feel we need more examples in the top-level docs. Use cases are similar to the call_git
helpers:
- run a command that is expected to work, raise exception if not
- check if a command fails or not, return bool
- run a command that is expected to fail, act on returncode
- run a command that produces output, variants
- expect and produce one line, fail if more
- expect and produce any number of lines
A thanks for catching that, I modified the code according to a reviewer-comment and forgot to change the docs.
We can do that, it would mean all output is assembled before it is returned to the user. I assume that we don't expect long slow outputs, e.g.
Thanks, will fix the docs (also an oversight after addressing a reviewer comment).
These are very useful suggestions, I suggest doing the following:
|
See datalad#654 Like require employing datalad#596 to solve properly.
TODO: update docstrings 1. make shell.__call__ consume everything 2. add shell.start 3. don't raise CommandError by default 4. add check to enable CommandError-raising 5. small improvements 6. extend tests
96310c9
to
1a99586
Compare
Previously, this has been a series of SSH calls, each running in its own shell. Now a persistent shell is used to interleave remote and local checks.
This commit adapts the changes made by mih in his forked branch to the changes that were made in this branch in the meantime.
Co-authored-by: Michael Hanke <[email protected]>
I will merge this now. There are some unresolved things and tests should still run. But there are too many things pending and we will work on them in parallel for better throughput. |
This PR addresses issue #572
It provides a
shell_connection
-context manager that will start a shell, for example, a local ssh-client, and allows interaction with the shell-process. Interactions are started by sending a command, which will be executed in the shell. The user will then receive the command'sstdout
. When the command terminates, the user can retrieve the exit code of the command.Usage example: