4

I'm trying to run a Powershell subprocess from Python. I need to send Powershell code from Python to the child process. I've got this far:

import subprocess
import time

args = ["powershell", "-NoProfile", "-InputFormat None", "-NonInteractive"]

startTime = time.time()
process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

process.stdin.write("Write-Host 'FINISHED';".encode("utf-8"))

result = ''
while 'FINISHED' not in result:
    result += process.stdout.read(32).decode('utf-8')
    if time.time() > startTime + 5:
        raise TimeoutError(result)
print(result)

This times out, because nothing ever gets written to stdout. I think the Write-Host cmdlet never gets executed. Even the simple bash/Cygwin code echo "Write-Host 'FINISHED';" | powershell doesn't seem to do the job.

For comparison, sending the code block using the -Command flag works correctly.

How can I convince Powershell to run the code which I'm sending to stdin?

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157

2 Answers2

3

There a couple of things you can consider:

  1. Invoke PowerShell in a mode where you provide it with a script file which it should execute. Write this script file prior to calling the subprocess. Use the -File <FilePath> parameter for PowerShell (cf. the docs)

  2. If you really want to go with the stdin technique, you might be missing a newline character after the command. If this does not help, you might need to send another control character that tells PowerShell that input EOF is reached. You definitely need to consult the PowerShell docs for finding out how to 'terminate' commands on stdin. One thing you definitely need is the -Command - arguments: The value of Command can be "-", a string. or a script block. If the value of Command is "-", the command text is read from standard input. You may also want to look at this little hack: https://stackoverflow.com/a/13877874/145400

  3. If you only want to execute one command, you can simplify your code by using out, err = subprocess.communicate(in)

Community
  • 1
  • 1
Dr. Jan-Philip Gehrcke
  • 33,287
  • 14
  • 85
  • 130
  • I can't use a file or `communicate` because need to send a number of code blocks to a _single_ process. Both of those options cause the subprocess to exit. Forking more than one Powershell process is not an option, unfortunately. – Benjamin Hodgson Feb 17 '14 at 16:21
  • 1
    It definitely is possible to feed PowerShell with input from stdin. I have added some more information (about `-Command -`). This should get you started. – Dr. Jan-Philip Gehrcke Feb 17 '14 at 16:26
  • So unfortunately it looks like Powershell doesn't execute the code you piped in until it recieves EOF. So what I'm trying to do isn't possible without using some sort of message passing, which is too much complexity for my application. Oh well :( – Benjamin Hodgson Feb 17 '14 at 17:08
  • That's unfortunate. Does the script (the PS input) depend on the PS output? If not, then you can generate the whole PS script before executing it, right? Also, you might want to execute PS multiple times, one after another. It might create significant overhead (for process creation etc), but might still work very well and be the simplest architecture. – Dr. Jan-Philip Gehrcke Feb 17 '14 at 17:11
  • I'm doing cross-process testing; Python sends commands to Powershell and makes assertions about what happens. So Python needs a response right away - I can't bundle all the commands up and send them at the end. Forking multiple Powershell processes is an option, but _boy_ is it slow! That's why I was looking into this in the first place. – Benjamin Hodgson Feb 18 '14 at 08:22
3

I had trouble with a similar task, but I was able to solve it.

First my example code:

import subprocess

args = ["powershell.exe", "-Command", r"-"]
process = subprocess.Popen(args, stdin = subprocess.PIPE, stdout =   subprocess.PIPE)

process.stdin.write(b"$data = Get-ChildItem C:\\temp\r\n")
process.stdin.write(b"Write-Host 'Finished 1st command'\r\n")
process.stdin.write(b"$data | Export-Clixml -Path c:\\temp\state.xml\r\n")
process.stdin.write(b"Write-Host 'Finished 2nd command'\r\n")

output = process.communicate()[0]

print(output.decode("utf-8"))
print("done")

The main issue was the correct argument list args. It is required to start the powershell with the -Command-flag, followed by "-" as indicated by Jan-Philipp.

Another mystery was the end-of-line character that is required to get the stuff executed. \r\n works quite well.

Getting the output of the Powershell is still an issue. But if you don't care about realtime, you can collect the output after finishing all executions by calling

output = process.communicate()[0]

However, the active Powershell will be terminated afterwards.

RaJa
  • 1,471
  • 13
  • 17