Repository: byllyfish/shellous (language: python)

Issue:
test_quiet_pty failing on FreeBSD
The test is marked xfail for now.

On FreeBSD only, running a command that produces no output in a pty will hang. (There's an issue detecting the output stream closure) If the process produces any output at all, there is no issue.
test_quiet_pty failing on FreeBSD
The test is marked xfail for now.

On FreeBSD only, running a command that produces no output in a pty will hang. (There's an issue detecting the output stream closure) If the process produces any output at all, there is no issue.


Discussion / hints:

Fix patch:
diff --git a/shellous/pty_util.py b/shellous/pty_util.py
index 2c1c7c5c..ab22460f 100644
--- a/shellous/pty_util.py
+++ b/shellous/pty_util.py
@@ -19,7 +19,7 @@
     pass
 
 from .log import LOG_DETAIL, LOGGER, log_method
-from .util import BSD_DERIVED, close_fds
+from .util import BSD_DERIVED, BSD_FREEBSD, close_fds
 
 _STDOUT_FILENO = 1
 _LFLAG = 3
@@ -105,22 +105,38 @@ class PtyStreamReaderProtocol(asyncio.StreamReaderProtocol):
     "Custom subclass of StreamReaderProtocol for pty's."
 
     _pty_child_fd: Optional[ChildFd] = None
+    _pty_timer: Optional[asyncio.TimerHandle] = None  # Issue #378
 
     def connection_lost(self, exc: Optional[Exception]):
         "Intercept EIO error and treat it as EOF."
         if LOG_DETAIL:
             LOGGER.info("PtyStreamReaderProtocol.connection_lost ex=%r", exc)
+        if BSD_DERIVED and self._pty_timer is not None:
+            self._pty_timer.cancel()
         if isinstance(exc, OSError) and exc.errno == errno.EIO:
             exc = None
         super().connection_lost(exc)
 
     if BSD_DERIVED:
         # BSD only: On Linux, use the default behavior.
-        def data_received(self, data: bytes):
-            "Close child_fd when first data received."
+
+        def _close_child_fd(self):
             if self._pty_child_fd:
                 self._pty_child_fd.close()
                 self._pty_child_fd = None
+
+        if BSD_FREEBSD:
+            # Timer is only necessary on FreeBSD, not MacOS.
+            def connection_made(self, transport):
+                # Set up timer to close child fd when no data is received.
+                super().connection_made(transport)
+                if LOG_DETAIL:
+                    LOGGER.info("PtyStreamReaderProtocol.connection_made")
+                self._pty_timer = self._loop.call_later(2.0, self._close_child_fd)
+
+        def data_received(self, data: bytes):
+            "Close child_fd when first data received."
+            self._close_child_fd()
             return super().data_received(data)
 
     def eof_received(self):
diff --git a/shellous/util.py b/shellous/util.py
index 3b3c2312..5fa93bea 100644
--- a/shellous/util.py
+++ b/shellous/util.py
@@ -27,7 +27,8 @@
 )
 
 # True if OS is derived from BSD.
-BSD_DERIVED = sys.platform.startswith("freebsd") or sys.platform == "darwin"
+BSD_FREEBSD = sys.platform.startswith("freebsd")
+BSD_DERIVED = BSD_FREEBSD or sys.platform == "darwin"
 
 
 def decode(data: Optional[bytes], encoding: str) -> str:


Test patch:
diff --git a/tests/unix/test_unix.py b/tests/unix/test_unix.py
index fbda2183..6e742ec2 100644
--- a/tests/unix/test_unix.py
+++ b/tests/unix/test_unix.py
@@ -1723,8 +1723,7 @@ async def test_context_manager_running():
 @pytest.mark.xfail(_is_uvloop(), reason="uvloop")
 async def test_context_manager_running_pty():
     "Test context manager in pty mode may NOT update running status of process."
-
-    async with sh("sleep", 3).stdout(sh.CAPTURE).set(pty=True) as sleep1:
+    async with sh("sleep", 3).set(pty=True) as sleep1:
         await asyncio.sleep(0.1)
         sleep1.cancel()
         await asyncio.sleep(0.1)
@@ -1738,9 +1737,6 @@ async def test_context_manager_running_pty():
 
 @pytest.mark.xfail(_is_uvloop(), reason="uvloop")
 async def test_quiet_pty():
-    "Test pty mode with command that doesn't produce any output."
-    if sys.platform.startswith("freebsd"):
-        pytest.xfail("Known issue on FreeBSD. To be fixed...")  # FIXME
-
+    "Test pty mode with command that doesn't produce any output (#378)."
     result = await sh("sleep", 1).set(pty=True)
     assert result == ""