What I'm trying to do
I'm trying to emulate the behavior of the following simple socat(1) command:
socat tcp-listen:SOME_PORT,fork,reuseaddr exec:'SOME_PROGRAM'
The above command creates a forking TCP server which forks and executes SOME_PROGRAM for each connection, redirecting both stdin and stdout of said command to the TCP socket.
Here's what I'd like to achieve:
- Create a simple TCP server with
asyncioto handle multiple concurrent connections. - Whenever a connection is received, start
SOME_PROGRAMas a sub-process. - Pass any data received from the socket to
SOME_PROGRAM's standard input. - Pass any data received from
SOME_PROGRAM's standard output to the socket. - When
SOME_PROGRAMexits, write a goodbye message along with the exit code to the socket and close the connection.
I would like to do this in pure Python, without using external libraries using the asyncio module.
What I have so far
Here's the code I wrote so far:
import asyncio
class ServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.client_addr = transport.get_extra_info('peername')
self.transport = transport
self.child_process = None
print('Connection with {} enstablished'.format(self.client_addr))
asyncio.ensure_future(self._create_subprocess())
def connection_lost(self, exception):
print('Connection with {} closed.'.format(self.client_addr))
if self.child_process.returncode is not None:
self.child_process.terminate()
def data_received(self, data):
print('Data received: {!r}'.format(data))
# Make sure the process has been spawned
# Does this even make sense? Looks so awkward to me...
while self.child_process is None:
continue
# Write any received data to child_process' stdin
self.child_process.stdin.write(data)
async def _create_subprocess(self):
self.child_process = await asyncio.create_subprocess_exec(
*TARGET_PROGRAM,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE
)
# Start reading child stdout
asyncio.ensure_future(self._pipe_child_stdout())
# Ideally I would register some callback here so that when
# child_process exits I can write to the socket a goodbye
# message and close the connection, but I don't know how
# I could do that...
async def _pipe_child_stdout(self):
# This does not seem to work, this function returns b'', that is an
# empty buffer, AFTER the process exits...
data = await self.child_process.stdout.read(100) # Arbitrary buffer size
print('Child process data: {!r}'.format(data))
if data:
# Send to socket
self.transport.write(data)
# Reschedule to read more data
asyncio.ensure_future(self._pipe_child_stdout())
SERVER_PORT = 6666
TARGET_PROGRAM = ['./test']
if __name__ == '__main__':
loop = asyncio.get_event_loop()
coro = loop.create_server(ServerProtocol, '0.0.0.0', SERVER_PORT)
server = loop.run_until_complete(coro)
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
And also the ./test program I'm trying to run as subprocess:
#!/usr/bin/env python3
import sys
if sys.stdin.read(2) == 'a\n':
sys.stdout.write('Good!\n')
else:
sys.exit(1)
if sys.stdin.read(2) == 'b\n':
sys.stdout.write('Wonderful!\n')
else:
sys.exit(1)
sys.exit(0)
Unfortunately the above code doesn't really work, and I'm kind of lost about what to try next.
What works as intended:
- The child process is correctly spawned, and also seems to correctly receive the input from the socket, because I can see it from
htopand I can also see that as soon as I sendb\nit terminates.
What doesn't work as intended:
Basically anything else...
- The child process' output is never sent to the socket, and actually never read at all. The call
await self.child_process.stdout.read(100)never seems to terminate: instead, it only terminates after the child process dies and the result is justb''(an emptybytesobject). - I'm not able to understand when the child process terminates: as I mentioned above, I'd like to send a "Goodbye" message to the socket along with
self.child_process.returncodewhen this happens, but I don't know how to do this in a way which makes sense.
What I tried:
- I tried creating the child process using
asyncio.loop.subprocess_exec()instead ofasyncio.create_subprocess_exec(). This solves the problem of knowing when the process terminates, since I can instantiate a subclass ofasyncio.SubprocessProtocoland use itsprocess_exited()method, but doesn't really help me at all, since if I do it this way I don't have a mean to talk to the process'stdinorstdoutanymore! That is, I don't have aProcessobject to interact with... - I tried playing around with
asyncio.loop.connect_write_pipe()andloop.connect_read_pipe()with no luck.
Questions
So, could someone please help me figure out what am I doing wrong? There has to be a way to make this work smoothly. When I first started, I was looking for a way to just effortlessly use some pipe redirection, but I don't know if that's even possible at this point. Is it? It looks like it should be.
asyncio. What I don't know is how to get the clients to talk to a subprocess.asyncio.open_connectionin the client handler withasyncio.create_subprocess_exec