Je dois écrire deux programmes qui seront exécutés en tant que processus parent et son enfant. Le processus parent génère l'enfant puis il communique via une paire de canaux connectés à ses stdin et stdout. La communication est peer-to-peer, c'est pourquoi j'ai besoin de l'asyncio. Une simple boucle de lecture/réponse ne fera pas l'affaire.
J'ai écrit le parent. Aucun problème, car asyncio
fournit tout ce dont j'avais besoin dans create_subprocess_exec()
.
Cependant, je ne sais pas comment créer un lecteur/graveur de flux similaire chez l'enfant. Je ne m'attendais à aucun problème. parce que les canaux sont déjà créés et que les descripteurs de fichier 0 et 1 sont prêts à être utilisés au démarrage du processus enfant. Aucune connexion ne doit être ouverte, aucun processus ne doit être généré.
C'est ma tentative qui ne marche pas:
import asyncio
import sys
_DEFAULT_LIMIT = 64 * 1024
async def connect_stdin_stdout(limit=_DEFAULT_LIMIT, loop=None):
if loop is None:
loop = asyncio.get_event_loop()
reader = asyncio.StreamReader(limit=limit, loop=loop)
protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
r_transport, _ = await loop.connect_read_pipe(lambda: protocol, sys.stdin)
w_transport, _ = await loop.connect_write_pipe(lambda: protocol, sys.stdout)
writer = asyncio.StreamWriter(w_transport, protocol, reader, loop)
return reader, writer
Le problème est que j'ai deux transports où je devrais en avoir un. La fonction échoue car elle tente de définir deux fois le transport du protocole:
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
await loop.connect_write_pipe(lambda: protocol, sys.stdout)
# !!!! assert self._transport is None, 'Transport already set'
J'ai essayé de passer un protocole factice à la première ligne, mais cette ligne n'est pas correcte non plus, car les deux transports sont nécessaires, pas un seul:
writer = asyncio.StreamWriter(w_transport, protocol, reader, loop)
Je suppose que je dois combiner deux transports unidirectionnels à un bidirectionnel en quelque sorte. Ou est-ce que mon approche est complètement fausse? Pourriez-vous s'il vous plaît me donner un conseil?
UPDATE: après quelques tests, cela semble fonctionner (mais ne me semble pas bon):
async def connect_stdin_stdout(limit=_DEFAULT_LIMIT, loop=None):
if loop is None:
loop = asyncio.get_event_loop()
reader = asyncio.StreamReader(limit=limit, loop=loop)
protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
dummy = asyncio.Protocol()
await loop.connect_read_pipe(lambda: protocol, sys.stdin) # sets read_transport
w_transport, _ = await loop.connect_write_pipe(lambda: dummy, sys.stdout)
writer = asyncio.StreamWriter(w_transport, protocol, reader, loop)
return reader, writer
Votre première version échoue car vous utilisez le mauvais protocole pour le côté rédacteur. StreamReaderProtocol
implémente des points d'ancrage pour réagir aux connexions entrantes et aux données, ce que le côté écriture n'a pas et ne devrait pas avoir à traiter.
La coroutine loop.connect_write_pipe()
utilise la fabrique de protocoles que vous avez transmise et renvoie l'instance de protocole résultante. Vous souhaitez utiliser le même objet de protocole dans le rédacteur de flux, au lieu du protocole utilisé pour le lecteur.
Ensuite, vous voulez que {not} _ veuille passer le lecteur stdin
au rédacteur de flux stdout
! Cette classe suppose que le lecteur et l'écrivain sont connectés au même descripteur de fichier, et ce n'est vraiment pas le cas ici.
Dans le récent passé j'ai construit ce qui suit pour gérer stdio pour un processus enfant; la fonction stdio()
est basée sur Nathan Hoad Gist sur le sujet , plus une solution de secours pour Windows où la prise en charge du traitement de stdio en tant que pipes est limitée .
Vous voulez que le rédacteur gère correctement la contre-pression, aussi ma version utilise-t-elle la classe (non documentée) asyncio.streams.FlowControlMixin
comme protocole pour cela; vous n'avez vraiment besoin de rien d'autre que ça:
import asyncio
import os
import sys
async def stdio(limit=asyncio.streams._DEFAULT_LIMIT, loop=None):
if loop is None:
loop = asyncio.get_event_loop()
if sys.platform == 'win32':
return _win32_stdio(loop)
reader = asyncio.StreamReader(limit=limit, loop=loop)
await loop.connect_read_pipe(
lambda: asyncio.StreamReaderProtocol(reader, loop=loop), sys.stdin)
writer_transport, writer_protocol = await loop.connect_write_pipe(
lambda: asyncio.streams.FlowControlMixin(loop=loop),
os.fdopen(sys.stdout.fileno(), 'wb'))
writer = asyncio.streams.StreamWriter(
writer_transport, writer_protocol, None, loop)
return reader, writer
def _win32_stdio(loop):
# no support for asyncio stdio yet on Windows, see https://bugs.python.org/issue26832
# use an executor to read from stdio and write to stdout
# note: if nothing ever drains the writer explicitly, no flushing ever takes place!
class Win32StdinReader:
def __init__(self):
self.stdin = sys.stdin.buffer
async def readline():
# a single call to sys.stdin.readline() is thread-safe
return await loop.run_in_executor(None, self.stdin.readline)
class Win32StdoutWriter:
def __init__(self):
self.buffer = []
self.stdout = sys.stdout.buffer
def write(self, data):
self.buffer.append(data)
async def drain(self):
data, self.buffer = self.buffer, []
# a single call to sys.stdout.writelines() is thread-safe
return await loop.run_in_executor(None, sys.stdout.writelines, data)
return Win32StdinReader(), Win32StdoutWriter()
Bien que peut-être un peu dépassé, j'ai trouvé ce billet de blog de Nathaniel J. Smith de 2016 sur l'asyncio et la curiosité être énormément utile pour comprendre comment l'asyncio, les protocoles, les transferts et la contre-pression et autres et accrocher ensemble. Cet article montre également pourquoi la création des objets de lecteur et d'écrivain pour stdio
est si prolixe et fastidieuse à l'heure actuelle.