"Fossies" - the Fresh Open Source Software Archive 
Member "polysh-polysh-0.13/polysh/stdin.py" (11 May 2020, 10076 Bytes) of package /linux/privat/polysh-polysh-0.13.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "stdin.py" see the
Fossies "Dox" file reference documentation.
1 """Polysh - Standard Input Routines
2
3 Copyright (c) 2006 Guillaume Chazarain <guichaz@gmail.com>
4 Copyright (c) 2018 InnoGames GmbH
5 """
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import asyncore
20 import errno
21 import os
22 import readline # Just to say we want to use it with raw_input
23 import signal
24 import socket
25 import subprocess
26 import sys
27 import tempfile
28 import termios
29 from threading import Thread, Event, Lock
30
31 from polysh import dispatchers, remote_dispatcher
32 from polysh.console import console_output, set_last_status_length
33 from polysh import completion
34 from typing import Optional
35
36 the_stdin_thread = None # type: StdinThread
37
38
39 class InputBuffer(object):
40 """The shared input buffer between the main thread and the stdin thread"""
41
42 def __init__(self) -> None:
43 self.lock = Lock()
44 self.buf = b''
45
46 def add(self, data: bytes) -> None:
47 """Add data to the buffer"""
48 with self.lock:
49 self.buf += data
50
51 def get(self) -> bytes:
52 """Get the content of the buffer"""
53 data = b''
54 with self.lock:
55 data, self.buf = self.buf, b''
56
57 return data
58
59
60 def process_input_buffer() -> None:
61 """Send the content of the input buffer to all remote processes, this must
62 be called in the main thread"""
63 from polysh.control_commands_helpers import handle_control_command
64 data = the_stdin_thread.input_buffer.get()
65 remote_dispatcher.log(b'> ' + data)
66
67 if data.startswith(b':'):
68 try:
69 handle_control_command(data[1:-1].decode())
70 except UnicodeDecodeError as e:
71 console_output(b'Could not decode command.')
72 return
73
74 if data.startswith(b'!'):
75 try:
76 retcode = subprocess.call(data[1:], shell=True)
77 except OSError as e:
78 if e.errno == errno.EINTR:
79 console_output(b'Child was interrupted\n')
80 retcode = 0
81 else:
82 raise
83 if retcode > 128 and retcode <= 192:
84 retcode = 128 - retcode
85 if retcode > 0:
86 console_output('Child returned {:d}\n'.format(retcode).encode())
87 elif retcode < 0:
88 console_output('Child was terminated by signal {:d}\n'.format(
89 -retcode).encode())
90 return
91
92 for r in dispatchers.all_instances():
93 try:
94 r.dispatch_command(data)
95 except asyncore.ExitNow as e:
96 raise e
97 except Exception as msg:
98 raise msg
99 console_output('{} for {}, disconnecting\n'.format(
100 str(msg), r.display_name).encode())
101 r.disconnect()
102 else:
103 if r.enabled and r.state is remote_dispatcher.STATE_IDLE:
104 r.change_state(remote_dispatcher.STATE_RUNNING)
105
106 # The stdin thread uses a synchronous (with ACK) socket to communicate with the
107 # main thread, which is most of the time waiting in the poll() loop.
108 # Socket character protocol:
109 # d: there is new data to send
110 # A: ACK, same reply for every message, communications are synchronous, so the
111 # stdin thread sends a character to the socket, the main thread processes it,
112 # sends the ACK, and the stdin thread can go on.
113
114
115 class SocketNotificationReader(asyncore.dispatcher):
116 """The socket reader in the main thread"""
117
118 def __init__(self, the_stdin_thread: 'StdinThread') -> None:
119 asyncore.dispatcher.__init__(self, the_stdin_thread.socket_read)
120
121 def _do(self, c: bytes) -> None:
122 if c == b'd':
123 process_input_buffer()
124 else:
125 raise Exception('Unknown code: %s' % (c))
126
127 def handle_read(self) -> None:
128 """Handle all the available character commands in the socket"""
129 while True:
130 try:
131 c = self.recv(1)
132 except socket.error as e:
133 if e.errno == errno.EWOULDBLOCK:
134 return
135 else:
136 raise
137 else:
138 self._do(c)
139 self.socket.setblocking(True)
140 self.send(b'A')
141 self.socket.setblocking(False)
142
143 def writable(self) -> bool:
144 """Our writes are blocking"""
145 return False
146
147
148 def write_main_socket(c: bytes) -> None:
149 """Synchronous write to the main socket, wait for ACK"""
150 the_stdin_thread.socket_write.send(c)
151 while True:
152 try:
153 the_stdin_thread.socket_write.recv(1)
154 except socket.error as e:
155 if e.errno != errno.EINTR:
156 raise
157 else:
158 break
159
160
161 #
162 # This file descriptor is used to interrupt readline in raw_input().
163 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
164 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
165 # a newline
166 tempfile_fd, tempfile_name = tempfile.mkstemp()
167 os.remove(tempfile_name)
168 os.write(tempfile_fd, b'\x03')
169
170
171 def get_stdin_pid(cached_result: Optional[int] = None) -> int:
172 """Try to get the PID of the stdin thread, otherwise get the whole process
173 ID"""
174 if cached_result is None:
175 try:
176 tasks = os.listdir('/proc/self/task')
177 except OSError as e:
178 if e.errno != errno.ENOENT:
179 raise
180 cached_result = os.getpid()
181 else:
182 tasks.remove(str(os.getpid()))
183 assert len(tasks) == 1
184 cached_result = int(tasks[0])
185 return cached_result
186
187
188 def interrupt_stdin_thread() -> None:
189 """The stdin thread may be in raw_input(), get out of it"""
190 dupped_stdin = os.dup(0) # Backup the stdin fd
191 assert not the_stdin_thread.interrupt_asked # Sanity check
192 the_stdin_thread.interrupt_asked = True # Not user triggered
193 os.lseek(tempfile_fd, 0, 0) # Rewind in the temp file
194 os.dup2(tempfile_fd, 0) # This will make raw_input() return
195 pid = get_stdin_pid()
196 os.kill(pid, signal.SIGWINCH) # Try harder to wake up raw_input()
197 the_stdin_thread.out_of_raw_input.wait() # Wait for this return
198 the_stdin_thread.interrupt_asked = False # Restore sanity
199 os.dup2(dupped_stdin, 0) # Restore stdin
200 os.close(dupped_stdin) # Cleanup
201
202
203 echo_enabled = True
204
205
206 def set_echo(echo: bool) -> None:
207 global echo_enabled
208 if echo != echo_enabled:
209 fd = sys.stdin.fileno()
210 attr = termios.tcgetattr(fd)
211 # The following raises a mypy warning, as python type hints don't allow
212 # per list item granularity. The last item in attr is List[bytes], but
213 # we don't access that here.
214 if echo:
215 attr[3] |= termios.ECHO # type: ignore
216 else:
217 attr[3] &= ~termios.ECHO # type: ignore
218 termios.tcsetattr(fd, termios.TCSANOW, attr)
219 echo_enabled = echo
220
221
222 class StdinThread(Thread):
223 """The stdin thread, used to call raw_input()"""
224
225 def __init__(self, interactive: bool) -> None:
226 Thread.__init__(self, name='stdin thread')
227 completion.install_completion_handler()
228 self.input_buffer = InputBuffer()
229
230 if interactive:
231 self.raw_input_wanted = Event()
232 self.in_raw_input = Event()
233 self.out_of_raw_input = Event()
234 self.out_of_raw_input.set()
235 s1, s2 = socket.socketpair()
236 self.socket_read, self.socket_write = s1, s2
237 self.interrupt_asked = False
238 self.setDaemon(True)
239 self.start()
240 self.socket_notification = SocketNotificationReader(self)
241 self.prepend_text = None # type: Optional[str]
242 readline.set_pre_input_hook(self.prepend_previous_text)
243
244 def prepend_previous_text(self) -> None:
245 if self.prepend_text:
246 readline.insert_text(self.prepend_text)
247 readline.redisplay()
248 self.prepend_text = None
249
250 def want_raw_input(self) -> None:
251 nr, total = dispatchers.count_awaited_processes()
252 if nr:
253 prompt = 'waiting (%d/%d)> ' % (nr, total)
254 else:
255 prompt = 'ready (%d)> ' % total
256 self.prompt = prompt
257 set_last_status_length(len(prompt))
258 self.raw_input_wanted.set()
259 while not self.in_raw_input.is_set():
260 self.socket_notification.handle_read()
261 self.in_raw_input.wait(0.1)
262 self.raw_input_wanted.clear()
263
264 def no_raw_input(self) -> None:
265 if not self.out_of_raw_input.is_set():
266 interrupt_stdin_thread()
267
268 # Beware of races
269 def run(self) -> None:
270 while True:
271 self.raw_input_wanted.wait()
272 self.out_of_raw_input.set()
273 self.in_raw_input.set()
274 self.out_of_raw_input.clear()
275 cmd = None
276 try:
277 cmd = input(self.prompt)
278 except EOFError:
279 if self.interrupt_asked:
280 cmd = readline.get_line_buffer()
281 else:
282 cmd = chr(4) # Ctrl-D
283 if self.interrupt_asked:
284 self.prepend_text = cmd
285 cmd = None
286 self.in_raw_input.clear()
287 self.out_of_raw_input.set()
288 if cmd:
289 if echo_enabled:
290 completion.add_to_history(cmd)
291 else:
292 completion.remove_last_history_item()
293 set_echo(True)
294 if cmd is not None:
295 self.input_buffer.add('{}\n'.format(cmd).encode())
296 write_main_socket(b'd')