Fix a typo in a comment.
[portage.git] / pym / portage / util / _pty.py
1 # Copyright 2010-2011 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 import array
5 import fcntl
6 import platform
7 import pty
8 import select
9 import sys
10 import termios
11
12 from portage import os, _unicode_decode, _unicode_encode
13 from portage.output import get_term_size, set_term_size
14 from portage.process import spawn_bash
15 from portage.util import writemsg
16
17 def _can_test_pty_eof():
18         """
19         The _test_pty_eof() function seems to hang on most
20         kernels other than Linux.
21         This was reported for the following kernels which used to work fine
22         without this EOF test: Darwin, AIX, FreeBSD.  They seem to hang on
23         the slave_file.close() call.  Note that Python's implementation of
24         openpty on Solaris already caused random hangs without this EOF test
25         and hence is globally disabled.
26         @rtype: bool
27         @returns: True if _test_pty_eof() won't hang, False otherwise.
28         """
29         return platform.system() in ("Linux",)
30
31 def _test_pty_eof(fdopen_buffered=False):
32         """
33         Returns True if this issues is fixed for the currently
34         running version of python: http://bugs.python.org/issue5380
35         Raises an EnvironmentError from openpty() if it fails.
36
37         NOTE: This issue is only problematic when array.fromfile()
38         is used, rather than os.read(). However, array.fromfile()
39         is preferred since it is approximately 10% faster.
40
41         New development: It appears that array.fromfile() is usable
42         with python3 as long as fdopen is called with a bufsize
43         argument of 0.
44         """
45
46         use_fork = False
47
48         test_string = 2 * "blah blah blah\n"
49         test_string = _unicode_decode(test_string,
50                 encoding='utf_8', errors='strict')
51
52         # may raise EnvironmentError
53         master_fd, slave_fd = pty.openpty()
54
55         # Non-blocking mode is required for Darwin kernel.
56         fcntl.fcntl(master_fd, fcntl.F_SETFL,
57                 fcntl.fcntl(master_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
58
59         # Disable post-processing of output since otherwise weird
60         # things like \n -> \r\n transformations may occur.
61         mode = termios.tcgetattr(slave_fd)
62         mode[1] &= ~termios.OPOST
63         termios.tcsetattr(slave_fd, termios.TCSANOW, mode)
64
65         # Simulate a subprocess writing some data to the
66         # slave end of the pipe, and then exiting.
67         pid = None
68         if use_fork:
69                 pids = spawn_bash(_unicode_encode("echo -n '%s'" % test_string,
70                         encoding='utf_8', errors='strict'), env=os.environ,
71                         fd_pipes={0:sys.stdin.fileno(), 1:slave_fd, 2:slave_fd},
72                         returnpid=True)
73                 if isinstance(pids, int):
74                         os.close(master_fd)
75                         os.close(slave_fd)
76                         raise EnvironmentError('spawn failed')
77                 pid = pids[0]
78         else:
79                 os.write(slave_fd, _unicode_encode(test_string,
80                         encoding='utf_8', errors='strict'))
81         os.close(slave_fd)
82
83         # If using a fork, we must wait for the child here,
84         # in order to avoid a race condition that would
85         # lead to inconsistent results.
86         if pid is not None:
87                 os.waitpid(pid, 0)
88
89         if fdopen_buffered:
90                 master_file = os.fdopen(master_fd, 'rb')
91         else:
92                 master_file = os.fdopen(master_fd, 'rb', 0)
93         eof = False
94         data = []
95         iwtd = [master_file]
96         owtd = []
97         ewtd = []
98
99         while not eof:
100
101                 events = select.select(iwtd, owtd, ewtd)
102                 if not events[0]:
103                         eof = True
104                         break
105
106                 buf = array.array('B')
107                 try:
108                         buf.fromfile(master_file, 1024)
109                 except (EOFError, IOError):
110                         eof = True
111
112                 if not buf:
113                         eof = True
114                 else:
115                         data.append(_unicode_decode(buf.tostring(),
116                                 encoding='utf_8', errors='strict'))
117
118         master_file.close()
119
120         return test_string == ''.join(data)
121
122 # If _test_pty_eof() can't be used for runtime detection of
123 # http://bugs.python.org/issue5380, openpty can't safely be used
124 # unless we can guarantee that the current version of python has
125 # been fixed (affects all current versions of python3). When
126 # this issue is fixed in python3, we can add another sys.hexversion
127 # conditional to enable openpty support in the fixed versions.
128 if sys.hexversion >= 0x3000000 and not _can_test_pty_eof():
129         _disable_openpty = True
130 else:
131         # Disable the use of openpty on Solaris as it seems Python's openpty
132         # implementation doesn't play nice on Solaris with Portage's
133         # behaviour causing hangs/deadlocks.
134         # Additional note for the future: on Interix, pipes do NOT work, so
135         # _disable_openpty on Interix must *never* be True
136         _disable_openpty = platform.system() in ("SunOS",)
137 _tested_pty = False
138
139 if not _can_test_pty_eof():
140         # Skip _test_pty_eof() on systems where it hangs.
141         _tested_pty = True
142
143 _fbsd_test_pty = platform.system() == 'FreeBSD'
144
145 def _create_pty_or_pipe(copy_term_size=None):
146         """
147         Try to create a pty and if then fails then create a normal
148         pipe instead.
149
150         @param copy_term_size: If a tty file descriptor is given
151                 then the term size will be copied to the pty.
152         @type copy_term_size: int
153         @rtype: tuple
154         @returns: A tuple of (is_pty, master_fd, slave_fd) where
155                 is_pty is True if a pty was successfully allocated, and
156                 False if a normal pipe was allocated.
157         """
158
159         got_pty = False
160
161         global _disable_openpty, _fbsd_test_pty, _tested_pty
162         if not (_tested_pty or _disable_openpty):
163                 try:
164                         if not _test_pty_eof():
165                                 _disable_openpty = True
166                 except EnvironmentError as e:
167                         _disable_openpty = True
168                         writemsg("openpty failed: '%s'\n" % str(e),
169                                 noiselevel=-1)
170                         del e
171                 _tested_pty = True
172
173         if _fbsd_test_pty and not _disable_openpty:
174                 # Test for python openpty breakage after freebsd7 to freebsd8
175                 # upgrade, which results in a 'Function not implemented' error
176                 # and the process being killed.
177                 pid = os.fork()
178                 if pid == 0:
179                         pty.openpty()
180                         os._exit(os.EX_OK)
181                 pid, status = os.waitpid(pid, 0)
182                 if (status & 0xff) == 140:
183                         _disable_openpty = True
184                 _fbsd_test_pty = False
185
186         if _disable_openpty:
187                 master_fd, slave_fd = os.pipe()
188         else:
189                 try:
190                         master_fd, slave_fd = pty.openpty()
191                         got_pty = True
192                 except EnvironmentError as e:
193                         _disable_openpty = True
194                         writemsg("openpty failed: '%s'\n" % str(e),
195                                 noiselevel=-1)
196                         del e
197                         master_fd, slave_fd = os.pipe()
198
199         if got_pty:
200                 # Disable post-processing of output since otherwise weird
201                 # things like \n -> \r\n transformations may occur.
202                 mode = termios.tcgetattr(slave_fd)
203                 mode[1] &= ~termios.OPOST
204                 termios.tcsetattr(slave_fd, termios.TCSANOW, mode)
205
206         if got_pty and \
207                 copy_term_size is not None and \
208                 os.isatty(copy_term_size):
209                 rows, columns = get_term_size()
210                 set_term_size(rows, columns, slave_fd)
211
212         return (got_pty, master_fd, slave_fd)