@ajksunkang-aios/kgraph-linux-x64 0.1.2 → 0.1.3

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.
Files changed (196) hide show
  1. package/bin/kgraph-launcher +15 -3
  2. package/lib/kgraph/scripts/build-bundle.sh +17 -4
  3. package/lib/site-packages/outcome/__init__.py +20 -0
  4. package/lib/site-packages/outcome/_impl.py +239 -0
  5. package/lib/site-packages/outcome/_util.py +33 -0
  6. package/lib/site-packages/outcome/_version.py +7 -0
  7. package/lib/site-packages/outcome/py.typed +0 -0
  8. package/lib/site-packages/outcome-1.3.0.post0.dist-info/INSTALLER +1 -0
  9. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE +3 -0
  10. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.APACHE2 +202 -0
  11. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.MIT +20 -0
  12. package/lib/site-packages/outcome-1.3.0.post0.dist-info/METADATA +63 -0
  13. package/lib/site-packages/outcome-1.3.0.post0.dist-info/RECORD +13 -0
  14. package/lib/site-packages/outcome-1.3.0.post0.dist-info/WHEEL +6 -0
  15. package/lib/site-packages/outcome-1.3.0.post0.dist-info/top_level.txt +1 -0
  16. package/lib/site-packages/sniffio/__init__.py +17 -0
  17. package/lib/site-packages/sniffio/_impl.py +95 -0
  18. package/lib/site-packages/sniffio/_tests/__init__.py +0 -0
  19. package/lib/site-packages/sniffio/_tests/test_sniffio.py +84 -0
  20. package/lib/site-packages/sniffio/_version.py +3 -0
  21. package/lib/site-packages/sniffio/py.typed +0 -0
  22. package/lib/site-packages/sniffio-1.3.1.dist-info/INSTALLER +1 -0
  23. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE +3 -0
  24. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.APACHE2 +202 -0
  25. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.MIT +20 -0
  26. package/lib/site-packages/sniffio-1.3.1.dist-info/METADATA +104 -0
  27. package/lib/site-packages/sniffio-1.3.1.dist-info/RECORD +14 -0
  28. package/lib/site-packages/sniffio-1.3.1.dist-info/WHEEL +5 -0
  29. package/lib/site-packages/sniffio-1.3.1.dist-info/top_level.txt +1 -0
  30. package/lib/site-packages/sortedcontainers/__init__.py +74 -0
  31. package/lib/site-packages/sortedcontainers/sorteddict.py +812 -0
  32. package/lib/site-packages/sortedcontainers/sortedlist.py +2646 -0
  33. package/lib/site-packages/sortedcontainers/sortedset.py +733 -0
  34. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/INSTALLER +1 -0
  35. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/LICENSE +13 -0
  36. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/METADATA +264 -0
  37. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/RECORD +10 -0
  38. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/WHEEL +6 -0
  39. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/top_level.txt +1 -0
  40. package/lib/site-packages/trio/__init__.py +133 -0
  41. package/lib/site-packages/trio/__main__.py +3 -0
  42. package/lib/site-packages/trio/_abc.py +714 -0
  43. package/lib/site-packages/trio/_channel.py +610 -0
  44. package/lib/site-packages/trio/_core/__init__.py +94 -0
  45. package/lib/site-packages/trio/_core/_asyncgens.py +243 -0
  46. package/lib/site-packages/trio/_core/_concat_tb.py +26 -0
  47. package/lib/site-packages/trio/_core/_entry_queue.py +223 -0
  48. package/lib/site-packages/trio/_core/_exceptions.py +169 -0
  49. package/lib/site-packages/trio/_core/_generated_instrumentation.py +50 -0
  50. package/lib/site-packages/trio/_core/_generated_io_epoll.py +98 -0
  51. package/lib/site-packages/trio/_core/_generated_io_kqueue.py +153 -0
  52. package/lib/site-packages/trio/_core/_generated_io_windows.py +204 -0
  53. package/lib/site-packages/trio/_core/_generated_run.py +269 -0
  54. package/lib/site-packages/trio/_core/_generated_windows_ffi.py +10 -0
  55. package/lib/site-packages/trio/_core/_instrumentation.py +117 -0
  56. package/lib/site-packages/trio/_core/_io_common.py +31 -0
  57. package/lib/site-packages/trio/_core/_io_epoll.py +385 -0
  58. package/lib/site-packages/trio/_core/_io_kqueue.py +292 -0
  59. package/lib/site-packages/trio/_core/_io_windows.py +1036 -0
  60. package/lib/site-packages/trio/_core/_ki.py +271 -0
  61. package/lib/site-packages/trio/_core/_local.py +104 -0
  62. package/lib/site-packages/trio/_core/_mock_clock.py +165 -0
  63. package/lib/site-packages/trio/_core/_parking_lot.py +317 -0
  64. package/lib/site-packages/trio/_core/_run.py +3148 -0
  65. package/lib/site-packages/trio/_core/_run_context.py +15 -0
  66. package/lib/site-packages/trio/_core/_tests/__init__.py +0 -0
  67. package/lib/site-packages/trio/_core/_tests/test_asyncgen.py +339 -0
  68. package/lib/site-packages/trio/_core/_tests/test_cancelled.py +222 -0
  69. package/lib/site-packages/trio/_core/_tests/test_exceptiongroup_gc.py +103 -0
  70. package/lib/site-packages/trio/_core/_tests/test_guest_mode.py +755 -0
  71. package/lib/site-packages/trio/_core/_tests/test_instrumentation.py +315 -0
  72. package/lib/site-packages/trio/_core/_tests/test_io.py +522 -0
  73. package/lib/site-packages/trio/_core/_tests/test_ki.py +703 -0
  74. package/lib/site-packages/trio/_core/_tests/test_local.py +118 -0
  75. package/lib/site-packages/trio/_core/_tests/test_mock_clock.py +193 -0
  76. package/lib/site-packages/trio/_core/_tests/test_parking_lot.py +389 -0
  77. package/lib/site-packages/trio/_core/_tests/test_run.py +3024 -0
  78. package/lib/site-packages/trio/_core/_tests/test_thread_cache.py +227 -0
  79. package/lib/site-packages/trio/_core/_tests/test_tutil.py +13 -0
  80. package/lib/site-packages/trio/_core/_tests/test_unbounded_queue.py +154 -0
  81. package/lib/site-packages/trio/_core/_tests/test_windows.py +305 -0
  82. package/lib/site-packages/trio/_core/_tests/tutil.py +117 -0
  83. package/lib/site-packages/trio/_core/_tests/type_tests/nursery_start.py +79 -0
  84. package/lib/site-packages/trio/_core/_tests/type_tests/run.py +51 -0
  85. package/lib/site-packages/trio/_core/_thread_cache.py +317 -0
  86. package/lib/site-packages/trio/_core/_traps.py +318 -0
  87. package/lib/site-packages/trio/_core/_unbounded_queue.py +163 -0
  88. package/lib/site-packages/trio/_core/_wakeup_socketpair.py +75 -0
  89. package/lib/site-packages/trio/_core/_windows_cffi.py +313 -0
  90. package/lib/site-packages/trio/_deprecate.py +171 -0
  91. package/lib/site-packages/trio/_dtls.py +1380 -0
  92. package/lib/site-packages/trio/_file_io.py +513 -0
  93. package/lib/site-packages/trio/_highlevel_generic.py +125 -0
  94. package/lib/site-packages/trio/_highlevel_open_tcp_listeners.py +251 -0
  95. package/lib/site-packages/trio/_highlevel_open_tcp_stream.py +397 -0
  96. package/lib/site-packages/trio/_highlevel_open_unix_stream.py +65 -0
  97. package/lib/site-packages/trio/_highlevel_serve_listeners.py +148 -0
  98. package/lib/site-packages/trio/_highlevel_socket.py +423 -0
  99. package/lib/site-packages/trio/_highlevel_ssl_helpers.py +180 -0
  100. package/lib/site-packages/trio/_path.py +289 -0
  101. package/lib/site-packages/trio/_repl.py +159 -0
  102. package/lib/site-packages/trio/_signals.py +185 -0
  103. package/lib/site-packages/trio/_socket.py +1326 -0
  104. package/lib/site-packages/trio/_ssl.py +964 -0
  105. package/lib/site-packages/trio/_subprocess.py +1178 -0
  106. package/lib/site-packages/trio/_subprocess_platform/__init__.py +123 -0
  107. package/lib/site-packages/trio/_subprocess_platform/kqueue.py +48 -0
  108. package/lib/site-packages/trio/_subprocess_platform/waitid.py +113 -0
  109. package/lib/site-packages/trio/_subprocess_platform/windows.py +11 -0
  110. package/lib/site-packages/trio/_sync.py +908 -0
  111. package/lib/site-packages/trio/_tests/__init__.py +0 -0
  112. package/lib/site-packages/trio/_tests/astrill-codesigning-cert.cer +0 -0
  113. package/lib/site-packages/trio/_tests/check_type_completeness.py +247 -0
  114. package/lib/site-packages/trio/_tests/module_with_deprecations.py +22 -0
  115. package/lib/site-packages/trio/_tests/pytest_plugin.py +54 -0
  116. package/lib/site-packages/trio/_tests/test_abc.py +72 -0
  117. package/lib/site-packages/trio/_tests/test_channel.py +750 -0
  118. package/lib/site-packages/trio/_tests/test_contextvars.py +56 -0
  119. package/lib/site-packages/trio/_tests/test_deprecate.py +277 -0
  120. package/lib/site-packages/trio/_tests/test_deprecate_strict_exception_groups_false.py +64 -0
  121. package/lib/site-packages/trio/_tests/test_dtls.py +950 -0
  122. package/lib/site-packages/trio/_tests/test_exports.py +626 -0
  123. package/lib/site-packages/trio/_tests/test_fakenet.py +317 -0
  124. package/lib/site-packages/trio/_tests/test_file_io.py +269 -0
  125. package/lib/site-packages/trio/_tests/test_highlevel_generic.py +98 -0
  126. package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_listeners.py +419 -0
  127. package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_stream.py +693 -0
  128. package/lib/site-packages/trio/_tests/test_highlevel_open_unix_stream.py +86 -0
  129. package/lib/site-packages/trio/_tests/test_highlevel_serve_listeners.py +186 -0
  130. package/lib/site-packages/trio/_tests/test_highlevel_socket.py +336 -0
  131. package/lib/site-packages/trio/_tests/test_highlevel_ssl_helpers.py +169 -0
  132. package/lib/site-packages/trio/_tests/test_path.py +279 -0
  133. package/lib/site-packages/trio/_tests/test_repl.py +428 -0
  134. package/lib/site-packages/trio/_tests/test_scheduler_determinism.py +47 -0
  135. package/lib/site-packages/trio/_tests/test_signals.py +186 -0
  136. package/lib/site-packages/trio/_tests/test_socket.py +1253 -0
  137. package/lib/site-packages/trio/_tests/test_ssl.py +1371 -0
  138. package/lib/site-packages/trio/_tests/test_subprocess.py +767 -0
  139. package/lib/site-packages/trio/_tests/test_sync.py +735 -0
  140. package/lib/site-packages/trio/_tests/test_testing.py +682 -0
  141. package/lib/site-packages/trio/_tests/test_testing_raisesgroup.py +1128 -0
  142. package/lib/site-packages/trio/_tests/test_threads.py +1173 -0
  143. package/lib/site-packages/trio/_tests/test_timeouts.py +281 -0
  144. package/lib/site-packages/trio/_tests/test_tracing.py +88 -0
  145. package/lib/site-packages/trio/_tests/test_trio.py +8 -0
  146. package/lib/site-packages/trio/_tests/test_unix_pipes.py +288 -0
  147. package/lib/site-packages/trio/_tests/test_util.py +349 -0
  148. package/lib/site-packages/trio/_tests/test_wait_for_object.py +225 -0
  149. package/lib/site-packages/trio/_tests/test_windows_pipes.py +112 -0
  150. package/lib/site-packages/trio/_tests/tools/__init__.py +0 -0
  151. package/lib/site-packages/trio/_tests/tools/test_gen_exports.py +179 -0
  152. package/lib/site-packages/trio/_tests/tools/test_mypy_annotate.py +140 -0
  153. package/lib/site-packages/trio/_tests/tools/test_sync_requirements.py +80 -0
  154. package/lib/site-packages/trio/_tests/type_tests/check_wraps.py +9 -0
  155. package/lib/site-packages/trio/_tests/type_tests/open_memory_channel.py +4 -0
  156. package/lib/site-packages/trio/_tests/type_tests/path.py +140 -0
  157. package/lib/site-packages/trio/_tests/type_tests/subprocesses.py +23 -0
  158. package/lib/site-packages/trio/_tests/type_tests/task_status.py +29 -0
  159. package/lib/site-packages/trio/_threads.py +610 -0
  160. package/lib/site-packages/trio/_timeouts.py +197 -0
  161. package/lib/site-packages/trio/_tools/__init__.py +0 -0
  162. package/lib/site-packages/trio/_tools/gen_exports.py +401 -0
  163. package/lib/site-packages/trio/_tools/mypy_annotate.py +126 -0
  164. package/lib/site-packages/trio/_tools/sync_requirements.py +98 -0
  165. package/lib/site-packages/trio/_tools/windows_ffi_build.py +220 -0
  166. package/lib/site-packages/trio/_unix_pipes.py +197 -0
  167. package/lib/site-packages/trio/_util.py +385 -0
  168. package/lib/site-packages/trio/_version.py +3 -0
  169. package/lib/site-packages/trio/_wait_for_object.py +67 -0
  170. package/lib/site-packages/trio/_windows_pipes.py +144 -0
  171. package/lib/site-packages/trio/abc.py +23 -0
  172. package/lib/site-packages/trio/from_thread.py +13 -0
  173. package/lib/site-packages/trio/lowlevel.py +95 -0
  174. package/lib/site-packages/trio/py.typed +0 -0
  175. package/lib/site-packages/trio/socket.py +602 -0
  176. package/lib/site-packages/trio/testing/__init__.py +58 -0
  177. package/lib/site-packages/trio/testing/_check_streams.py +570 -0
  178. package/lib/site-packages/trio/testing/_checkpoints.py +69 -0
  179. package/lib/site-packages/trio/testing/_fake_net.py +584 -0
  180. package/lib/site-packages/trio/testing/_memory_streams.py +633 -0
  181. package/lib/site-packages/trio/testing/_network.py +36 -0
  182. package/lib/site-packages/trio/testing/_raises_group.py +1015 -0
  183. package/lib/site-packages/trio/testing/_sequencer.py +87 -0
  184. package/lib/site-packages/trio/testing/_trio_test.py +50 -0
  185. package/lib/site-packages/trio/to_thread.py +4 -0
  186. package/lib/site-packages/trio-0.33.0.dist-info/INSTALLER +1 -0
  187. package/lib/site-packages/trio-0.33.0.dist-info/METADATA +186 -0
  188. package/lib/site-packages/trio-0.33.0.dist-info/RECORD +156 -0
  189. package/lib/site-packages/trio-0.33.0.dist-info/REQUESTED +0 -0
  190. package/lib/site-packages/trio-0.33.0.dist-info/WHEEL +5 -0
  191. package/lib/site-packages/trio-0.33.0.dist-info/entry_points.txt +2 -0
  192. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE +3 -0
  193. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.APACHE2 +202 -0
  194. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.MIT +22 -0
  195. package/lib/site-packages/trio-0.33.0.dist-info/top_level.txt +1 -0
  196. package/package.json +1 -1
@@ -0,0 +1,385 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import select
5
+ import sys
6
+ from collections import defaultdict
7
+ from typing import TYPE_CHECKING, Literal, TypeAlias
8
+
9
+ import attrs
10
+
11
+ from .. import _core
12
+ from ._io_common import wake_all
13
+ from ._run import Task, _public
14
+ from ._wakeup_socketpair import WakeupSocketpair
15
+
16
+ if TYPE_CHECKING:
17
+ from .._core import Abort, RaiseCancelT
18
+ from .._file_io import _HasFileNo
19
+
20
+
21
+ @attrs.define(eq=False)
22
+ class EpollWaiters:
23
+ read_task: Task | None = None
24
+ write_task: Task | None = None
25
+ current_flags: int = 0
26
+
27
+
28
+ assert not TYPE_CHECKING or sys.platform == "linux"
29
+
30
+
31
+ EventResult: TypeAlias = "list[tuple[int, int]]"
32
+
33
+
34
+ @attrs.frozen(eq=False)
35
+ class _EpollStatistics:
36
+ tasks_waiting_read: int
37
+ tasks_waiting_write: int
38
+ backend: Literal["epoll"] = attrs.field(init=False, default="epoll")
39
+
40
+
41
+ # Some facts about epoll
42
+ # ----------------------
43
+ #
44
+ # Internally, an epoll object is sort of like a WeakKeyDictionary where the
45
+ # keys are tuples of (fd number, file object). When you call epoll_ctl, you
46
+ # pass in an fd; that gets converted to an (fd number, file object) tuple by
47
+ # looking up the fd in the process's fd table at the time of the call. When an
48
+ # event happens on the file object, epoll_wait drops the file object part, and
49
+ # just returns the fd number in its event. So from the outside it looks like
50
+ # it's keeping a table of fds, but really it's a bit more complicated. This
51
+ # has some subtle consequences.
52
+ #
53
+ # In general, file objects inside the kernel are reference counted. Each entry
54
+ # in a process's fd table holds a strong reference to the corresponding file
55
+ # object, and most operations that use file objects take a temporary strong
56
+ # reference while they're working. So when you call close() on an fd, that
57
+ # might or might not cause the file object to be deallocated -- it depends on
58
+ # whether there are any other references to that file object. Some common ways
59
+ # this can happen:
60
+ #
61
+ # - after calling dup(), you have two fds in the same process referring to the
62
+ # same file object. Even if you close one fd (= remove that entry from the
63
+ # fd table), the file object will be kept alive by the other fd.
64
+ # - when calling fork(), the child inherits a copy of the parent's fd table,
65
+ # so all the file objects get another reference. (But if the fork() is
66
+ # followed by exec(), then all of the child's fds that have the CLOEXEC flag
67
+ # set will be closed at that point.)
68
+ # - most syscalls that work on fds take a strong reference to the underlying
69
+ # file object while they're using it. So there's one thread blocked in
70
+ # read(fd), and then another thread calls close() on the last fd referring
71
+ # to that object, the underlying file won't actually be closed until
72
+ # after read() returns.
73
+ #
74
+ # However, epoll does *not* take a reference to any of the file objects in its
75
+ # interest set (that's what makes it similar to a WeakKeyDictionary). File
76
+ # objects inside an epoll interest set will be deallocated if all *other*
77
+ # references to them are closed. And when that happens, the epoll object will
78
+ # automatically deregister that file object and stop reporting events on it.
79
+ # So that's quite handy.
80
+ #
81
+ # But, what happens if we do this?
82
+ #
83
+ # fd1 = open(...)
84
+ # epoll_ctl(EPOLL_CTL_ADD, fd1, ...)
85
+ # fd2 = dup(fd1)
86
+ # close(fd1)
87
+ #
88
+ # In this case, the dup() keeps the underlying file object alive, so it
89
+ # remains registered in the epoll object's interest set, as the tuple (fd1,
90
+ # file object). But, fd1 no longer refers to this file object! You might think
91
+ # there was some magic to handle this, but unfortunately no; the consequences
92
+ # are totally predictable from what I said above:
93
+ #
94
+ # If any events occur on the file object, then epoll will report them as
95
+ # happening on fd1, even though that doesn't make sense.
96
+ #
97
+ # Perhaps we would like to deregister fd1 to stop getting nonsensical events.
98
+ # But how? When we call epoll_ctl, we have to pass an fd number, which will
99
+ # get expanded to an (fd number, file object) tuple. We can't pass fd1,
100
+ # because when epoll_ctl tries to look it up, it won't find our file object.
101
+ # And we can't pass fd2, because that will get expanded to (fd2, file object),
102
+ # which is a different lookup key. In fact, it's *impossible* to de-register
103
+ # this fd!
104
+ #
105
+ # We could even have fd1 get assigned to another file object, and then we can
106
+ # have multiple keys registered simultaneously using the same fd number, like:
107
+ # (fd1, file object 1), (fd1, file object 2). And if events happen on either
108
+ # file object, then epoll will happily report that something happened to
109
+ # "fd1".
110
+ #
111
+ # Now here's what makes this especially nasty: suppose the old file object
112
+ # becomes, say, readable. That means that every time we call epoll_wait, it
113
+ # will return immediately to tell us that "fd1" is readable. Normally, we
114
+ # would handle this by de-registering fd1, waking up the corresponding call to
115
+ # wait_readable, then the user will call read() or recv() or something, and
116
+ # we're fine. But if this happens on a stale fd where we can't remove the
117
+ # registration, then we might get stuck in a state where epoll_wait *always*
118
+ # returns immediately, so our event loop becomes unable to sleep, and now our
119
+ # program is burning 100% of the CPU doing nothing, with no way out.
120
+ #
121
+ #
122
+ # What does this mean for Trio?
123
+ # -----------------------------
124
+ #
125
+ # Since we don't control the user's code, we have no way to guarantee that we
126
+ # don't get stuck with stale fd's in our epoll interest set. For example, a
127
+ # user could call wait_readable(fd) in one task, and then while that's
128
+ # running, they might close(fd) from another task. In this situation, they're
129
+ # *supposed* to call notify_closing(fd) to let us know what's happening, so we
130
+ # can interrupt the wait_readable() call and avoid getting into this mess. And
131
+ # that's the only thing that can possibly work correctly in all cases. But
132
+ # sometimes user code has bugs. So if this does happen, we'd like to degrade
133
+ # gracefully, and survive without corrupting Trio's internal state or
134
+ # otherwise causing the whole program to explode messily.
135
+ #
136
+ # Our solution: we always use EPOLLONESHOT. This way, we might get *one*
137
+ # spurious event on a stale fd, but then epoll will automatically silence it
138
+ # until we explicitly say that we want more events... and if we have a stale
139
+ # fd, then we actually can't re-enable it! So we can't get stuck in an
140
+ # infinite busy-loop. If there's a stale fd hanging around, then it might
141
+ # cause a spurious `BusyResourceError`, or cause one wait_* call to return
142
+ # before it should have... but in general, the wait_* functions are allowed to
143
+ # have some spurious wakeups; the user code will just attempt the operation,
144
+ # get EWOULDBLOCK, and call wait_* again. And the program as a whole will
145
+ # survive, any exceptions will propagate, etc.
146
+ #
147
+ # As a bonus, EPOLLONESHOT also saves us having to explicitly deregister fds
148
+ # on the normal wakeup path, so it's a bit more efficient in general.
149
+ #
150
+ # However, EPOLLONESHOT has a few trade-offs to consider:
151
+ #
152
+ # First, you can't combine EPOLLONESHOT with EPOLLEXCLUSIVE. This is a bit sad
153
+ # in one somewhat rare case: if you have a multi-process server where a group
154
+ # of processes all share the same listening socket, then EPOLLEXCLUSIVE can be
155
+ # used to avoid "thundering herd" problems when a new connection comes in. But
156
+ # this isn't too bad. It's not clear if EPOLLEXCLUSIVE even works for us
157
+ # anyway:
158
+ #
159
+ # https://stackoverflow.com/questions/41582560/how-does-epolls-epollexclusive-mode-interact-with-level-triggering
160
+ #
161
+ # And it's not clear that EPOLLEXCLUSIVE is a great approach either:
162
+ #
163
+ # https://blog.cloudflare.com/the-sad-state-of-linux-socket-balancing/
164
+ #
165
+ # And if we do need to support this, we could always add support through some
166
+ # more-specialized API in the future. So this isn't a blocker to using
167
+ # EPOLLONESHOT.
168
+ #
169
+ # Second, EPOLLONESHOT does not actually *deregister* the fd after delivering
170
+ # an event (EPOLL_CTL_DEL). Instead, it keeps the fd registered, but
171
+ # effectively does an EPOLL_CTL_MOD to set the fd's interest flags to
172
+ # all-zeros. So we could still end up with an fd hanging around in the
173
+ # interest set for a long time, even if we're not using it.
174
+ #
175
+ # Fortunately, this isn't a problem, because it's only a weak reference – if
176
+ # we have a stale fd that's been silenced by EPOLLONESHOT, then it wastes a
177
+ # tiny bit of kernel memory remembering this fd that can never be revived, but
178
+ # when the underlying file object is eventually closed, that memory will be
179
+ # reclaimed. So that's OK.
180
+ #
181
+ # The other issue is that when someone calls wait_*, using EPOLLONESHOT means
182
+ # that if we have ever waited for this fd before, we have to use EPOLL_CTL_MOD
183
+ # to re-enable it; but if it's a new fd, we have to use EPOLL_CTL_ADD. How do
184
+ # we know which one to use? There's no reasonable way to track which fds are
185
+ # currently registered -- remember, we're assuming the user might have gone
186
+ # and rearranged their fds without telling us!
187
+ #
188
+ # Fortunately, this also has a simple solution: if we wait on a socket or
189
+ # other fd once, then we'll probably wait on it lots of times. And the epoll
190
+ # object itself knows which fds it already has registered. So when an fd comes
191
+ # in, we optimistically assume that it's been waited on before, and try doing
192
+ # EPOLL_CTL_MOD. And if that fails with an ENOENT error, then we try again
193
+ # with EPOLL_CTL_ADD.
194
+ #
195
+ # So that's why this code is the way it is. And now you know more than you
196
+ # wanted to about how epoll works.
197
+
198
+
199
+ @attrs.define(eq=False)
200
+ class EpollIOManager:
201
+ # Using lambda here because otherwise crash on import with gevent monkey patching
202
+ # See https://github.com/python-trio/trio/issues/2848
203
+ _epoll: select.epoll = attrs.Factory(lambda: select.epoll())
204
+ # {fd: EpollWaiters}
205
+ _registered: defaultdict[int, EpollWaiters] = attrs.Factory(
206
+ lambda: defaultdict(EpollWaiters),
207
+ )
208
+ _force_wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair)
209
+ _force_wakeup_fd: int | None = None
210
+
211
+ def __attrs_post_init__(self) -> None:
212
+ self._epoll.register(self._force_wakeup.wakeup_sock, select.EPOLLIN)
213
+ self._force_wakeup_fd = self._force_wakeup.wakeup_sock.fileno()
214
+
215
+ def statistics(self) -> _EpollStatistics:
216
+ tasks_waiting_read = 0
217
+ tasks_waiting_write = 0
218
+ for waiter in self._registered.values():
219
+ if waiter.read_task is not None:
220
+ tasks_waiting_read += 1
221
+ if waiter.write_task is not None:
222
+ tasks_waiting_write += 1
223
+ return _EpollStatistics(
224
+ tasks_waiting_read=tasks_waiting_read,
225
+ tasks_waiting_write=tasks_waiting_write,
226
+ )
227
+
228
+ def close(self) -> None:
229
+ self._epoll.close()
230
+ self._force_wakeup.close()
231
+
232
+ def force_wakeup(self) -> None:
233
+ self._force_wakeup.wakeup_thread_and_signal_safe()
234
+
235
+ # Return value must be False-y IFF the timeout expired, NOT if any I/O
236
+ # happened or force_wakeup was called. Otherwise it can be anything; gets
237
+ # passed straight through to process_events.
238
+ def get_events(self, timeout: float) -> EventResult:
239
+ # max_events must be > 0 or epoll gets cranky
240
+ # accessing self._registered from a thread looks dangerous, but it's
241
+ # OK because it doesn't matter if our value is a little bit off.
242
+ max_events = max(1, len(self._registered))
243
+ return self._epoll.poll(timeout, max_events)
244
+
245
+ def process_events(self, events: EventResult) -> None:
246
+ for fd, flags in events:
247
+ if fd == self._force_wakeup_fd:
248
+ self._force_wakeup.drain()
249
+ continue
250
+ waiters = self._registered[fd]
251
+ # EPOLLONESHOT always clears the flags when an event is delivered
252
+ waiters.current_flags = 0
253
+ # Clever hack stolen from selectors.EpollSelector: an event
254
+ # with EPOLLHUP or EPOLLERR flags wakes both readers and
255
+ # writers.
256
+ if flags & ~select.EPOLLIN and waiters.write_task is not None:
257
+ _core.reschedule(waiters.write_task)
258
+ waiters.write_task = None
259
+ if flags & ~select.EPOLLOUT and waiters.read_task is not None:
260
+ _core.reschedule(waiters.read_task)
261
+ waiters.read_task = None
262
+ self._update_registrations(fd)
263
+
264
+ def _update_registrations(self, fd: int) -> None:
265
+ waiters = self._registered[fd]
266
+ wanted_flags = 0
267
+ if waiters.read_task is not None:
268
+ wanted_flags |= select.EPOLLIN
269
+ if waiters.write_task is not None:
270
+ wanted_flags |= select.EPOLLOUT
271
+ if wanted_flags != waiters.current_flags:
272
+ try:
273
+ try:
274
+ # First try EPOLL_CTL_MOD
275
+ self._epoll.modify(fd, wanted_flags | select.EPOLLONESHOT)
276
+ except OSError:
277
+ # If that fails, it might be a new fd; try EPOLL_CTL_ADD
278
+ self._epoll.register(fd, wanted_flags | select.EPOLLONESHOT)
279
+ waiters.current_flags = wanted_flags
280
+ except OSError as exc:
281
+ # If everything fails, probably it's a bad fd, e.g. because
282
+ # the fd was closed behind our back. In this case we don't
283
+ # want to try to unregister the fd, because that will probably
284
+ # fail too. Just clear our state and wake everyone up.
285
+ del self._registered[fd]
286
+ # This could raise (in case we're calling this inside one of
287
+ # the to-be-woken tasks), so we have to do it last.
288
+ wake_all(waiters, exc)
289
+ return
290
+ if not wanted_flags:
291
+ del self._registered[fd]
292
+
293
+ async def _epoll_wait(self, fd: int | _HasFileNo, attr_name: str) -> None:
294
+ if not isinstance(fd, int):
295
+ fd = fd.fileno()
296
+ waiters = self._registered[fd]
297
+ if getattr(waiters, attr_name) is not None:
298
+ raise _core.BusyResourceError(
299
+ "another task is already reading / writing this fd",
300
+ )
301
+ setattr(waiters, attr_name, _core.current_task())
302
+ self._update_registrations(fd)
303
+
304
+ def abort(_: RaiseCancelT) -> Abort:
305
+ setattr(waiters, attr_name, None)
306
+ self._update_registrations(fd)
307
+ return _core.Abort.SUCCEEDED
308
+
309
+ await _core.wait_task_rescheduled(abort)
310
+
311
+ @_public
312
+ async def wait_readable(self, fd: int | _HasFileNo) -> None:
313
+ """Block until the kernel reports that the given object is readable.
314
+
315
+ On Unix systems, ``fd`` must either be an integer file descriptor,
316
+ or else an object with a ``.fileno()`` method which returns an
317
+ integer file descriptor. Any kind of file descriptor can be passed,
318
+ though the exact semantics will depend on your kernel. For example,
319
+ this probably won't do anything useful for on-disk files.
320
+
321
+ On Windows systems, ``fd`` must either be an integer ``SOCKET``
322
+ handle, or else an object with a ``.fileno()`` method which returns
323
+ an integer ``SOCKET`` handle. File descriptors aren't supported,
324
+ and neither are handles that refer to anything besides a
325
+ ``SOCKET``.
326
+
327
+ :raises trio.BusyResourceError:
328
+ if another task is already waiting for the given socket to
329
+ become readable.
330
+ :raises trio.ClosedResourceError:
331
+ if another task calls :func:`notify_closing` while this
332
+ function is still working.
333
+ """
334
+ await self._epoll_wait(fd, "read_task")
335
+
336
+ @_public
337
+ async def wait_writable(self, fd: int | _HasFileNo) -> None:
338
+ """Block until the kernel reports that the given object is writable.
339
+
340
+ See `wait_readable` for the definition of ``fd``.
341
+
342
+ :raises trio.BusyResourceError:
343
+ if another task is already waiting for the given socket to
344
+ become writable.
345
+ :raises trio.ClosedResourceError:
346
+ if another task calls :func:`notify_closing` while this
347
+ function is still working.
348
+ """
349
+ await self._epoll_wait(fd, "write_task")
350
+
351
+ @_public
352
+ def notify_closing(self, fd: int | _HasFileNo) -> None:
353
+ """Notify waiters of the given object that it will be closed.
354
+
355
+ Call this before closing a file descriptor (on Unix) or socket (on
356
+ Windows). This will cause any `wait_readable` or `wait_writable`
357
+ calls on the given object to immediately wake up and raise
358
+ `~trio.ClosedResourceError`.
359
+
360
+ This doesn't actually close the object – you still have to do that
361
+ yourself afterwards. Also, you want to be careful to make sure no
362
+ new tasks start waiting on the object in between when you call this
363
+ and when it's actually closed. So to close something properly, you
364
+ usually want to do these steps in order:
365
+
366
+ 1. Explicitly mark the object as closed, so that any new attempts
367
+ to use it will abort before they start.
368
+ 2. Call `notify_closing` to wake up any already-existing users.
369
+ 3. Actually close the object.
370
+
371
+ It's also possible to do them in a different order if that's more
372
+ convenient, *but only if* you make sure not to have any checkpoints in
373
+ between the steps. This way they all happen in a single atomic
374
+ step, so other tasks won't be able to tell what order they happened
375
+ in anyway.
376
+ """
377
+ if not isinstance(fd, int):
378
+ fd = fd.fileno()
379
+ wake_all(
380
+ self._registered[fd],
381
+ _core.ClosedResourceError("another task closed this fd"),
382
+ )
383
+ del self._registered[fd]
384
+ with contextlib.suppress(OSError, ValueError):
385
+ self._epoll.unregister(fd)
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import errno
4
+ import select
5
+ import sys
6
+ from contextlib import contextmanager
7
+ from typing import TYPE_CHECKING, Literal, TypeAlias
8
+
9
+ import attrs
10
+ import outcome
11
+
12
+ from .. import _core
13
+ from ._run import _public
14
+ from ._wakeup_socketpair import WakeupSocketpair
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable, Iterator
18
+
19
+ from .._core import Abort, RaiseCancelT, Task, UnboundedQueue
20
+ from .._file_io import _HasFileNo
21
+
22
+ assert not TYPE_CHECKING or (sys.platform != "linux" and sys.platform != "win32")
23
+
24
+ EventResult: TypeAlias = "list[select.kevent]"
25
+
26
+
27
+ @attrs.frozen(eq=False)
28
+ class _KqueueStatistics:
29
+ tasks_waiting: int
30
+ monitors: int
31
+ backend: Literal["kqueue"] = attrs.field(init=False, default="kqueue")
32
+
33
+
34
+ @attrs.define(eq=False)
35
+ class KqueueIOManager:
36
+ _kqueue: select.kqueue = attrs.Factory(select.kqueue)
37
+ # {(ident, filter): Task or UnboundedQueue}
38
+ _registered: dict[tuple[int, int], Task | UnboundedQueue[select.kevent]] = (
39
+ attrs.Factory(dict)
40
+ )
41
+ _force_wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair)
42
+ _force_wakeup_fd: int | None = None
43
+
44
+ def __attrs_post_init__(self) -> None:
45
+ force_wakeup_event = select.kevent(
46
+ self._force_wakeup.wakeup_sock,
47
+ select.KQ_FILTER_READ,
48
+ select.KQ_EV_ADD,
49
+ )
50
+ self._kqueue.control([force_wakeup_event], 0)
51
+ self._force_wakeup_fd = self._force_wakeup.wakeup_sock.fileno()
52
+
53
+ def statistics(self) -> _KqueueStatistics:
54
+ tasks_waiting = 0
55
+ monitors = 0
56
+ for receiver in self._registered.values():
57
+ if type(receiver) is _core.Task:
58
+ tasks_waiting += 1
59
+ else:
60
+ monitors += 1
61
+ return _KqueueStatistics(tasks_waiting=tasks_waiting, monitors=monitors)
62
+
63
+ def close(self) -> None:
64
+ self._kqueue.close()
65
+ self._force_wakeup.close()
66
+
67
+ def force_wakeup(self) -> None:
68
+ self._force_wakeup.wakeup_thread_and_signal_safe()
69
+
70
+ def get_events(self, timeout: float) -> EventResult:
71
+ # max_events must be > 0 or kqueue gets cranky
72
+ # and we generally want this to be strictly larger than the actual
73
+ # number of events we get, so that we can tell that we've gotten
74
+ # all the events in just 1 call.
75
+ max_events = len(self._registered) + 1
76
+ events = []
77
+ while True:
78
+ batch = self._kqueue.control([], max_events, timeout)
79
+ events += batch
80
+ if len(batch) < max_events:
81
+ break
82
+ else: # TODO: test this line
83
+ timeout = 0
84
+ # and loop back to the start
85
+ return events
86
+
87
+ def process_events(self, events: EventResult) -> None:
88
+ for event in events:
89
+ key = (event.ident, event.filter)
90
+ if event.ident == self._force_wakeup_fd:
91
+ self._force_wakeup.drain()
92
+ continue
93
+ receiver = self._registered[key]
94
+ if event.flags & select.KQ_EV_ONESHOT: # TODO: test this branch
95
+ del self._registered[key]
96
+ if isinstance(receiver, _core.Task):
97
+ _core.reschedule(receiver, outcome.Value(event))
98
+ else:
99
+ receiver.put_nowait(event) # TODO: test this line
100
+
101
+ # kevent registration is complicated -- e.g. aio submission can
102
+ # implicitly perform a EV_ADD, and EVFILT_PROC with NOTE_TRACK will
103
+ # automatically register filters for child processes. So our lowlevel
104
+ # API is *very* low-level: we expose the kqueue itself for adding
105
+ # events or sticking into AIO submission structs, and split waiting
106
+ # off into separate methods. It's your responsibility to make sure
107
+ # that handle_io never receives an event without a corresponding
108
+ # registration! This may be challenging if you want to be careful
109
+ # about e.g. KeyboardInterrupt. Possibly this API could be improved to
110
+ # be more ergonomic...
111
+
112
+ @_public
113
+ def current_kqueue(self) -> select.kqueue:
114
+ """TODO: these are implemented, but are currently more of a sketch than
115
+ anything real. See `#26
116
+ <https://github.com/python-trio/trio/issues/26>`__.
117
+ """
118
+ return self._kqueue
119
+
120
+ @contextmanager
121
+ @_public
122
+ def monitor_kevent(
123
+ self,
124
+ ident: int,
125
+ filter: int,
126
+ ) -> Iterator[_core.UnboundedQueue[select.kevent]]:
127
+ """TODO: these are implemented, but are currently more of a sketch than
128
+ anything real. See `#26
129
+ <https://github.com/python-trio/trio/issues/26>`__.
130
+ """
131
+ key = (ident, filter)
132
+ if key in self._registered:
133
+ raise _core.BusyResourceError(
134
+ "attempt to register multiple listeners for same ident/filter pair",
135
+ )
136
+ q = _core.UnboundedQueue[select.kevent]()
137
+ self._registered[key] = q
138
+ try:
139
+ yield q
140
+ finally:
141
+ del self._registered[key]
142
+
143
+ @_public
144
+ async def wait_kevent(
145
+ self,
146
+ ident: int,
147
+ filter: int,
148
+ abort_func: Callable[[RaiseCancelT], Abort],
149
+ ) -> Abort:
150
+ """TODO: these are implemented, but are currently more of a sketch than
151
+ anything real. See `#26
152
+ <https://github.com/python-trio/trio/issues/26>`__.
153
+ """
154
+ key = (ident, filter)
155
+ if key in self._registered:
156
+ raise _core.BusyResourceError(
157
+ "attempt to register multiple listeners for same ident/filter pair",
158
+ )
159
+ self._registered[key] = _core.current_task()
160
+
161
+ def abort(raise_cancel: RaiseCancelT) -> Abort:
162
+ r = abort_func(raise_cancel)
163
+ if r is _core.Abort.SUCCEEDED: # TODO: test this branch
164
+ del self._registered[key]
165
+ return r
166
+
167
+ # wait_task_rescheduled does not have its return type typed
168
+ return await _core.wait_task_rescheduled(abort) # type: ignore[no-any-return]
169
+
170
+ async def _wait_common(
171
+ self,
172
+ fd: int | _HasFileNo,
173
+ filter: int,
174
+ ) -> None:
175
+ if not isinstance(fd, int):
176
+ fd = fd.fileno()
177
+ flags = select.KQ_EV_ADD | select.KQ_EV_ONESHOT
178
+ event = select.kevent(fd, filter, flags)
179
+ self._kqueue.control([event], 0)
180
+
181
+ def abort(_: RaiseCancelT) -> Abort:
182
+ event = select.kevent(fd, filter, select.KQ_EV_DELETE)
183
+ try:
184
+ self._kqueue.control([event], 0)
185
+ except OSError as exc:
186
+ # kqueue tracks individual fds (*not* the underlying file
187
+ # object, see _io_epoll.py for a long discussion of why this
188
+ # distinction matters), and automatically deregisters an event
189
+ # if the fd is closed. So if kqueue.control says that it
190
+ # doesn't know about this event, then probably it's because
191
+ # the fd was closed behind our backs. (Too bad we can't ask it
192
+ # to wake us up when this happens, versus discovering it after
193
+ # the fact... oh well, you can't have everything.)
194
+ #
195
+ # FreeBSD reports this using EBADF. macOS uses ENOENT.
196
+ if exc.errno in (errno.EBADF, errno.ENOENT): # pragma: no branch
197
+ pass
198
+ else: # pragma: no cover
199
+ # As far as we know, this branch can't happen.
200
+ raise
201
+ return _core.Abort.SUCCEEDED
202
+
203
+ await self.wait_kevent(fd, filter, abort)
204
+
205
+ @_public
206
+ async def wait_readable(self, fd: int | _HasFileNo) -> None:
207
+ """Block until the kernel reports that the given object is readable.
208
+
209
+ On Unix systems, ``fd`` must either be an integer file descriptor,
210
+ or else an object with a ``.fileno()`` method which returns an
211
+ integer file descriptor. Any kind of file descriptor can be passed,
212
+ though the exact semantics will depend on your kernel. For example,
213
+ this probably won't do anything useful for on-disk files.
214
+
215
+ On Windows systems, ``fd`` must either be an integer ``SOCKET``
216
+ handle, or else an object with a ``.fileno()`` method which returns
217
+ an integer ``SOCKET`` handle. File descriptors aren't supported,
218
+ and neither are handles that refer to anything besides a
219
+ ``SOCKET``.
220
+
221
+ :raises trio.BusyResourceError:
222
+ if another task is already waiting for the given socket to
223
+ become readable.
224
+ :raises trio.ClosedResourceError:
225
+ if another task calls :func:`notify_closing` while this
226
+ function is still working.
227
+ """
228
+ await self._wait_common(fd, select.KQ_FILTER_READ)
229
+
230
+ @_public
231
+ async def wait_writable(self, fd: int | _HasFileNo) -> None:
232
+ """Block until the kernel reports that the given object is writable.
233
+
234
+ See `wait_readable` for the definition of ``fd``.
235
+
236
+ :raises trio.BusyResourceError:
237
+ if another task is already waiting for the given socket to
238
+ become writable.
239
+ :raises trio.ClosedResourceError:
240
+ if another task calls :func:`notify_closing` while this
241
+ function is still working.
242
+ """
243
+ await self._wait_common(fd, select.KQ_FILTER_WRITE)
244
+
245
+ @_public
246
+ def notify_closing(self, fd: int | _HasFileNo) -> None:
247
+ """Notify waiters of the given object that it will be closed.
248
+
249
+ Call this before closing a file descriptor (on Unix) or socket (on
250
+ Windows). This will cause any `wait_readable` or `wait_writable`
251
+ calls on the given object to immediately wake up and raise
252
+ `~trio.ClosedResourceError`.
253
+
254
+ This doesn't actually close the object – you still have to do that
255
+ yourself afterwards. Also, you want to be careful to make sure no
256
+ new tasks start waiting on the object in between when you call this
257
+ and when it's actually closed. So to close something properly, you
258
+ usually want to do these steps in order:
259
+
260
+ 1. Explicitly mark the object as closed, so that any new attempts
261
+ to use it will abort before they start.
262
+ 2. Call `notify_closing` to wake up any already-existing users.
263
+ 3. Actually close the object.
264
+
265
+ It's also possible to do them in a different order if that's more
266
+ convenient, *but only if* you make sure not to have any checkpoints in
267
+ between the steps. This way they all happen in a single atomic
268
+ step, so other tasks won't be able to tell what order they happened
269
+ in anyway.
270
+ """
271
+ if not isinstance(fd, int):
272
+ fd = fd.fileno()
273
+
274
+ for filter_ in [select.KQ_FILTER_READ, select.KQ_FILTER_WRITE]:
275
+ key = (fd, filter_)
276
+ receiver = self._registered.get(key)
277
+
278
+ if receiver is None:
279
+ continue
280
+
281
+ if type(receiver) is _core.Task:
282
+ event = select.kevent(fd, filter_, select.KQ_EV_DELETE)
283
+ self._kqueue.control([event], 0)
284
+ exc = _core.ClosedResourceError("another task closed this fd")
285
+ _core.reschedule(receiver, outcome.Error(exc))
286
+ del self._registered[key]
287
+ else:
288
+ # XX this is an interesting example of a case where being able
289
+ # to close a queue would be useful...
290
+ raise NotImplementedError(
291
+ "can't close an fd that monitor_kevent is using",
292
+ )