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/stdinshould bear no relation to how it was started./dev/stdinor runs for some magical reason with different permissionsstraceconfirms thatcatis invoked correctly but (in the asyncio case) getsENXIOwhen 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, argto 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].socket.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.-filename (esp. GNU tools likecat) will be able to use stdin in this case. It will depend on the implementation of the OP'sfooprogram whether that is a useful workaround or not.