moat-lib-stream 0.1.2__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.
- moat/lib/stream/__init__.py +95 -0
- moat/lib/stream/_cbor.py +148 -0
- moat/lib/stream/_console.py +62 -0
- moat/lib/stream/_serial.py +72 -0
- moat/lib/stream/anyio.py +285 -0
- moat/lib/stream/base.py +323 -0
- moat/lib/stream/cbor.py +35 -0
- moat/lib/stream/log.py +160 -0
- moat/lib/stream/reliable.py +624 -0
- moat/lib/stream/tcp.py +60 -0
- moat/lib/stream/terminal.py +161 -0
- moat/lib/stream/unix.py +33 -0
- moat_lib_stream-0.1.2.dist-info/METADATA +68 -0
- moat_lib_stream-0.1.2.dist-info/RECORD +17 -0
- moat_lib_stream-0.1.2.dist-info/WHEEL +5 -0
- moat_lib_stream-0.1.2.dist-info/licenses/LICENSE.txt +14 -0
- moat_lib_stream-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stream infrastructure for handling data streams in a structured manner.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
# Import base classes directly (not lazy)
|
|
8
|
+
from .base import Base as Base
|
|
9
|
+
from .base import BaseBlk as BaseBlk
|
|
10
|
+
from .base import BaseBuf as BaseBuf
|
|
11
|
+
from .base import BaseConn as BaseConn
|
|
12
|
+
from .base import BaseMsg as BaseMsg
|
|
13
|
+
from .base import StackedBlk as StackedBlk
|
|
14
|
+
from .base import StackedBuf as StackedBuf
|
|
15
|
+
from .base import StackedConn as StackedConn
|
|
16
|
+
from .base import StackedMsg as StackedMsg
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING as _TC
|
|
19
|
+
|
|
20
|
+
if _TC:
|
|
21
|
+
from .anyio import AnyioBuf as AnyioBuf
|
|
22
|
+
from .anyio import BufAnyio as BufAnyio
|
|
23
|
+
from .anyio import FilenoBuf as FilenoBuf
|
|
24
|
+
from .anyio import ProcessBuf as ProcessBuf
|
|
25
|
+
from .anyio import ProcessDeadError as ProcessDeadError
|
|
26
|
+
from .anyio import RemoteBufAnyio as RemoteBufAnyio
|
|
27
|
+
from .anyio import SingleAnyioBuf as SingleAnyioBuf
|
|
28
|
+
from .cbor import CBORMsgBlk as CBORMsgBlk
|
|
29
|
+
from .cbor import CBORMsgBuf as CBORMsgBuf
|
|
30
|
+
from .log import LogBlk as LogBlk
|
|
31
|
+
from .log import LogBuf as LogBuf
|
|
32
|
+
from .log import LogMsg as LogMsg
|
|
33
|
+
from .reliable import EphemeralMsg as EphemeralMsg
|
|
34
|
+
from .reliable import ReliableMsg as ReliableMsg
|
|
35
|
+
from .tcp import TcpLink as TcpLink
|
|
36
|
+
from .terminal import FilenoTerm as FilenoTerm
|
|
37
|
+
from .terminal import TermBuf as TermBuf
|
|
38
|
+
from .unix import UnixLink as UnixLink
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Lazy loading
|
|
42
|
+
_imports = {
|
|
43
|
+
# Logging
|
|
44
|
+
"LogMsg": "log",
|
|
45
|
+
"LogBlk": "log",
|
|
46
|
+
"LogBuf": "log",
|
|
47
|
+
# CBOR
|
|
48
|
+
"CBORMsgBuf": "cbor",
|
|
49
|
+
"CBORMsgBlk": "cbor",
|
|
50
|
+
# AnyIO
|
|
51
|
+
"ProcessDeadError": "anyio",
|
|
52
|
+
"AnyioBuf": "anyio",
|
|
53
|
+
"FilenoBuf": "anyio",
|
|
54
|
+
"RemoteBufAnyio": "anyio",
|
|
55
|
+
"BufAnyio": "anyio",
|
|
56
|
+
"SingleAnyioBuf": "anyio",
|
|
57
|
+
"ProcessBuf": "anyio",
|
|
58
|
+
# Reliable messaging
|
|
59
|
+
"ReliableMsg": "reliable",
|
|
60
|
+
"EphemeralMsg": "reliable",
|
|
61
|
+
# Network connections
|
|
62
|
+
"TcpLink": "tcp",
|
|
63
|
+
"UnixLink": "unix",
|
|
64
|
+
# Terminal
|
|
65
|
+
"FilenoTerm": "terminal",
|
|
66
|
+
"TermBuf": "terminal",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __getattr__(attr: str):
|
|
71
|
+
try:
|
|
72
|
+
mod = _imports[attr]
|
|
73
|
+
except KeyError:
|
|
74
|
+
raise AttributeError(attr) from None
|
|
75
|
+
value = getattr(__import__(mod, globals(), None, True, 1), attr)
|
|
76
|
+
globals()[attr] = value
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
# Base classes (not lazy)
|
|
82
|
+
"Base",
|
|
83
|
+
"BaseBlk",
|
|
84
|
+
"BaseBuf",
|
|
85
|
+
"BaseConn",
|
|
86
|
+
"BaseMsg",
|
|
87
|
+
"StackedBlk",
|
|
88
|
+
"StackedBuf",
|
|
89
|
+
"StackedConn",
|
|
90
|
+
"StackedMsg",
|
|
91
|
+
] + list(_imports.keys())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def __dir__():
|
|
95
|
+
return __all__
|
moat/lib/stream/_cbor.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CBOR message encoding/decoding for stream layers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from moat.lib.micro import AC_use, Lock, log
|
|
8
|
+
from moat.lib.stream import BaseBuf, StackedMsg
|
|
9
|
+
|
|
10
|
+
from ._console import _CReader
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING # isort:skip
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from moat.lib.codec import Codec
|
|
16
|
+
from moat.util.liner import Liner
|
|
17
|
+
|
|
18
|
+
from collections.abc import Awaitable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CBORMsgBuf(StackedMsg):
|
|
23
|
+
"""
|
|
24
|
+
structured messages > CBOR bytestream
|
|
25
|
+
|
|
26
|
+
Use this if your stream is reliable (TCP, USB, …) but doesn't support
|
|
27
|
+
message boundaries.
|
|
28
|
+
|
|
29
|
+
If @console is set and a prefix is used, sends data atomically.
|
|
30
|
+
Otherwise two separate write calls are used to save on message copying.
|
|
31
|
+
|
|
32
|
+
Config:
|
|
33
|
+
console (bool):
|
|
34
|
+
Flag how to handle non-framed data.
|
|
35
|
+
True: collect for crd/cwr, False: print incoming, None: ignore.
|
|
36
|
+
msg_prefix(int):
|
|
37
|
+
bytecode of prefix for messages (as opposed to console data)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
cons = False
|
|
41
|
+
codec: Codec = None
|
|
42
|
+
liner: Liner = None
|
|
43
|
+
|
|
44
|
+
def __init__(self, stream: BaseBuf, cfg: dict):
|
|
45
|
+
#
|
|
46
|
+
# console: size of console buffer, 128 if True
|
|
47
|
+
# msg_prefix: int: code for start-of-packet
|
|
48
|
+
#
|
|
49
|
+
super().__init__(stream, cfg)
|
|
50
|
+
self.w_lock = Lock()
|
|
51
|
+
|
|
52
|
+
pref = cfg.get("msg_prefix")
|
|
53
|
+
if pref is not None:
|
|
54
|
+
pref = bytes((pref,))
|
|
55
|
+
self.pref = pref
|
|
56
|
+
|
|
57
|
+
cons = cfg.get("console", False)
|
|
58
|
+
if cons:
|
|
59
|
+
_CReader.__init__(self, cons)
|
|
60
|
+
|
|
61
|
+
async def setup(self):
|
|
62
|
+
await super().setup()
|
|
63
|
+
if self.cons is False:
|
|
64
|
+
from moat.util.liner import Liner # noqa:PLC0415
|
|
65
|
+
|
|
66
|
+
self.liner = await AC_use(self, Liner())
|
|
67
|
+
|
|
68
|
+
async def cwr(self, buf):
|
|
69
|
+
if not self.cons:
|
|
70
|
+
return
|
|
71
|
+
return await self.s.wr(buf)
|
|
72
|
+
|
|
73
|
+
def crd(self, buf) -> Awaitable:
|
|
74
|
+
return _CReader.crd(self, buf)
|
|
75
|
+
|
|
76
|
+
async def send(self, msg: Any) -> None:
|
|
77
|
+
try:
|
|
78
|
+
msg = self.codec.encode(msg)
|
|
79
|
+
except Exception:
|
|
80
|
+
log("MSG:\n%r", msg)
|
|
81
|
+
raise
|
|
82
|
+
async with self.w_lock:
|
|
83
|
+
if self.pref is not None:
|
|
84
|
+
if True: # self.cons:
|
|
85
|
+
msg = self.pref + msg # must be atomic
|
|
86
|
+
else:
|
|
87
|
+
await self.s.wr(self.pref)
|
|
88
|
+
await self.s.wr(msg)
|
|
89
|
+
|
|
90
|
+
async def recv(self) -> Any:
|
|
91
|
+
"""
|
|
92
|
+
Receive the next object.
|
|
93
|
+
"""
|
|
94
|
+
# Pre+postcondition: the codec does not have an object in progress.
|
|
95
|
+
|
|
96
|
+
buf = bytearray(64)
|
|
97
|
+
if self.pref is None:
|
|
98
|
+
# easy case
|
|
99
|
+
while True:
|
|
100
|
+
try:
|
|
101
|
+
r = next(self.codec)
|
|
102
|
+
except StopIteration:
|
|
103
|
+
n = await self.s.rd(buf)
|
|
104
|
+
self.codec.feed(memoryview(buf)[:n])
|
|
105
|
+
else:
|
|
106
|
+
if self.cons and isinstance(r, int) and r >= 0:
|
|
107
|
+
_CReader.cput(self, r)
|
|
108
|
+
else:
|
|
109
|
+
return r
|
|
110
|
+
|
|
111
|
+
while True:
|
|
112
|
+
b = bytearray(1)
|
|
113
|
+
# read until we get a prefix byte
|
|
114
|
+
if self.codec.unfeed(b) == 0:
|
|
115
|
+
n = await self.s.rd(buf)
|
|
116
|
+
self.codec.feed(memoryview(buf)[:n])
|
|
117
|
+
elif b == self.pref:
|
|
118
|
+
break
|
|
119
|
+
elif self.cons:
|
|
120
|
+
_CReader.cput(self, b[0])
|
|
121
|
+
elif self.liner is not None:
|
|
122
|
+
self.liner(b)
|
|
123
|
+
|
|
124
|
+
while True:
|
|
125
|
+
# read until we get an object
|
|
126
|
+
try:
|
|
127
|
+
return next(self.codec)
|
|
128
|
+
except StopIteration:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
n = await self.s.rd(buf)
|
|
132
|
+
self.codec.feed(memoryview(buf)[:n])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _CBORMsgBlk(StackedMsg):
|
|
136
|
+
"""
|
|
137
|
+
structured messages > chunked bytestrings
|
|
138
|
+
|
|
139
|
+
Use this if the layer below supports byte boundaries
|
|
140
|
+
(one bytestring-ized message per call).
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
async def send(self, msg):
|
|
144
|
+
await self.s.snd(self.codec.encode(msg))
|
|
145
|
+
|
|
146
|
+
async def recv(self):
|
|
147
|
+
m = await self.s.rcv()
|
|
148
|
+
return self.codec.decode(m)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Console data handling for stream layers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from moat.lib.micro import Event
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _CReader:
|
|
11
|
+
"""
|
|
12
|
+
A mix-in that processes incoming console data.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cons: bool | int):
|
|
16
|
+
if cons is True:
|
|
17
|
+
try:
|
|
18
|
+
import machine # noqa: PLC0415,F401
|
|
19
|
+
except ImportError:
|
|
20
|
+
cons = 32768
|
|
21
|
+
else:
|
|
22
|
+
cons = 240
|
|
23
|
+
self.cevt = Event()
|
|
24
|
+
self.cpos = 0
|
|
25
|
+
self.cbuf = bytearray(cons)
|
|
26
|
+
self.cons = cons
|
|
27
|
+
self.intr = 0
|
|
28
|
+
|
|
29
|
+
async def crd(self, buf: bytearray):
|
|
30
|
+
"""read waiting console data"""
|
|
31
|
+
if not self.cons:
|
|
32
|
+
raise EOFError
|
|
33
|
+
if not self.cpos:
|
|
34
|
+
await self.cevt.wait()
|
|
35
|
+
self.cevt = Event()
|
|
36
|
+
n = min(len(buf), self.cpos)
|
|
37
|
+
buf[:n] = self.cbuf[:n]
|
|
38
|
+
if n < self.cpos:
|
|
39
|
+
self.cbuf[: self.cpos - n] = self.cbuf[n : self.cpos]
|
|
40
|
+
self.cpos -= n
|
|
41
|
+
else:
|
|
42
|
+
self.cpos = 0
|
|
43
|
+
return n
|
|
44
|
+
|
|
45
|
+
def cput(self, b: int):
|
|
46
|
+
"""store a byte in the console buffer"""
|
|
47
|
+
if self.cpos == len(self.cbuf):
|
|
48
|
+
if len(self.cbuf) > 100:
|
|
49
|
+
bfull = b"\n?BUF\n"
|
|
50
|
+
self.cbuf[0 : len(bfull)] = bfull
|
|
51
|
+
self.cpos = len(bfull)
|
|
52
|
+
else:
|
|
53
|
+
self.cpos = 0
|
|
54
|
+
if b != 3:
|
|
55
|
+
self.intr = 0
|
|
56
|
+
elif self.intr > 2:
|
|
57
|
+
raise KeyboardInterrupt
|
|
58
|
+
else:
|
|
59
|
+
self.intr += 1
|
|
60
|
+
self.cbuf[self.cpos] = b
|
|
61
|
+
self.cpos += 1
|
|
62
|
+
self.cevt.set()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SerialPacker protocol support for stream layers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from moat.lib.micro import Lock
|
|
8
|
+
from moat.lib.stream import BaseBuf, StackedBlk
|
|
9
|
+
|
|
10
|
+
from ._console import _CReader
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SerialPackerBlkBuf(StackedBlk):
|
|
14
|
+
"""
|
|
15
|
+
chunked bytestrings > SerialPacker-ized stream
|
|
16
|
+
|
|
17
|
+
Use this (and a CBORHandler and a Reliable) if your AIO stream
|
|
18
|
+
is unreliable (TTL serial).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
cons = False
|
|
22
|
+
|
|
23
|
+
def __init__(self, stream: BaseBuf, frame: dict, console: bool | int = False):
|
|
24
|
+
super().__init__(None)
|
|
25
|
+
|
|
26
|
+
from serialpacker import SerialPacker # noqa: PLC0415
|
|
27
|
+
|
|
28
|
+
self.s = stream
|
|
29
|
+
self.p = SerialPacker(**frame)
|
|
30
|
+
self.buf = bytearray(16)
|
|
31
|
+
self.i = 0
|
|
32
|
+
self.n = 0
|
|
33
|
+
self.w_lock = Lock()
|
|
34
|
+
if console:
|
|
35
|
+
_CReader.__init__(self, console)
|
|
36
|
+
|
|
37
|
+
async def crd(self, buf):
|
|
38
|
+
if not self.cons:
|
|
39
|
+
raise EOFError
|
|
40
|
+
return await _CReader.crd(self, buf)
|
|
41
|
+
|
|
42
|
+
async def cwr(self, buf):
|
|
43
|
+
if not self.cons:
|
|
44
|
+
return
|
|
45
|
+
return await super().wr(buf)
|
|
46
|
+
|
|
47
|
+
async def recv(self):
|
|
48
|
+
while True:
|
|
49
|
+
while self.i < self.n:
|
|
50
|
+
msg = self.p.feed(self.buf[self.i])
|
|
51
|
+
self.i += 1
|
|
52
|
+
if isinstance(msg, int):
|
|
53
|
+
if self.cons:
|
|
54
|
+
_CReader.cput(self, msg)
|
|
55
|
+
elif msg is not None:
|
|
56
|
+
return msg
|
|
57
|
+
|
|
58
|
+
n = await self.s.rd(self.buf)
|
|
59
|
+
if not n:
|
|
60
|
+
raise EOFError
|
|
61
|
+
self.i = 0
|
|
62
|
+
self.n = n
|
|
63
|
+
|
|
64
|
+
async def send(self, msg):
|
|
65
|
+
h, msg, t = self.p.frame(msg)
|
|
66
|
+
async with self.w_lock:
|
|
67
|
+
if not self.cons:
|
|
68
|
+
await self.s.wr(h)
|
|
69
|
+
await self.s.wr(msg)
|
|
70
|
+
await self.s.wr(t)
|
|
71
|
+
else:
|
|
72
|
+
await self.s.wr(h + msg + t)
|
moat/lib/stream/anyio.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AnyIO stream adaptors for MoaT (CPython-specific).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
import fcntl
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import termios
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
|
|
14
|
+
from moat.util import CtxObj
|
|
15
|
+
from moat.lib.micro import AC_use
|
|
16
|
+
from moat.lib.stream import BaseBuf
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING # isort:skip
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from moat.micro.cmd.tree.dir import SubDispatch
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProcessDeadError(RuntimeError):
|
|
25
|
+
"""Process has died"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AnyioBuf(BaseBuf):
|
|
29
|
+
"""
|
|
30
|
+
Adapts an anyio stream to MoaT.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def stream(self) -> anyio.abc.ByteStream:
|
|
34
|
+
"""
|
|
35
|
+
Create the stream to use.
|
|
36
|
+
|
|
37
|
+
Use `AC_use` to arrange for closing it. This class will not do it
|
|
38
|
+
for you.
|
|
39
|
+
"""
|
|
40
|
+
raise NotImplementedError(f"Override {self.__class__.__name__}.stream")
|
|
41
|
+
|
|
42
|
+
async def wr(self, buf) -> int:
|
|
43
|
+
"basic send"
|
|
44
|
+
try:
|
|
45
|
+
await self.s.send(buf)
|
|
46
|
+
except (anyio.EndOfStream, anyio.ClosedResourceError):
|
|
47
|
+
raise EOFError from None
|
|
48
|
+
else:
|
|
49
|
+
return len(buf)
|
|
50
|
+
|
|
51
|
+
async def rd(self, buf) -> int:
|
|
52
|
+
"basic receive-into"
|
|
53
|
+
try:
|
|
54
|
+
res = await self.s.receive(len(buf))
|
|
55
|
+
except (anyio.EndOfStream, anyio.ClosedResourceError):
|
|
56
|
+
raise EOFError from None
|
|
57
|
+
else:
|
|
58
|
+
buf[: len(res)] = res
|
|
59
|
+
return len(res)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FilenoBuf(BaseBuf):
|
|
63
|
+
"""
|
|
64
|
+
Adapts a Unix file descriptor to MoaT.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
rfd: int
|
|
69
|
+
wfd: int
|
|
70
|
+
rfl: int
|
|
71
|
+
wfl: int
|
|
72
|
+
term: bytes
|
|
73
|
+
|
|
74
|
+
def __init__(self, cfg, fd: int, wfd: int | None = None):
|
|
75
|
+
super().__init__(cfg)
|
|
76
|
+
self.rfd = fd
|
|
77
|
+
self.wfd = fd if wfd is None else wfd
|
|
78
|
+
self.term = None
|
|
79
|
+
|
|
80
|
+
async def stream(self) -> anyio.abc.ByteStream:
|
|
81
|
+
"Dummy here"
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
async def setup(self):
|
|
85
|
+
"Change to nonblocking and raw"
|
|
86
|
+
self.rfl = fcntl.fcntl(self.rfd, fcntl.F_SETFL, 0)
|
|
87
|
+
if self.rfd != self.wfd:
|
|
88
|
+
self.wfl = fcntl.fcntl(self.wfd, fcntl.F_SETFL, 0)
|
|
89
|
+
|
|
90
|
+
fcntl.fcntl(self.rfd, fcntl.F_SETFL, self.rfl | os.O_NDELAY)
|
|
91
|
+
if self.rfd != self.wfd:
|
|
92
|
+
fcntl.fcntl(self.wfd, fcntl.F_SETFL, self.wfl | os.O_NDELAY)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
self.term = termios.tcgetattr(self.rfd)
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
98
|
+
else:
|
|
99
|
+
new = self.term[:]
|
|
100
|
+
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
|
|
101
|
+
new[6][termios.VMIN] = 1
|
|
102
|
+
new[6][termios.VTIME] = 0
|
|
103
|
+
termios.tcsetattr(self.rfd, termios.TCSANOW, new)
|
|
104
|
+
|
|
105
|
+
async def teardown(self):
|
|
106
|
+
"Return to blocking and cooked"
|
|
107
|
+
fcntl.fcntl(self.rfd, fcntl.F_SETFL, self.rfl)
|
|
108
|
+
if self.rfd != self.wfd:
|
|
109
|
+
fcntl.fcntl(self.wfd, fcntl.F_SETFL, self.wfl)
|
|
110
|
+
if self.term is not None:
|
|
111
|
+
termios.tcsetattr(self.rfd, termios.TCSANOW, self.term)
|
|
112
|
+
|
|
113
|
+
async def wr(self, buf) -> int:
|
|
114
|
+
"basic send"
|
|
115
|
+
on = len(buf)
|
|
116
|
+
while n := len(buf):
|
|
117
|
+
await anyio.wait_writable(self.wfd)
|
|
118
|
+
nn = os.write(self.wfd, buf)
|
|
119
|
+
if nn <= 0:
|
|
120
|
+
raise OSError
|
|
121
|
+
if nn == n:
|
|
122
|
+
return on
|
|
123
|
+
buf = memoryview(buf)[nn:]
|
|
124
|
+
|
|
125
|
+
async def rd(self, buf) -> int:
|
|
126
|
+
"basic receive-into"
|
|
127
|
+
await anyio.wait_readable(self.rfd)
|
|
128
|
+
bf = os.read(self.rfd, len(buf))
|
|
129
|
+
buf[: len(bf)] = bf
|
|
130
|
+
return len(bf)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RemoteBufAnyio(anyio.abc.ByteStream):
|
|
134
|
+
"""
|
|
135
|
+
Adapts a MoaT buf stream to a remote buffer read/write
|
|
136
|
+
|
|
137
|
+
TODO: use remote iteration for receiving
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, disp: SubDispatch):
|
|
141
|
+
self.disp = disp
|
|
142
|
+
|
|
143
|
+
async def receive(self, max_bytes=256):
|
|
144
|
+
"forward to ``.rd``"
|
|
145
|
+
return await self.disp.rd(n=max_bytes)
|
|
146
|
+
|
|
147
|
+
async def send(self, buf):
|
|
148
|
+
"forward to ``.wr``"
|
|
149
|
+
await self.disp.wr(b=buf)
|
|
150
|
+
|
|
151
|
+
async def aclose(self):
|
|
152
|
+
"no-op"
|
|
153
|
+
|
|
154
|
+
async def send_eof(self):
|
|
155
|
+
"not implemented"
|
|
156
|
+
raise NotImplementedError("EOF")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class BufAnyio(anyio.abc.ByteStream):
|
|
160
|
+
"""
|
|
161
|
+
Adapts a MoaT Buf stream to an anyio bytestream.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
par = None
|
|
165
|
+
|
|
166
|
+
def __init__(self, stream: BaseBuf):
|
|
167
|
+
self.stream = stream
|
|
168
|
+
|
|
169
|
+
async def __aenter__(self):
|
|
170
|
+
self.s = await self.stream.__aenter__()
|
|
171
|
+
|
|
172
|
+
async def __aexit__(self, *tb):
|
|
173
|
+
return await self.stream.__aexit__(*tb)
|
|
174
|
+
|
|
175
|
+
async def receive(self, max_bytes=256):
|
|
176
|
+
"forward to ``.rd``"
|
|
177
|
+
b = bytearray(max_bytes)
|
|
178
|
+
r = await self.s.rd(b)
|
|
179
|
+
if r == max_bytes:
|
|
180
|
+
return b
|
|
181
|
+
elif r <= max_bytes >> 2:
|
|
182
|
+
return bytes(b[:r])
|
|
183
|
+
else:
|
|
184
|
+
b = memoryview(b)
|
|
185
|
+
return b[:r]
|
|
186
|
+
|
|
187
|
+
async def send(self, buf):
|
|
188
|
+
"forward to ``.wr``"
|
|
189
|
+
await self.s.wr(buf)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SingleAnyioBuf(AnyioBuf):
|
|
193
|
+
"""
|
|
194
|
+
Adapts an AnyIO stream to MoaT.
|
|
195
|
+
|
|
196
|
+
The stream is passed to the class constructor and can only be used
|
|
197
|
+
once.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, stream):
|
|
201
|
+
self._s = stream
|
|
202
|
+
|
|
203
|
+
async def stream(self): # noqa:D102
|
|
204
|
+
return await AC_use(self, self._s)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ProcessBuf(CtxObj, AnyioBuf):
|
|
208
|
+
"""
|
|
209
|
+
A stream that connects to an external process.
|
|
210
|
+
|
|
211
|
+
Config:
|
|
212
|
+
- exec: path to the executable
|
|
213
|
+
- argv: Arguments
|
|
214
|
+
- env: environment vars
|
|
215
|
+
|
|
216
|
+
You can set these as attributes on the object, statically or in your
|
|
217
|
+
subclass's `setup` method. Configuration can then override them.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
proc: anyio.Process = None
|
|
221
|
+
exec: str | None = None
|
|
222
|
+
cwd: str | None = None
|
|
223
|
+
argv: list[str] | None = None
|
|
224
|
+
env: dict[str, str] | None = None
|
|
225
|
+
|
|
226
|
+
def __init__(self, cfg, executable: str | None = None, **kw):
|
|
227
|
+
super().__init__(cfg)
|
|
228
|
+
kw.setdefault("stderr", sys.stderr)
|
|
229
|
+
self.kw = kw
|
|
230
|
+
self.exec = executable
|
|
231
|
+
|
|
232
|
+
def open_args(self):
|
|
233
|
+
"""Return keyword arguments for `anyio.open_process`.
|
|
234
|
+
|
|
235
|
+
Default is whatever has been passed to the ProcessBuf constructor.
|
|
236
|
+
"""
|
|
237
|
+
# Ugh, anyio doesn't accept 'executable'
|
|
238
|
+
if self.exec is not None:
|
|
239
|
+
# self.kw["executable"] = self.exec
|
|
240
|
+
self.argv[0] = self.exec
|
|
241
|
+
elif "/" in (a0 := str(self.argv[0])): # noqa:F841 # a0 unused
|
|
242
|
+
# self.kw["executable"] = a0
|
|
243
|
+
# self.argv[0] = a0.rsplit("/",1)[1]
|
|
244
|
+
pass
|
|
245
|
+
for k in ("cwd", "env"):
|
|
246
|
+
if (v := getattr(self, k)) is not None:
|
|
247
|
+
self.kw[k] = v
|
|
248
|
+
|
|
249
|
+
return self.kw
|
|
250
|
+
|
|
251
|
+
@asynccontextmanager
|
|
252
|
+
async def _ctx(self):
|
|
253
|
+
await self.setup()
|
|
254
|
+
proc = None
|
|
255
|
+
for k in ("exec", "argv", "env", "cwd"):
|
|
256
|
+
if (v := self.cfg.get(k, None)) is not None:
|
|
257
|
+
setattr(self, k, v)
|
|
258
|
+
if self.argv is None:
|
|
259
|
+
raise ValueError("Don't know what")
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
async with await anyio.open_process(self.argv, **self.open_args()) as proc:
|
|
263
|
+
try:
|
|
264
|
+
async with SingleAnyioBuf(
|
|
265
|
+
anyio.streams.stapled.StapledByteStream(proc.stdin, proc.stdout),
|
|
266
|
+
) as s:
|
|
267
|
+
yield s
|
|
268
|
+
await proc.wait()
|
|
269
|
+
except BaseException:
|
|
270
|
+
try:
|
|
271
|
+
proc.kill()
|
|
272
|
+
with anyio.CancelScope(shield=True):
|
|
273
|
+
await proc.wait()
|
|
274
|
+
raise
|
|
275
|
+
finally:
|
|
276
|
+
proc = None # noqa:PLW2901
|
|
277
|
+
finally:
|
|
278
|
+
if proc is not None and proc.returncode != 0 and proc.returncode != -9:
|
|
279
|
+
raise ProcessDeadError(f"{self} died with {proc.returncode}")
|
|
280
|
+
|
|
281
|
+
async def setup(self): # noqa:D102
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
async def stream(self): # noqa:D102
|
|
285
|
+
raise RuntimeError("should not be called")
|