1
- import fcntl
2
1
import os
3
2
import errno
4
3
5
- from ._abc import SendStream , ReceiveStream
4
+ from ._abc import Stream
6
5
from ._util import ConflictDetector
7
6
8
7
import trio
9
8
9
+ if os .name != "posix" :
10
+ # We raise an error here rather than gating the import in hazmat.py
11
+ # in order to keep jedi static analysis happy.
12
+ raise ImportError
13
+
10
14
11
15
class _FdHolder :
12
16
# This class holds onto a raw file descriptor, in non-blocking mode, and
@@ -33,9 +37,9 @@ def __init__(self, fd: int):
33
37
if not isinstance (fd , int ):
34
38
raise TypeError ("file descriptor must be an int" )
35
39
self .fd = fd
36
- # Flip the fd to non-blocking mode
37
- flags = fcntl . fcntl ( self . fd , fcntl . F_GETFL )
38
- fcntl . fcntl ( self . fd , fcntl . F_SETFL , flags | os . O_NONBLOCK )
40
+ # Store original state, and ensure non-blocking mode is enabled
41
+ self . _original_is_blocking = os . get_blocking ( fd )
42
+ os . set_blocking ( fd , False )
39
43
40
44
@property
41
45
def closed (self ):
@@ -53,6 +57,7 @@ def _raw_close(self):
53
57
return
54
58
fd = self .fd
55
59
self .fd = - 1
60
+ os .set_blocking (fd , self ._original_is_blocking )
56
61
os .close (fd )
57
62
58
63
def __del__ (self ):
@@ -65,21 +70,53 @@ async def aclose(self):
65
70
await trio .hazmat .checkpoint ()
66
71
67
72
68
- class PipeSendStream (SendStream ):
69
- """Represents a send stream over an os.pipe object."""
73
+ class FdStream (Stream ):
74
+ """
75
+ Represents a stream given the file descriptor to a pipe, TTY, etc.
76
+
77
+ *fd* must refer to a file that is open for reading and/or writing and
78
+ supports non-blocking I/O (pipes and TTYs will work, on-disk files probably
79
+ not). The returned stream takes ownership of the fd, so closing the stream
80
+ will close the fd too. As with `os.fdopen`, you should not directly use
81
+ an fd after you have wrapped it in a stream using this function.
82
+
83
+ To be used as a Trio stream, an open file must be placed in non-blocking
84
+ mode. Unfortunately, this impacts all I/O that goes through the
85
+ underlying open file, including I/O that uses a different
86
+ file descriptor than the one that was passed to Trio. If other threads
87
+ or processes are using file descriptors that are related through `os.dup`
88
+ or inheritance across `os.fork` to the one that Trio is using, they are
89
+ unlikely to be prepared to have non-blocking I/O semantics suddenly
90
+ thrust upon them. For example, you can use ``FdStream(os.dup(0))`` to
91
+ obtain a stream for reading from standard input, but it is only safe to
92
+ do so with heavy caveats: your stdin must not be shared by any other
93
+ processes and you must not make any calls to synchronous methods of
94
+ `sys.stdin` until the stream returned by `FdStream` is closed. See
95
+ `issue #174 <https://github.com/python-trio/trio/issues/174>`__ for a
96
+ discussion of the challenges involved in relaxing this restriction.
97
+
98
+ Args:
99
+ fd (int): The fd to be wrapped.
100
+
101
+ Returns:
102
+ A new `FdStream` object.
103
+ """
70
104
71
105
def __init__ (self , fd : int ):
72
106
self ._fd_holder = _FdHolder (fd )
73
- self ._conflict_detector = ConflictDetector (
74
- "another task is using this pipe"
107
+ self ._send_conflict_detector = ConflictDetector (
108
+ "another task is using this stream for send"
109
+ )
110
+ self ._receive_conflict_detector = ConflictDetector (
111
+ "another task is using this stream for receive"
75
112
)
76
113
77
114
async def send_all (self , data : bytes ):
78
- with self ._conflict_detector :
115
+ with self ._send_conflict_detector :
79
116
# have to check up front, because send_all(b"") on a closed pipe
80
117
# should raise
81
118
if self ._fd_holder .closed :
82
- raise trio .ClosedResourceError ("this pipe was already closed" )
119
+ raise trio .ClosedResourceError ("file was already closed" )
83
120
await trio .hazmat .checkpoint ()
84
121
length = len (data )
85
122
# adapted from the SocketStream code
@@ -94,40 +131,24 @@ async def send_all(self, data: bytes):
94
131
except OSError as e :
95
132
if e .errno == errno .EBADF :
96
133
raise trio .ClosedResourceError (
97
- "this pipe was closed"
134
+ "file was already closed"
98
135
) from None
99
136
else :
100
137
raise trio .BrokenResourceError from e
101
138
102
139
async def wait_send_all_might_not_block (self ) -> None :
103
- with self ._conflict_detector :
140
+ with self ._send_conflict_detector :
104
141
if self ._fd_holder .closed :
105
- raise trio .ClosedResourceError ("this pipe was already closed" )
142
+ raise trio .ClosedResourceError ("file was already closed" )
106
143
try :
107
144
await trio .hazmat .wait_writable (self ._fd_holder .fd )
108
145
except BrokenPipeError as e :
109
146
# kqueue: raises EPIPE on wait_writable instead
110
147
# of sending, which is annoying
111
148
raise trio .BrokenResourceError from e
112
149
113
- async def aclose (self ):
114
- await self ._fd_holder .aclose ()
115
-
116
- def fileno (self ):
117
- return self ._fd_holder .fd
118
-
119
-
120
- class PipeReceiveStream (ReceiveStream ):
121
- """Represents a receive stream over an os.pipe object."""
122
-
123
- def __init__ (self , fd : int ):
124
- self ._fd_holder = _FdHolder (fd )
125
- self ._conflict_detector = ConflictDetector (
126
- "another task is using this pipe"
127
- )
128
-
129
150
async def receive_some (self , max_bytes : int ) -> bytes :
130
- with self ._conflict_detector :
151
+ with self ._receive_conflict_detector :
131
152
if not isinstance (max_bytes , int ):
132
153
raise TypeError ("max_bytes must be integer >= 1" )
133
154
@@ -143,7 +164,7 @@ async def receive_some(self, max_bytes: int) -> bytes:
143
164
except OSError as e :
144
165
if e .errno == errno .EBADF :
145
166
raise trio .ClosedResourceError (
146
- "this pipe was closed"
167
+ "file was already closed"
147
168
) from None
148
169
else :
149
170
raise trio .BrokenResourceError from e
0 commit comments