python-plugin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyplugin/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ """pyplugin — wire-compatible Python port of HashiCorp's go-plugin."""
2
+ from __future__ import annotations
3
+
4
+ from .broker import GRPCBroker
5
+ from .client import Client, ClientConfig
6
+ from .errors import (
7
+ AppProtocolMismatch,
8
+ CoreProtocolMismatch,
9
+ HandshakeError,
10
+ MagicCookieMismatch,
11
+ ProcessExitedError,
12
+ PyPluginError,
13
+ StartTimeout,
14
+ TLSError,
15
+ UnsupportedProtocol,
16
+ )
17
+ from .handshake import (
18
+ CORE_PROTOCOL_VERSION,
19
+ HandshakeConfig,
20
+ HandshakeLine,
21
+ NETWORK_TCP,
22
+ NETWORK_UNIX,
23
+ PROTOCOL_GRPC,
24
+ )
25
+ from .plugin import Plugin, PluginSet, VersionedPlugins
26
+ from .reattach import ReattachConfig
27
+ from .server import ServeConfig, serve
28
+
29
+ __all__ = [
30
+ "CORE_PROTOCOL_VERSION",
31
+ "Client",
32
+ "ClientConfig",
33
+ "GRPCBroker",
34
+ "HandshakeConfig",
35
+ "HandshakeLine",
36
+ "NETWORK_TCP",
37
+ "NETWORK_UNIX",
38
+ "PROTOCOL_GRPC",
39
+ "Plugin",
40
+ "PluginSet",
41
+ "ReattachConfig",
42
+ "ServeConfig",
43
+ "VersionedPlugins",
44
+ "serve",
45
+ "PyPluginError",
46
+ "HandshakeError",
47
+ "CoreProtocolMismatch",
48
+ "AppProtocolMismatch",
49
+ "UnsupportedProtocol",
50
+ "MagicCookieMismatch",
51
+ "ProcessExitedError",
52
+ "StartTimeout",
53
+ "TLSError",
54
+ ]
File without changes
@@ -0,0 +1,40 @@
1
+ # Generated by the Protocol Buffers compiler. DO NOT EDIT!
2
+ # source: grpc_broker.proto
3
+ # plugin: grpclib.plugin.main
4
+ import abc
5
+ import typing
6
+
7
+ import grpclib.const
8
+ import grpclib.client
9
+ if typing.TYPE_CHECKING:
10
+ import grpclib.server
11
+
12
+ from . import grpc_broker_pb2
13
+
14
+
15
+ class GRPCBrokerBase(abc.ABC):
16
+
17
+ @abc.abstractmethod
18
+ async def StartStream(self, stream: 'grpclib.server.Stream[grpc_broker_pb2.ConnInfo, grpc_broker_pb2.ConnInfo]') -> None:
19
+ pass
20
+
21
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
22
+ return {
23
+ '/plugin.GRPCBroker/StartStream': grpclib.const.Handler(
24
+ self.StartStream,
25
+ grpclib.const.Cardinality.STREAM_STREAM,
26
+ grpc_broker_pb2.ConnInfo,
27
+ grpc_broker_pb2.ConnInfo,
28
+ ),
29
+ }
30
+
31
+
32
+ class GRPCBrokerStub:
33
+
34
+ def __init__(self, channel: grpclib.client.Channel) -> None:
35
+ self.StartStream = grpclib.client.StreamStreamMethod(
36
+ channel,
37
+ '/plugin.GRPCBroker/StartStream',
38
+ grpc_broker_pb2.ConnInfo,
39
+ grpc_broker_pb2.ConnInfo,
40
+ )
@@ -0,0 +1,41 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: grpc_broker.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'grpc_broker.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11grpc_broker.proto\x12\x06plugin\"\x9b\x01\n\x08\x43onnInfo\x12\x12\n\nservice_id\x18\x01 \x01(\r\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12%\n\x05knock\x18\x04 \x01(\x0b\x32\x16.plugin.ConnInfo.Knock\x1a\x32\n\x05Knock\x12\r\n\x05knock\x18\x01 \x01(\x08\x12\x0b\n\x03\x61\x63k\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t2C\n\nGRPCBroker\x12\x35\n\x0bStartStream\x12\x10.plugin.ConnInfo\x1a\x10.plugin.ConnInfo(\x01\x30\x01\x42\x30Z.github.com/hashicorp/go-plugin/internal/pluginb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_broker_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'Z.github.com/hashicorp/go-plugin/internal/plugin'
35
+ _globals['_CONNINFO']._serialized_start=30
36
+ _globals['_CONNINFO']._serialized_end=185
37
+ _globals['_CONNINFO_KNOCK']._serialized_start=135
38
+ _globals['_CONNINFO_KNOCK']._serialized_end=185
39
+ _globals['_GRPCBROKER']._serialized_start=187
40
+ _globals['_GRPCBROKER']._serialized_end=254
41
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,40 @@
1
+ # Generated by the Protocol Buffers compiler. DO NOT EDIT!
2
+ # source: grpc_controller.proto
3
+ # plugin: grpclib.plugin.main
4
+ import abc
5
+ import typing
6
+
7
+ import grpclib.const
8
+ import grpclib.client
9
+ if typing.TYPE_CHECKING:
10
+ import grpclib.server
11
+
12
+ from . import grpc_controller_pb2
13
+
14
+
15
+ class GRPCControllerBase(abc.ABC):
16
+
17
+ @abc.abstractmethod
18
+ async def Shutdown(self, stream: 'grpclib.server.Stream[grpc_controller_pb2.Empty, grpc_controller_pb2.Empty]') -> None:
19
+ pass
20
+
21
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
22
+ return {
23
+ '/plugin.GRPCController/Shutdown': grpclib.const.Handler(
24
+ self.Shutdown,
25
+ grpclib.const.Cardinality.UNARY_UNARY,
26
+ grpc_controller_pb2.Empty,
27
+ grpc_controller_pb2.Empty,
28
+ ),
29
+ }
30
+
31
+
32
+ class GRPCControllerStub:
33
+
34
+ def __init__(self, channel: grpclib.client.Channel) -> None:
35
+ self.Shutdown = grpclib.client.UnaryUnaryMethod(
36
+ channel,
37
+ '/plugin.GRPCController/Shutdown',
38
+ grpc_controller_pb2.Empty,
39
+ grpc_controller_pb2.Empty,
40
+ )
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: grpc_controller.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'grpc_controller.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15grpc_controller.proto\x12\x06plugin\"\x07\n\x05\x45mpty2:\n\x0eGRPCController\x12(\n\x08Shutdown\x12\r.plugin.Empty\x1a\r.plugin.EmptyB0Z.github.com/hashicorp/go-plugin/internal/pluginb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_controller_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'Z.github.com/hashicorp/go-plugin/internal/plugin'
35
+ _globals['_EMPTY']._serialized_start=33
36
+ _globals['_EMPTY']._serialized_end=40
37
+ _globals['_GRPCCONTROLLER']._serialized_start=42
38
+ _globals['_GRPCCONTROLLER']._serialized_end=100
39
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,41 @@
1
+ # Generated by the Protocol Buffers compiler. DO NOT EDIT!
2
+ # source: grpc_stdio.proto
3
+ # plugin: grpclib.plugin.main
4
+ import abc
5
+ import typing
6
+
7
+ import grpclib.const
8
+ import grpclib.client
9
+ if typing.TYPE_CHECKING:
10
+ import grpclib.server
11
+
12
+ import google.protobuf.empty_pb2
13
+ from . import grpc_stdio_pb2
14
+
15
+
16
+ class GRPCStdioBase(abc.ABC):
17
+
18
+ @abc.abstractmethod
19
+ async def StreamStdio(self, stream: 'grpclib.server.Stream[google.protobuf.empty_pb2.Empty, grpc_stdio_pb2.StdioData]') -> None:
20
+ pass
21
+
22
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
23
+ return {
24
+ '/plugin.GRPCStdio/StreamStdio': grpclib.const.Handler(
25
+ self.StreamStdio,
26
+ grpclib.const.Cardinality.UNARY_STREAM,
27
+ google.protobuf.empty_pb2.Empty,
28
+ grpc_stdio_pb2.StdioData,
29
+ ),
30
+ }
31
+
32
+
33
+ class GRPCStdioStub:
34
+
35
+ def __init__(self, channel: grpclib.client.Channel) -> None:
36
+ self.StreamStdio = grpclib.client.UnaryStreamMethod(
37
+ channel,
38
+ '/plugin.GRPCStdio/StreamStdio',
39
+ google.protobuf.empty_pb2.Empty,
40
+ grpc_stdio_pb2.StdioData,
41
+ )
@@ -0,0 +1,42 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: grpc_stdio.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'grpc_stdio.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
26
+
27
+
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10grpc_stdio.proto\x12\x06plugin\x1a\x1bgoogle/protobuf/empty.proto\"u\n\tStdioData\x12*\n\x07\x63hannel\x18\x01 \x01(\x0e\x32\x19.plugin.StdioData.Channel\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\".\n\x07\x43hannel\x12\x0b\n\x07INVALID\x10\x00\x12\n\n\x06STDOUT\x10\x01\x12\n\n\x06STDERR\x10\x02\x32G\n\tGRPCStdio\x12:\n\x0bStreamStdio\x12\x16.google.protobuf.Empty\x1a\x11.plugin.StdioData0\x01\x42\x30Z.github.com/hashicorp/go-plugin/internal/pluginb\x06proto3')
29
+
30
+ _globals = globals()
31
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
32
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_stdio_pb2', _globals)
33
+ if not _descriptor._USE_C_DESCRIPTORS:
34
+ _globals['DESCRIPTOR']._loaded_options = None
35
+ _globals['DESCRIPTOR']._serialized_options = b'Z.github.com/hashicorp/go-plugin/internal/plugin'
36
+ _globals['_STDIODATA']._serialized_start=57
37
+ _globals['_STDIODATA']._serialized_end=174
38
+ _globals['_STDIODATA_CHANNEL']._serialized_start=128
39
+ _globals['_STDIODATA_CHANNEL']._serialized_end=174
40
+ _globals['_GRPCSTDIO']._serialized_start=176
41
+ _globals['_GRPCSTDIO']._serialized_end=247
42
+ # @@protoc_insertion_point(module_scope)
pyplugin/broker.py ADDED
@@ -0,0 +1,225 @@
1
+ """GRPCBroker — bidi sub-channel multiplexer (grpclib async).
2
+
3
+ Same semantics as go-plugin's non-multiplexed broker: ``accept_and_serve(id, …)``
4
+ opens a fresh listener and sends a ``ConnInfo`` over the broker stream;
5
+ ``dial(id)`` waits for that ``ConnInfo`` and opens a channel to the address.
6
+ A demux task pumps inbound stream messages into per-id ``asyncio.Queue``s.
7
+
8
+ Multiplexing the broker over the main socket (``PLUGIN_MULTIPLEX_GRPC``) is
9
+ not implemented — we always advertise ``false`` if the env var is set.
10
+
11
+ Sub-channels use mTLS reusing the same cert material as the main channel:
12
+ each side has its leaf cert and key, plus the peer's cert pinned as trust root.
13
+ We hold those PEMs on the broker so we can build the correct SSL context
14
+ (server-side for ``accept_and_serve``, client-side for ``dial``).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import itertools
20
+ import ssl
21
+ from dataclasses import dataclass
22
+ from typing import Awaitable, Callable, Optional
23
+
24
+ from grpclib.client import Channel
25
+ from grpclib.config import Configuration
26
+ from grpclib.server import Server, Stream
27
+
28
+ from . import mtls
29
+ from ._generated import grpc_broker_grpc, grpc_broker_pb2
30
+ from .transport import open_listener
31
+
32
+
33
+ _DIAL_TIMEOUT = 5.0
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class TLSMaterial:
38
+ """PEM bytes used to derive per-direction SSL contexts."""
39
+ cert_pem: bytes
40
+ key_pem: bytes
41
+ peer_cert_pem: bytes
42
+
43
+
44
+ class _ServerStreamServicer(grpc_broker_grpc.GRPCBrokerBase):
45
+ """Plugin-side bridge: pumps a single bidi stream into per-side queues."""
46
+
47
+ def __init__(self) -> None:
48
+ self.incoming: asyncio.Queue[grpc_broker_pb2.ConnInfo | None] = asyncio.Queue()
49
+ self.outgoing: asyncio.Queue[grpc_broker_pb2.ConnInfo | None] = asyncio.Queue()
50
+ self.connected = asyncio.Event()
51
+
52
+ async def StartStream(self, stream: Stream) -> None: # noqa: N802
53
+ self.connected.set()
54
+
55
+ async def reader() -> None:
56
+ try:
57
+ async for msg in stream:
58
+ await self.incoming.put(msg)
59
+ finally:
60
+ await self.incoming.put(None)
61
+
62
+ async def writer() -> None:
63
+ while True:
64
+ msg = await self.outgoing.get()
65
+ if msg is None:
66
+ return
67
+ await stream.send_message(msg)
68
+
69
+ await asyncio.gather(reader(), writer(), return_exceptions=True)
70
+
71
+
72
+ class GRPCBroker:
73
+ """Public broker facade. Lives on both host and plugin sides."""
74
+
75
+ def __init__(
76
+ self,
77
+ *,
78
+ send: Callable[[grpc_broker_pb2.ConnInfo], Awaitable[None]],
79
+ close: Callable[[], None],
80
+ tls: Optional[TLSMaterial] = None,
81
+ ) -> None:
82
+ self._send = send
83
+ self._close = close
84
+ self._tls = tls
85
+ self._lock = asyncio.Lock()
86
+ self._pending: dict[int, asyncio.Queue[grpc_broker_pb2.ConnInfo]] = {}
87
+ self._next_id = itertools.count(1)
88
+ self._closed = False
89
+ self._servers: list[Server] = []
90
+
91
+ def next_id(self) -> int:
92
+ return next(self._next_id)
93
+
94
+ def _q_for(self, sid: int) -> asyncio.Queue[grpc_broker_pb2.ConnInfo]:
95
+ q = self._pending.get(sid)
96
+ if q is None:
97
+ q = asyncio.Queue(maxsize=1)
98
+ self._pending[sid] = q
99
+ return q
100
+
101
+ async def deliver(self, msg: grpc_broker_pb2.ConnInfo) -> None:
102
+ """Run-loop callback — push an incoming message onto the per-id queue."""
103
+ q = self._q_for(msg.service_id)
104
+ try:
105
+ q.put_nowait(msg)
106
+ except asyncio.QueueFull:
107
+ pass # drop duplicate Knock/ack — not used for non-mux model
108
+
109
+ def _server_ssl(self) -> Optional[ssl.SSLContext]:
110
+ if self._tls is None:
111
+ return None
112
+ return mtls.server_ssl_context(
113
+ cert_pem=self._tls.cert_pem,
114
+ key_pem=self._tls.key_pem,
115
+ peer_cert_pem=self._tls.peer_cert_pem,
116
+ )
117
+
118
+ def _client_ssl(self) -> Optional[ssl.SSLContext]:
119
+ if self._tls is None:
120
+ return None
121
+ return mtls.client_ssl_context(
122
+ cert_pem=self._tls.cert_pem,
123
+ key_pem=self._tls.key_pem,
124
+ peer_cert_pem=self._tls.peer_cert_pem,
125
+ )
126
+
127
+ async def accept_and_serve(self, service_id: int, servicers: list) -> Server:
128
+ """Open a fresh listener, run a grpclib ``Server`` on it for ``service_id``,
129
+ and notify the peer via the broker stream."""
130
+ listener = open_listener()
131
+ server = Server(servicers)
132
+ srv_ssl = self._server_ssl()
133
+ if listener.network == "unix":
134
+ await server.start(path=listener.address, ssl=srv_ssl)
135
+ else:
136
+ host, port = listener.address.split(":")
137
+ await server.start(host=host, port=int(port), ssl=srv_ssl)
138
+ self._servers.append(server)
139
+ await self._send(grpc_broker_pb2.ConnInfo(
140
+ service_id=service_id, network=listener.network, address=listener.address,
141
+ ))
142
+ return server
143
+
144
+ async def dial(self, service_id: int, *, timeout: float = _DIAL_TIMEOUT) -> Channel:
145
+ """Wait for the peer's ``ConnInfo`` for ``service_id`` and open a channel."""
146
+ q = self._q_for(service_id)
147
+ try:
148
+ ci = await asyncio.wait_for(q.get(), timeout=timeout)
149
+ except asyncio.TimeoutError as e:
150
+ raise TimeoutError(f"timeout waiting for broker conn info for id {service_id}") from e
151
+
152
+ cli_ssl = self._client_ssl()
153
+ cfg = Configuration(ssl_target_name_override="localhost") if cli_ssl else None
154
+ if ci.network == "unix":
155
+ return Channel(path=ci.address, ssl=cli_ssl, config=cfg)
156
+ host, port = ci.address.split(":")
157
+ return Channel(host=host, port=int(port), ssl=cli_ssl, config=cfg)
158
+
159
+ async def close(self) -> None:
160
+ if self._closed:
161
+ return
162
+ self._closed = True
163
+ for srv in self._servers:
164
+ srv.close()
165
+ try:
166
+ await srv.wait_closed()
167
+ except Exception: # noqa: BLE001
168
+ pass
169
+ self._close()
170
+
171
+
172
+ def make_server_side_broker(tls: TLSMaterial | None) -> tuple[_ServerStreamServicer, GRPCBroker, "asyncio.Task[None]"]:
173
+ """Build (servicer, broker, demux-task) for the plugin side."""
174
+ servicer = _ServerStreamServicer()
175
+
176
+ async def send(msg: grpc_broker_pb2.ConnInfo) -> None:
177
+ await servicer.outgoing.put(msg)
178
+
179
+ def close() -> None:
180
+ servicer.outgoing.put_nowait(None)
181
+
182
+ broker = GRPCBroker(send=send, close=close, tls=tls)
183
+
184
+ async def demux() -> None:
185
+ while True:
186
+ msg = await servicer.incoming.get()
187
+ if msg is None:
188
+ return
189
+ await broker.deliver(msg)
190
+
191
+ return servicer, broker, asyncio.ensure_future(demux())
192
+
193
+
194
+ def make_client_side_broker(channel: Channel, tls: TLSMaterial | None) -> tuple[GRPCBroker, "asyncio.Task[None]"]:
195
+ """Build (broker, run-task) for the host side, dialing StartStream."""
196
+ stub = grpc_broker_grpc.GRPCBrokerStub(channel)
197
+ outgoing: asyncio.Queue[grpc_broker_pb2.ConnInfo | None] = asyncio.Queue()
198
+ stream_holder: dict = {}
199
+
200
+ async def send(msg: grpc_broker_pb2.ConnInfo) -> None:
201
+ await outgoing.put(msg)
202
+
203
+ def close() -> None:
204
+ outgoing.put_nowait(None)
205
+
206
+ broker = GRPCBroker(send=send, close=close, tls=tls)
207
+
208
+ async def runner() -> None:
209
+ async with stub.StartStream.open() as stream:
210
+ stream_holder["stream"] = stream
211
+
212
+ async def writer() -> None:
213
+ while True:
214
+ msg = await outgoing.get()
215
+ if msg is None:
216
+ return
217
+ await stream.send_message(msg)
218
+
219
+ async def reader() -> None:
220
+ async for msg in stream:
221
+ await broker.deliver(msg)
222
+
223
+ await asyncio.gather(writer(), reader(), return_exceptions=True)
224
+
225
+ return broker, asyncio.ensure_future(runner())