readerboard 0.7.0__tar.gz → 0.7.1__tar.gz
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.
- {readerboard-0.7.0/readerboard.egg-info → readerboard-0.7.1}/PKG-INFO +18 -2
- {readerboard-0.7.0 → readerboard-0.7.1}/README.md +16 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/pyproject.toml +7 -2
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/__init__.py +1 -1
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/transport/serial_link.py +63 -0
- {readerboard-0.7.0 → readerboard-0.7.1/readerboard.egg-info}/PKG-INFO +18 -2
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard.egg-info/requires.txt +1 -1
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_transport.py +101 -1
- {readerboard-0.7.0 → readerboard-0.7.1}/LICENSE +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/__main__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/__init__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/app.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/deps.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/errors.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/models.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/api/routes.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/config.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/icons.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/logging_setup.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/names.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/__init__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/constants.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/frames.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/markup.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/replies.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/protocol/tokens.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/py.typed +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/services/__init__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/services/alerts.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/services/clock.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/services/commands.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/services/registry.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/sign/__init__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/sign/controller.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/sign/layout.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/sign/pool.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/sign/state.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/transport/__init__.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/transport/base.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard/transport/fake.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard.egg-info/SOURCES.txt +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard.egg-info/dependency_links.txt +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard.egg-info/entry_points.txt +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/readerboard.egg-info/top_level.txt +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/setup.cfg +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_alerts.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_api.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_clock.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_component_names.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_config.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_constant_values.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_controller.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_frames.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_icons.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_launch_configurations.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_logging_setup.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_markup.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_open_docs.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_pictures.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_pool.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_registry.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_replies.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_run_against_a_sign.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_run_with_simulator.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_state.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_tool_icons.py +0 -0
- {readerboard-0.7.0 → readerboard-0.7.1}/tests/test_variables.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,7 +34,7 @@ Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
|
34
34
|
Requires-Dist: pyserial<4,>=3.5
|
|
35
35
|
Requires-Dist: tzdata>=2026.3; sys_platform == "win32"
|
|
36
36
|
Provides-Extra: dev
|
|
37
|
-
Requires-Dist: pytest<
|
|
37
|
+
Requires-Dist: pytest<9.2,>=9.1.1; extra == "dev"
|
|
38
38
|
Requires-Dist: pytest-asyncio<2,>=1.4.0; extra == "dev"
|
|
39
39
|
Requires-Dist: httpx2<3,>=2.10.0; extra == "dev"
|
|
40
40
|
Requires-Dist: anyio<4.15,>=4.14.2; extra == "dev"
|
|
@@ -184,6 +184,22 @@ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
|
|
|
184
184
|
probably negotiating telnet rather than passing bytes through, and `rfc2217://`
|
|
185
185
|
is the form that speaks that.
|
|
186
186
|
|
|
187
|
+
An adapter that drops off the network says nothing as it goes. The service asks
|
|
188
|
+
the operating system to give a network link up about a minute after the adapter
|
|
189
|
+
stops answering: on Linux, a minute after the first write it did not
|
|
190
|
+
acknowledge, or, if nothing was written, about a minute after it last answered,
|
|
191
|
+
found by keepalive probes. A platform that does not take one of these options
|
|
192
|
+
keeps its own default for it, which can be much longer. The service itself only
|
|
193
|
+
finds out at its first read or write after the link has gone, and `GET /health`
|
|
194
|
+
shows `"connected": false` from then on. A sign with a variable updated every
|
|
195
|
+
minute is written to that often. One left alone is written to at the refresh,
|
|
196
|
+
every `refresh_interval_seconds`, which is 15 minutes by default, and a refresh
|
|
197
|
+
that lands before the link has gone is buffered like any other write, so on a
|
|
198
|
+
quiet sign `link.connected` can stay true for a refresh interval and a minute
|
|
199
|
+
after the adapter has gone. It is still worth a monitor watching, and needs no
|
|
200
|
+
key, but a ping of the adapter answers within a minute however quiet the sign
|
|
201
|
+
is, and is the better alarm.
|
|
202
|
+
|
|
187
203
|
### The API key, and config.local.toml
|
|
188
204
|
|
|
189
205
|
The key is not an argument. A launch configuration is a tracked file and a
|
|
@@ -140,6 +140,22 @@ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
|
|
|
140
140
|
probably negotiating telnet rather than passing bytes through, and `rfc2217://`
|
|
141
141
|
is the form that speaks that.
|
|
142
142
|
|
|
143
|
+
An adapter that drops off the network says nothing as it goes. The service asks
|
|
144
|
+
the operating system to give a network link up about a minute after the adapter
|
|
145
|
+
stops answering: on Linux, a minute after the first write it did not
|
|
146
|
+
acknowledge, or, if nothing was written, about a minute after it last answered,
|
|
147
|
+
found by keepalive probes. A platform that does not take one of these options
|
|
148
|
+
keeps its own default for it, which can be much longer. The service itself only
|
|
149
|
+
finds out at its first read or write after the link has gone, and `GET /health`
|
|
150
|
+
shows `"connected": false` from then on. A sign with a variable updated every
|
|
151
|
+
minute is written to that often. One left alone is written to at the refresh,
|
|
152
|
+
every `refresh_interval_seconds`, which is 15 minutes by default, and a refresh
|
|
153
|
+
that lands before the link has gone is buffered like any other write, so on a
|
|
154
|
+
quiet sign `link.connected` can stay true for a refresh interval and a minute
|
|
155
|
+
after the adapter has gone. It is still worth a monitor watching, and needs no
|
|
156
|
+
key, but a ping of the adapter answers within a minute however quiet the sign
|
|
157
|
+
is, and is the better alarm.
|
|
158
|
+
|
|
143
159
|
### The API key, and config.local.toml
|
|
144
160
|
|
|
145
161
|
The key is not an argument. A launch configuration is a tracked file and a
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "readerboard"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
description = "An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -63,7 +63,12 @@ dependencies = [
|
|
|
63
63
|
|
|
64
64
|
[project.optional-dependencies]
|
|
65
65
|
dev = [
|
|
66
|
-
|
|
66
|
+
# pytest 9.2 renamed CallSpec2 and deprecated the old name, which the anyio
|
|
67
|
+
# held below 4.15 further down still imports in its pytest plugin. That is
|
|
68
|
+
# the same collection error again, raised as the plugin loads, so no test
|
|
69
|
+
# runs at all. anyio 4.16 copes with the rename, so this ceiling comes off
|
|
70
|
+
# when the anyio one does.
|
|
71
|
+
"pytest>=9.1.1,<9.2",
|
|
67
72
|
"pytest-asyncio>=1.4.0,<2",
|
|
68
73
|
# starlette's TestClient moved to httpx2; plain httpx now raises a
|
|
69
74
|
# deprecation warning, which this suite is configured to treat as an error.
|
|
@@ -14,11 +14,20 @@ Reconnection is deliberately passive. When the link is down, ``ensure_open``
|
|
|
14
14
|
refuses to try again until a backoff window has passed, and ``write`` fails
|
|
15
15
|
immediately rather than blocking a request behind a device that is not there.
|
|
16
16
|
Something has to drive the retries; that is the controller's reconnect loop.
|
|
17
|
+
|
|
18
|
+
A link over the network is also told to notice a peer that has gone. An adapter
|
|
19
|
+
that drops off the network says nothing as it goes, and without being told
|
|
20
|
+
otherwise the operating system keeps accepting writes to it: Linux retries an
|
|
21
|
+
unacknowledged one for about a quarter of an hour before reporting anything, and
|
|
22
|
+
an idle connection is never checked at all. Meanwhile ``GET /health`` says the
|
|
23
|
+
link is up and the sign holds whatever it was last sent, which on a sign showing
|
|
24
|
+
the time looks exactly like a working one. See :func:`_notice_a_dead_peer`.
|
|
17
25
|
"""
|
|
18
26
|
|
|
19
27
|
from __future__ import annotations
|
|
20
28
|
|
|
21
29
|
import logging
|
|
30
|
+
import socket
|
|
22
31
|
import threading
|
|
23
32
|
import time
|
|
24
33
|
from collections.abc import Callable
|
|
@@ -35,6 +44,20 @@ logger = logging.getLogger(__name__)
|
|
|
35
44
|
# lock, and never let the controller reach its deadline.
|
|
36
45
|
READ_LIMIT_BYTES = 4096
|
|
37
46
|
|
|
47
|
+
# How a network link finds out that the adapter at the other end has gone. An
|
|
48
|
+
# idle connection is probed after KEEPALIVE_IDLE_SECONDS of quiet, every
|
|
49
|
+
# KEEPALIVE_INTERVAL_SECONDS after that, and given up after KEEPALIVE_PROBES
|
|
50
|
+
# unanswered probes: about a minute. Probes are only sent while nothing is
|
|
51
|
+
# waiting to be acknowledged, and a write that went out as the adapter vanished
|
|
52
|
+
# is exactly that, so Linux is also told to give up on unacknowledged data after
|
|
53
|
+
# UNACKNOWLEDGED_TIMEOUT_SECONDS rather than its own quarter of an hour. Windows
|
|
54
|
+
# already gives up on a retransmission within a minute, and Python exposes no
|
|
55
|
+
# equivalent of that option there.
|
|
56
|
+
KEEPALIVE_IDLE_SECONDS = 30
|
|
57
|
+
KEEPALIVE_INTERVAL_SECONDS = 10
|
|
58
|
+
KEEPALIVE_PROBES = 3
|
|
59
|
+
UNACKNOWLEDGED_TIMEOUT_SECONDS = 60
|
|
60
|
+
|
|
38
61
|
|
|
39
62
|
class SerialTransport:
|
|
40
63
|
"""A held-open pyserial link with capped exponential backoff on failure."""
|
|
@@ -186,6 +209,8 @@ class SerialTransport:
|
|
|
186
209
|
self._port = None
|
|
187
210
|
raise TransportError("could not open %s: %s" % (self._url, err)) from err
|
|
188
211
|
|
|
212
|
+
_notice_a_dead_peer(self._port, self._url)
|
|
213
|
+
|
|
189
214
|
if self._failures:
|
|
190
215
|
logger.info("link to %s is back after %d failed attempts", self._url, self._failures)
|
|
191
216
|
else:
|
|
@@ -245,3 +270,41 @@ class SerialTransport:
|
|
|
245
270
|
# A port that will not close cleanly is still a port we are done
|
|
246
271
|
# with, and there is nothing useful for a caller to do about it.
|
|
247
272
|
logger.debug("ignoring error while closing %s", self._url, exc_info=True)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _notice_a_dead_peer(port: object, url: str) -> None:
|
|
276
|
+
"""Tell a network link's socket to find out when the other end has gone.
|
|
277
|
+
|
|
278
|
+
Only ``socket://`` and ``rfc2217://`` links have a socket, and pyserial
|
|
279
|
+
keeps it on a private attribute, so it is looked for rather than assumed: a
|
|
280
|
+
cable and ``loop://`` have none and are left alone. A test pins the
|
|
281
|
+
attribute against the pyserial this is installed with, so that an upgrade
|
|
282
|
+
renaming it fails the suite rather than quietly losing this.
|
|
283
|
+
|
|
284
|
+
Each option is set on its own and a refusal is logged and passed over, so
|
|
285
|
+
that a platform without one of them still opens the link with the rest. An
|
|
286
|
+
option this platform does not define at all is skipped in the same way.
|
|
287
|
+
"""
|
|
288
|
+
sock = getattr(port, "_socket", None)
|
|
289
|
+
if not isinstance(sock, socket.socket):
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# macOS spells the idle time TCP_KEEPALIVE; everywhere else it is
|
|
293
|
+
# TCP_KEEPIDLE. Each is looked up by name because not every platform
|
|
294
|
+
# defines every one of them.
|
|
295
|
+
idle_option = "TCP_KEEPIDLE" if hasattr(socket, "TCP_KEEPIDLE") else "TCP_KEEPALIVE"
|
|
296
|
+
options = [
|
|
297
|
+
(socket.SOL_SOCKET, "SO_KEEPALIVE", 1),
|
|
298
|
+
(socket.IPPROTO_TCP, idle_option, KEEPALIVE_IDLE_SECONDS),
|
|
299
|
+
(socket.IPPROTO_TCP, "TCP_KEEPINTVL", KEEPALIVE_INTERVAL_SECONDS),
|
|
300
|
+
(socket.IPPROTO_TCP, "TCP_KEEPCNT", KEEPALIVE_PROBES),
|
|
301
|
+
(socket.IPPROTO_TCP, "TCP_USER_TIMEOUT", UNACKNOWLEDGED_TIMEOUT_SECONDS * 1000),
|
|
302
|
+
]
|
|
303
|
+
for level, name, value in options:
|
|
304
|
+
option = getattr(socket, name, None)
|
|
305
|
+
if option is None:
|
|
306
|
+
continue
|
|
307
|
+
try:
|
|
308
|
+
sock.setsockopt(level, option, value)
|
|
309
|
+
except OSError as err:
|
|
310
|
+
logger.debug("could not set %s on the link to %s: %s", name, url, err)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,7 +34,7 @@ Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
|
34
34
|
Requires-Dist: pyserial<4,>=3.5
|
|
35
35
|
Requires-Dist: tzdata>=2026.3; sys_platform == "win32"
|
|
36
36
|
Provides-Extra: dev
|
|
37
|
-
Requires-Dist: pytest<
|
|
37
|
+
Requires-Dist: pytest<9.2,>=9.1.1; extra == "dev"
|
|
38
38
|
Requires-Dist: pytest-asyncio<2,>=1.4.0; extra == "dev"
|
|
39
39
|
Requires-Dist: httpx2<3,>=2.10.0; extra == "dev"
|
|
40
40
|
Requires-Dist: anyio<4.15,>=4.14.2; extra == "dev"
|
|
@@ -184,6 +184,22 @@ sign shows nothing or shows rubbish, and the adapter answers on port 23, it is
|
|
|
184
184
|
probably negotiating telnet rather than passing bytes through, and `rfc2217://`
|
|
185
185
|
is the form that speaks that.
|
|
186
186
|
|
|
187
|
+
An adapter that drops off the network says nothing as it goes. The service asks
|
|
188
|
+
the operating system to give a network link up about a minute after the adapter
|
|
189
|
+
stops answering: on Linux, a minute after the first write it did not
|
|
190
|
+
acknowledge, or, if nothing was written, about a minute after it last answered,
|
|
191
|
+
found by keepalive probes. A platform that does not take one of these options
|
|
192
|
+
keeps its own default for it, which can be much longer. The service itself only
|
|
193
|
+
finds out at its first read or write after the link has gone, and `GET /health`
|
|
194
|
+
shows `"connected": false` from then on. A sign with a variable updated every
|
|
195
|
+
minute is written to that often. One left alone is written to at the refresh,
|
|
196
|
+
every `refresh_interval_seconds`, which is 15 minutes by default, and a refresh
|
|
197
|
+
that lands before the link has gone is buffered like any other write, so on a
|
|
198
|
+
quiet sign `link.connected` can stay true for a refresh interval and a minute
|
|
199
|
+
after the adapter has gone. It is still worth a monitor watching, and needs no
|
|
200
|
+
key, but a ping of the adapter answers within a minute however quiet the sign
|
|
201
|
+
is, and is the better alarm.
|
|
202
|
+
|
|
187
203
|
### The API key, and config.local.toml
|
|
188
204
|
|
|
189
205
|
The key is not an argument. A launch configuration is a tracked file and a
|
|
@@ -11,10 +11,47 @@ import threading
|
|
|
11
11
|
import time
|
|
12
12
|
|
|
13
13
|
import pytest
|
|
14
|
+
import serial
|
|
15
|
+
from serial.rfc2217 import PortManager
|
|
14
16
|
|
|
15
17
|
from readerboard.transport.base import Transport, TransportError
|
|
16
18
|
from readerboard.transport.fake import FakeTransport
|
|
17
|
-
from readerboard.transport.serial_link import
|
|
19
|
+
from readerboard.transport.serial_link import (
|
|
20
|
+
KEEPALIVE_IDLE_SECONDS,
|
|
21
|
+
KEEPALIVE_INTERVAL_SECONDS,
|
|
22
|
+
KEEPALIVE_PROBES,
|
|
23
|
+
READ_LIMIT_BYTES,
|
|
24
|
+
UNACKNOWLEDGED_TIMEOUT_SECONDS,
|
|
25
|
+
SerialTransport,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def assert_told_to_notice_a_dead_peer(transport: SerialTransport) -> None:
|
|
30
|
+
"""Check that an open network link's socket has every option it was given.
|
|
31
|
+
|
|
32
|
+
The socket is a private attribute of pyserial's. The first assertion is the
|
|
33
|
+
one that fails if an upgrade renames it, rather than the link quietly opening
|
|
34
|
+
without any of what follows. An option this platform does not define is not
|
|
35
|
+
set, so it is not checked either.
|
|
36
|
+
"""
|
|
37
|
+
sock = transport._port._socket
|
|
38
|
+
assert isinstance(sock, socket.socket)
|
|
39
|
+
|
|
40
|
+
assert sock.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) != 0
|
|
41
|
+
# The same choice of name the code makes: macOS calls the idle time
|
|
42
|
+
# TCP_KEEPALIVE, and checking only TCP_KEEPIDLE would check nothing there.
|
|
43
|
+
idle_option = "TCP_KEEPIDLE" if hasattr(socket, "TCP_KEEPIDLE") else "TCP_KEEPALIVE"
|
|
44
|
+
expected = {
|
|
45
|
+
idle_option: KEEPALIVE_IDLE_SECONDS,
|
|
46
|
+
"TCP_KEEPINTVL": KEEPALIVE_INTERVAL_SECONDS,
|
|
47
|
+
"TCP_KEEPCNT": KEEPALIVE_PROBES,
|
|
48
|
+
"TCP_USER_TIMEOUT": UNACKNOWLEDGED_TIMEOUT_SECONDS * 1000,
|
|
49
|
+
}
|
|
50
|
+
for name, value in expected.items():
|
|
51
|
+
option = getattr(socket, name, None)
|
|
52
|
+
if option is None:
|
|
53
|
+
continue
|
|
54
|
+
assert sock.getsockopt(socket.IPPROTO_TCP, option) == value, name
|
|
18
55
|
|
|
19
56
|
|
|
20
57
|
class Clock:
|
|
@@ -150,6 +187,69 @@ class TestSerialTransportOverASocket:
|
|
|
150
187
|
listener.close()
|
|
151
188
|
server.join(5)
|
|
152
189
|
|
|
190
|
+
def test_the_link_is_told_to_notice_an_adapter_that_has_gone(self):
|
|
191
|
+
# An adapter that drops off the network says nothing as it goes. These
|
|
192
|
+
# options are what make the operating system find out, and the link
|
|
193
|
+
# stays reported as up until it does.
|
|
194
|
+
listener = socket.socket()
|
|
195
|
+
listener.bind(("127.0.0.1", 0))
|
|
196
|
+
listener.listen(1)
|
|
197
|
+
transport = SerialTransport("socket://127.0.0.1:%d" % listener.getsockname()[1])
|
|
198
|
+
try:
|
|
199
|
+
transport.ensure_open()
|
|
200
|
+
assert_told_to_notice_a_dead_peer(transport)
|
|
201
|
+
finally:
|
|
202
|
+
transport.close()
|
|
203
|
+
listener.close()
|
|
204
|
+
|
|
205
|
+
# pyserial's RFC 2217 client starts its reader thread with the deprecated
|
|
206
|
+
# setDaemon and setName. They are not ours to fix, and filterwarnings =
|
|
207
|
+
# ["error"] would otherwise fail the open before anything here is checked.
|
|
208
|
+
@pytest.mark.filterwarnings(
|
|
209
|
+
"ignore:set(Daemon|Name)\\(\\) is deprecated:DeprecationWarning"
|
|
210
|
+
)
|
|
211
|
+
def test_an_rfc2217_link_is_told_the_same(self):
|
|
212
|
+
# pyserial's RFC 2217 handler is a separate class from its socket one
|
|
213
|
+
# and keeps its own socket, so it is pinned separately. It will not
|
|
214
|
+
# open against a listener that only accepts: it negotiates first, so
|
|
215
|
+
# the other end is pyserial's own RFC 2217 server, driving a loop://
|
|
216
|
+
# port that stands in for the serial side of an adapter.
|
|
217
|
+
listener = socket.socket()
|
|
218
|
+
listener.bind(("127.0.0.1", 0))
|
|
219
|
+
listener.listen(1)
|
|
220
|
+
done = threading.Event()
|
|
221
|
+
|
|
222
|
+
def serve() -> None:
|
|
223
|
+
conn, _ = listener.accept()
|
|
224
|
+
with conn, serial.serial_for_url("loop://") as port:
|
|
225
|
+
conn.settimeout(0.1)
|
|
226
|
+
|
|
227
|
+
class Connection:
|
|
228
|
+
def write(self, data: bytes) -> None:
|
|
229
|
+
conn.sendall(data)
|
|
230
|
+
|
|
231
|
+
manager = PortManager(port, Connection())
|
|
232
|
+
while not done.is_set():
|
|
233
|
+
try:
|
|
234
|
+
data = conn.recv(1024)
|
|
235
|
+
except TimeoutError:
|
|
236
|
+
continue
|
|
237
|
+
if not data:
|
|
238
|
+
return
|
|
239
|
+
port.write(b"".join(manager.filter(data)))
|
|
240
|
+
|
|
241
|
+
server = threading.Thread(target=serve, daemon=True)
|
|
242
|
+
server.start()
|
|
243
|
+
transport = SerialTransport("rfc2217://127.0.0.1:%d" % listener.getsockname()[1])
|
|
244
|
+
try:
|
|
245
|
+
transport.ensure_open()
|
|
246
|
+
assert_told_to_notice_a_dead_peer(transport)
|
|
247
|
+
finally:
|
|
248
|
+
done.set()
|
|
249
|
+
transport.close()
|
|
250
|
+
listener.close()
|
|
251
|
+
server.join(5)
|
|
252
|
+
|
|
153
253
|
def test_a_link_that_never_stops_delivering_still_hands_back(self):
|
|
154
254
|
# Without the cap this read would never return, and the controller
|
|
155
255
|
# would never reach its deadline.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|