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 +54 -0
- pyplugin/_generated/__init__.py +0 -0
- pyplugin/_generated/grpc_broker_grpc.py +40 -0
- pyplugin/_generated/grpc_broker_pb2.py +41 -0
- pyplugin/_generated/grpc_controller_grpc.py +40 -0
- pyplugin/_generated/grpc_controller_pb2.py +39 -0
- pyplugin/_generated/grpc_stdio_grpc.py +41 -0
- pyplugin/_generated/grpc_stdio_pb2.py +42 -0
- pyplugin/broker.py +225 -0
- pyplugin/client.py +399 -0
- pyplugin/controller.py +22 -0
- pyplugin/cookie.py +43 -0
- pyplugin/errors.py +39 -0
- pyplugin/handshake.py +121 -0
- pyplugin/health.py +38 -0
- pyplugin/logging_bridge.py +70 -0
- pyplugin/mtls.py +169 -0
- pyplugin/plugin.py +38 -0
- pyplugin/process.py +66 -0
- pyplugin/proto/grpc_broker.proto +21 -0
- pyplugin/proto/grpc_controller.proto +12 -0
- pyplugin/proto/grpc_stdio.proto +22 -0
- pyplugin/reattach.py +27 -0
- pyplugin/server.py +204 -0
- pyplugin/stdio.py +36 -0
- pyplugin/transport.py +103 -0
- python_plugin-0.1.0.dist-info/METADATA +254 -0
- python_plugin-0.1.0.dist-info/RECORD +30 -0
- python_plugin-0.1.0.dist-info/WHEEL +4 -0
- python_plugin-0.1.0.dist-info/licenses/LICENSE +21 -0
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())
|