5

I am dealing with a Python script which does, after some preparation work, launch ssh. My script is actually a small CLI tool. On Unix-like systems, at the end of its life, the Python script replaces itself with the ssh client, so the user can now interact with ssh directly (i.e. run arbitrary commands on the remote machine etc):

os.execvpe('ssh', ['ssh', '-o', 'foo', 'user@host'], os.environ)

Positive surprise & side-note in case you are wondering: Windows 10 actually has a native version of OpenSSH now built-in, so there is a ssh command on this platform.

os.execvpe is present in the Python standard library on Windows, but it does not replace the original (Python) process. The situation is ... somewhat complicated: 1, 2, 3. Bottom line: Windows does not implement the corresponding POSIX semantics for replacing a running process.

The common wisdom is to use subprocess.Popen instead, ok, effectively creating a child process. I can launch the child so that the parent keeps running OR I can launch the child while the parent dies (I think that Windows does support the latter just like Unix-like systems). Either way, the user can not interact with the child in the command line.

Assuming that I keep the parent alive, I now have to write a ton of code to pass user I/O to/from the child through the parent, like so for instance. The latter involves managing streams and even threads, depending on how well it is supposed to behave - a lot of places for potential issues and breakages down the road. I do not like to do this (if I can avoid it).

How can I efficiently replace os.execvpe on Windows in the described scenario?


EDIT (1): Bits and pieces, which may be relevant ...

I guess it depends on figuring out how to correctly configure a STARTUPINFO object before passing it into Popen. A command line can in fact be inherited in Windows.


EDIT (2): A partial solution via pywin32 - ssh opens into a second, new cmd window and can be interacted with. The original shell with Python remains open, Python itself quits:

from win32.Demos.winprocess import Process
from shlex import join
Process(join(['ssh', '-o', 'foo', 'user@host']))
s-m-e
  • 3,433
  • 2
  • 34
  • 71
  • You might want to check https://pexpect.readthedocs.io/en/stable. – CristiFati May 12 '21 at 13:37
  • @CristiFati Wrapping `ssh` in `expect` - it's a variation of "manually" passing through all user I/O. At the end of the day, it is still rather complicated, but thanks for the idea. – s-m-e May 12 '21 at 14:00

2 Answers2

0

A partial and incomplete solution looks about as follows, see TODO comments:

import win32api, win32process, win32con
from shlex import join

si = win32process.STARTUPINFO()

# TODO fix flags
si.dwFlags = win32con.STARTF_USESTDHANDLES ^ win32con.STARTF_USESHOWWINDOW

# inherit stdin, stdout and stderr
si.hStdInput = win32api.GetStdHandle(win32api.STD_INPUT_HANDLE)
si.hStdOutput = win32api.GetStdHandle(win32api.STD_OUTPUT_HANDLE)
si.hStdError = win32api.GetStdHandle(win32api.STD_ERROR_HANDLE)

# TODO fix value?
si.wShowWindow = 1

# TODO set values?
# si.dwX, si.dwY = ...
# si.dwXSize, si.dwYSize = ...
# si.lpDesktop = ...

procArgs = (
    None,  # appName
    join(['ssh', '-o', 'foo', 'user@host']),  # commandLine
    None,  # processAttributes
    None,  # threadAttributes
    1,  # bInheritHandles TODO ?
    win32process.CREATE_NEW_CONSOLE,  # dwCreationFlags
    None,  # newEnvironment
    None,  # currentDirectory
    si, # startupinfo
)

procHandles = win32process.CreateProcess(*procArgs) # run ...

ssh opens into a second, new cmd.exe window and can be interacted with. The original cmd.exe window with Python in it remains open, Python itself quits, returning control to cmd.exe itself. It is usable, although inconsistent and ugly.

I guess it comes down to configuring win32process.STARTUPINFO correctly, but even after heaving read tons of documentation on it, I am somehow failing to make sense of it ...

s-m-e
  • 3,433
  • 2
  • 34
  • 71
-2

You can use subprocess.Popen or or subprocess.call function instead of os.execvpe. They have flag shell which ensures that child process can get stdin. I have tried in windows using following code:

import os
import subprocess
subprocess.Popen('ssh -o foo user@host', shell=True, env=os.environ)

And it works.

Elbek
  • 616
  • 1
  • 4
  • 15
  • @s-m-e, can you elaborate, is there something I missed in question? – Elbek May 12 '21 at 09:15
  • I want to launch `ssh` so a user can *interactively* use it. My Python script runs in the command line. So does `ssh`. `ssh` is supposed to *inherit* the command line from the Python process so a user can actually *interact* with `ssh`. Your suggestion launches `ssh`, but it does not turn control over to it. Instead, it dies in the background together with my Python script. – s-m-e May 12 '21 at 09:32