diff -r e64a82371d72 Doc/library/pty.rst --- a/Doc/library/pty.rst Fri Dec 23 22:13:29 2016 +0200 +++ b/Doc/library/pty.rst Sat Dec 24 14:49:36 2016 +0100 @@ -41,9 +41,13 @@ .. function:: spawn(argv[, master_read[, stdin_read]]) - Spawn a process, and connect its controlling terminal with the current - process's standard io. This is often used to baffle programs which insist on - reading from the controlling terminal. + Spawn a slave process, and connect its controlling master terminal with the + current process's standard io. This is often used to baffle programs which + insist on reading from the controlling terminal. + + A loop copies STDIN of the master to the slave and data received from the + slave to STDOUT. It is not signaled to the slave if STDIN of the master + closes down. The functions *master_read* and *stdin_read* should be functions which read from a file descriptor. The defaults try to read 1024 bytes each time they are @@ -91,3 +95,14 @@ script.write(('Script done on %s\n' % time.asctime()).encode()) print('Script done, file is', filename) + +Caveats +------ + +.. sectionauthor:: Cornelius Diekmann + +Be aware that processes started by :func:`spawn` do not receive any information +about STDIN of their master shutting down. For example, if run on a terminal on +a Linux system, ``/bin/sh < /dev/null`` closes immediately. However, +``./python -c "import pty; pty.spawn(\"/bin/sh\")" < /dev/null`` does not close +because the spawned slave shell is not notified that STDIN is closed. diff -r e64a82371d72 Lib/test/test_pty.py --- a/Lib/test/test_pty.py Fri Dec 23 22:13:29 2016 +0200 +++ b/Lib/test/test_pty.py Sat Dec 24 14:49:36 2016 +0100 @@ -198,6 +198,17 @@ # pty.fork() passed. +class _MockSelectOnNoWaitablesError(Exception): + """Raised when _mock_select is called with three empty lists as file + descriptors to wait on. Behavior of real select is platform-dependent and + likely infinite blocking on Linux.""" + pass + +class _MockSelectEternalWait(Exception): + """Used both as exception and placeholder value. Models that select will + block indefinitely.""" + pass + class SmallPtyTests(unittest.TestCase): """These tests don't spawn children or hang.""" @@ -236,9 +247,41 @@ return socketpair def _mock_select(self, rfds, wfds, xfds): + """Simulates the behavior of select.select. Only implemented for reader + waiting list (first parameter).""" + assert wfds == [] and xfds == [] # This will raise IndexError when no more expected calls exist. self.assertEqual(self.select_rfds_lengths.pop(0), len(rfds)) - return self.select_rfds_results.pop(0), [], [] + if len(rfds) == 0: + raise _MockSelectOnNoWaitablesError + rfds_result = self.select_rfds_results.pop(0) + # Placeholder to model eternal blocking + if rfds_result is _MockSelectEternalWait: + raise _MockSelectEternalWait + return rfds_result, [], [] + + def test__mock_select(self): + """Test the select proxy of the test class. Such meta testing.""" + self.select_rfds_lengths.append(0) + with self.assertRaises(_MockSelectOnNoWaitablesError): + self._mock_select([], [], []) + + # Prepare two select calls. Second one will block forever. + self.select_rfds_lengths.append(3) + self.select_rfds_results.append("foo") + self.select_rfds_lengths.append(3) + self.select_rfds_results.append(_MockSelectEternalWait) + + # Call one + self.assertEquals(self._mock_select([1, 2, 3], [], []), + ("foo", [], [])) + + # Call two + with self.assertRaises(_MockSelectEternalWait): + self._mock_select([1, 2, 3], [], []) + + assert self.select_rfds_lengths == [], "select_rfds_lengths is cleaned" + assert self.select_rfds_results == [], "select_rfds_results is cleaned" def test__copy_to_each(self): """Test the normal data case on both master_fd and stdin.""" @@ -268,8 +311,8 @@ self.assertEqual(os.read(read_from_stdout_fd, 20), b'from master') self.assertEqual(os.read(masters[1], 20), b'from stdin') - def test__copy_eof_on_all(self): - """Test the empty read EOF case on both master_fd and stdin.""" + def __copy_eof_helper(self, close_stdin, close_master): + """Helper to test the empty read EOF case on master_fd and/or stdin.""" read_from_stdout_fd, mock_stdout_fd = self._pipe() pty.STDOUT_FILENO = mock_stdout_fd mock_stdin_fd, write_to_stdin_fd = self._pipe() @@ -277,20 +320,80 @@ socketpair = self._socketpair() masters = [s.fileno() for s in socketpair] - socketpair[1].close() - os.close(write_to_stdin_fd) + # close fds or fill with dummy data in order to prevent blocking on + # one read call + if close_master: + socketpair[1].close() + else: + os.write(masters[1], b'from master') + if close_stdin: + os.close(write_to_stdin_fd) + else: + os.write(write_to_stdin_fd, b'from stdin') - # Expect two select calls, the last one will cause IndexError + # Expect two select calls. The first one returns master_fd and STDIN. + # The second one will cause: + # _MockSelectOnNoWaitablesError if both fds are closed. + # BrokenPipeError if only master_fd is closed. + # _MockSelectEternalWait otherwise. pty.select = self._mock_select self.select_rfds_lengths.append(2) self.select_rfds_results.append([mock_stdin_fd, masters[0]]) - # We expect that both fds were removed from the fds list as they - # both encountered an EOF before the second select call. - self.select_rfds_lengths.append(0) + # We expect that all closed fds were removed from the fds list as all + # closed fds encountered an EOF before the second call. + remaining_fds_after_first_select = sum(0 if closed else 1 for + closed in [close_master, close_stdin]) + self.select_rfds_lengths.append(remaining_fds_after_first_select) + self.select_rfds_results.append(_MockSelectEternalWait) + if close_master and close_stdin: + with self.assertRaises(_MockSelectOnNoWaitablesError): + pty._copy(masters[0]) + #clean, result is never consumed + self.assertEquals(self.select_rfds_results.pop(0), + _MockSelectEternalWait) + elif close_master and not close_stdin: + with self.assertRaises(BrokenPipeError): + # We expect to find a closed master but some data in STDIN. + # The _copy loop will try to write the data from STDIN to + # master, which is a broken pipe. + # + # Only closed master and not closed STDIN is a problem: + # closed STDIN but not closed master is not a broken pipe. + # In this case, we just read from master and write to STDOUT + pty._copy(masters[0]) + #clean, we exited after first select call + self.assertEquals(self.select_rfds_lengths.pop(0), 1) + self.assertEquals(self.select_rfds_results.pop(0), + _MockSelectEternalWait) + + else: + with self.assertRaises(_MockSelectEternalWait): + pty._copy(masters[0]) + + # We expect that everything is consumed + self.assertEquals(self.select_rfds_results, []) + self.assertEquals(self.select_rfds_lengths, []) + + # We expect that calling again raises an error with self.assertRaises(IndexError): pty._copy(masters[0]) + def test__copy_eof_on_all(self): + """Test the empty read EOF case on both master_fd and stdin.""" + self.__copy_eof_helper(close_stdin=True, close_master=True) + + def test__copy_eof_on_stdin(self): + """Test the empty read EOF case on only stdin.""" + self.__copy_eof_helper(close_stdin=True, close_master=False) + + def test__copy_eof_on_master(self): + """Test the empty read EOF case on only master_fd.""" + self.__copy_eof_helper(close_stdin=False, close_master=True) + + def test__copy_eof_on_helper(self): + """Test that _copy will block forever in this setting.""" + self.__copy_eof_helper(close_stdin=False, close_master=False) def tearDownModule(): reap_children()