|
11 | 11 | from meshtastic.stream_interface import StreamInterface |
12 | 12 |
|
13 | 13 | DEFAULT_TCP_PORT = 4403 |
| 14 | + |
| 15 | +# How long close() gives the device to consume what we last wrote before forcing |
| 16 | +# the connection down. See close() for why this exists. |
| 17 | +GRACEFUL_CLOSE_TIMEOUT = 0.25 |
| 18 | + |
14 | 19 | logger = logging.getLogger(__name__) |
15 | 20 |
|
16 | 21 |
|
@@ -80,13 +85,39 @@ def myConnect(self) -> None: |
80 | 85 | server_address = (self.hostname, self.portNumber) |
81 | 86 | self.socket = socket.create_connection(server_address) |
82 | 87 |
|
| 88 | + def _wait_for_reader_exit(self, timeout: float) -> None: |
| 89 | + """Wait briefly for the reader thread to drain and exit after a half-close. |
| 90 | +
|
| 91 | + Returns as soon as it exits, or after timeout: the device is not obliged |
| 92 | + to close just because we did. |
| 93 | + """ |
| 94 | + rx = getattr(self, "_rxThread", None) |
| 95 | + if rx is None or rx is threading.current_thread(): |
| 96 | + return |
| 97 | + with contextlib.suppress(Exception): |
| 98 | + rx.join(timeout) |
| 99 | + |
83 | 100 | def close(self) -> None: |
84 | 101 | """Close a connection to the device.""" |
85 | 102 | logger.debug("Closing TCP stream") |
86 | 103 | # Sometimes the socket read might be blocked in the reader thread. |
87 | 104 | # Therefore force a shutdown first to unblock reader thread reads. |
88 | 105 | self._wantExit = True |
89 | 106 | if self.socket is not None: |
| 107 | + # Half-close first. shutdown(SHUT_WR) sends FIN, which tells the |
| 108 | + # device we are done writing and lets it consume what we last wrote |
| 109 | + # before the connection goes away -- typically the admin message a |
| 110 | + # one-shot command such as `--set` just sent. |
| 111 | + # |
| 112 | + # Going straight to shutdown(SHUT_RDWR) + close() while either side |
| 113 | + # still has unread data makes the stack send RST instead. Winsock |
| 114 | + # then discards data the peer had already received but not yet read, |
| 115 | + # so the write is lost; Linux delivers it before reporting |
| 116 | + # ECONNRESET, which is why this only bites on Windows. |
| 117 | + with contextlib.suppress(Exception): |
| 118 | + self.socket.shutdown(socket.SHUT_WR) |
| 119 | + self._wait_for_reader_exit(GRACEFUL_CLOSE_TIMEOUT) |
| 120 | + |
90 | 121 | with contextlib.suppress( |
91 | 122 | Exception |
92 | 123 | ): # Ignore errors in shutdown, because we might have a race with the server |
|
0 commit comments