Backround
I am calling an executable from Python and need to pass a variable to the executable. The executable however expects a file and does not read from stdin.
I circumvented that problem previously when using the subprocess module by simply calling the executable to read from /dev/stdin
along the lines of:
# with executable 'foo'
cmd = ['foo', '/dev/stdin']
input_variable = 'bar'
with subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as process:
stdout, stderr = process.communicate(input_variable)
print(f"{process.returncode}, {stdout}, {stderr}")
This worked fine so far. In order to add concurrency, I am now implementing asyncio and as such need to replace the subprocess module with the asyncio subprocess module.
Problem
Calling asyncio subprocess for a program using /dev/stdin
fails. Using the following async function:
import asyncio
async def invoke_subprocess(cmd, args, input_variable):
process = await asyncio.create_subprocess_exec(
cmd,
args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate(input=bytes(input_variable, 'utf-8'))
print(f"{process.returncode}, {stdout.decode()}, {stderr.decode()}")
This generally works for files, but fails for /dev/stdin
:
# 'cat' can be used for 'foo' to test the behavior
asyncio.run(invoke_subprocess('foo', '/path/to/file/containing/bar', 'not used')) # works
asyncio.run(invoke_subprocess('foo', '/dev/stdin', 'bar')) # fails with "No such device or address"
How can I call asyncio.create_subprocess_exec on /dev/stdin
?
Note: I have already tried and failed via asyncio.create_subprocess_shell and writing a temporary file is not an option as the file system is readonly.
Minimal example using 'cat'
Script main.py
:
import subprocess
import asyncio
def invoke_subprocess(cmd, arg, input_variable):
with subprocess.Popen(
[cmd, arg],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as process:
stdout, stderr = process.communicate(input_variable)
print(f"{process.returncode}, {stdout}, {stderr}")
async def invoke_async_subprocess(cmd, arg, input_variable):
process = await asyncio.create_subprocess_exec(
cmd,
arg,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate(input=input_variable)
print(f"{process.returncode}, {stdout.decode()}, {stderr.decode()}")
cmd = 'cat'
arg = '/dev/stdin'
input_variable = b'hello world'
# normal subprocess
invoke_subprocess(cmd, arg, input_variable)
asyncio.run(invoke_async_subprocess(cmd, arg, input_variable))
Returns:
> python3 main.py
0, b'hello world', b''
1, , cat: /dev/stdin: No such device or address
Tested on:
- Ubuntu 21.10, Python 3.9.7
- Linux Mint 20.2, Python 3.8.10
- Docker image: python:3-alpine
/dev/stdin
should bear no relation to how it was started. – Glynn/dev/stdin
or runs for some magical reason with different permissions – Gannesstrace -f
to check how it is being executed and (hopefully) what goes wrong. The "no such device or address" error is unusual, to say the least. – Glynncat
as a program as well which indicates that this may not be related to the program itself. – Gannesstrace -f
is primarily to check how asyncio is execing the program, especially when compared to what the regular subprocess module is doing. They are supposed to be doing the same thing. – Glynncat
)? If so, please edit the question to include it. Also, please state whether this is reproducible on multiple machines, or just on a machine with a particular kind of setup. In either case, please specify the type and version of the operating system you're testing it on. – Glynnstrace
confirms thatcat
is invoked correctly but (in the asyncio case) getsENXIO
when opening/dev/stdin
. It seems the result of an implementation detail of asyncio subprocess vs regular subprocess is interfering with this. To see the difference, changecmd, arg
to something like"ls", "-l", "/self/proc/fd/0"
. For subprocess you'll get something like/proc/self/fd/0 -> pipe:[7344202]
, whereas for asyncio you'll get/proc/self/fd/0 -> socket:[7339428]
. – Glynnsocket.socketpair()
to communicate with the subprocess, whereas subprocess uses a pipe. Asyncio claims that "not all platforms support selecting read events on the write end of a pipe", naming AIX in particular. This seemingly inoccuous change breaks re-opening of/dev/stdin
, which works with a pipe, but doesn't work with a socket. Bummer. – Glynn-
filename (esp. GNU tools likecat
) will be able to use stdin in this case. It will depend on the implementation of the OP'sfoo
program whether that is a useful workaround or not. – Docia