1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 # See the COPYING file for license information.
17 # Copyright (c) 2006 Guillaume Chazarain <guichaz@gmail.com>
22 import readline
# Just to say we want to use it with raw_input
29 from threading
import Thread
, Event
, Lock
31 from polysh
import dispatchers
, remote_dispatcher
32 from polysh
.console
import console_output
, set_last_status_length
33 from polysh
import completion
35 class input_buffer(object):
36 """The shared input buffer between the main thread and the stdin thread"""
42 """Add data to the buffer"""
50 """Get the content of the buffer"""
60 def process_input_buffer():
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('> ' + data
)
67 if data
.startswith(':'):
68 handle_control_command(data
[1:-1])
71 if data
.startswith('!'):
73 retcode
= subprocess
.call(data
[1:], shell
=True)
75 if e
.errno
== errno
.EINTR
:
76 console_output('Child was interrupted\n')
80 if retcode
> 128 and retcode
<= 192:
81 retcode
= 128 - retcode
83 console_output('Child returned %d\n' % retcode
)
85 console_output('Child was terminated by signal %d\n' % -retcode
)
88 for r
in dispatchers
.all_instances():
90 r
.dispatch_command(data
)
91 except asyncore
.ExitNow
, e
:
93 except Exception, msg
:
94 console_output('%s for %s, disconnecting\n' % (msg
, r
.display_name
))
97 if r
.enabled
and r
.state
is remote_dispatcher
.STATE_IDLE
:
98 r
.change_state(remote_dispatcher
.STATE_RUNNING
)
100 # The stdin thread uses a synchronous (with ACK) socket to communicate with the
101 # main thread, which is most of the time waiting in the poll() loop.
102 # Socket character protocol:
103 # d: there is new data to send
104 # A: ACK, same reply for every message, communications are synchronous, so the
105 # stdin thread sends a character to the socket, the main thread processes it,
106 # sends the ACK, and the stdin thread can go on.
108 class socket_notification_reader(asyncore
.dispatcher
):
109 """The socket reader in the main thread"""
111 asyncore
.dispatcher
.__init
__(self
, the_stdin_thread
.socket_read
)
115 process_input_buffer()
117 raise Exception, 'Unknown code: %s' % (c
)
119 def handle_read(self
):
120 """Handle all the available character commands in the socket"""
124 except socket
.error
, why
:
125 if why
[0] == errno
.EWOULDBLOCK
:
131 self
.socket
.setblocking(True)
133 self
.socket
.setblocking(False)
136 """Our writes are blocking"""
139 def write_main_socket(c
):
140 """Synchronous write to the main socket, wait for ACK"""
141 the_stdin_thread
.socket_write
.send(c
)
144 the_stdin_thread
.socket_write
.recv(1)
145 except socket
.error
, e
:
146 if e
[0] != errno
.EINTR
:
152 # This file descriptor is used to interrupt readline in raw_input().
153 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
154 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
156 tempfile_fd
, tempfile_name
= tempfile
.mkstemp()
157 os
.remove(tempfile_name
)
158 os
.write(tempfile_fd
, chr(3))
160 def get_stdin_pid(cached_result
=None):
161 """Try to get the PID of the stdin thread, otherwise get the whole process
163 if cached_result
is None:
165 tasks
= os
.listdir('/proc/self/task')
167 if e
.errno
!= errno
.ENOENT
:
169 cached_result
= os
.getpid()
171 tasks
.remove(str(os
.getpid()))
172 assert len(tasks
) == 1
173 cached_result
= int(tasks
[0])
176 def interrupt_stdin_thread():
177 """The stdin thread may be in raw_input(), get out of it"""
178 dupped_stdin
= os
.dup(0) # Backup the stdin fd
179 assert not the_stdin_thread
.interrupt_asked
# Sanity check
180 the_stdin_thread
.interrupt_asked
= True # Not user triggered
181 os
.lseek(tempfile_fd
, 0, 0) # Rewind in the temp file
182 os
.dup2(tempfile_fd
, 0) # This will make raw_input() return
183 pid
= get_stdin_pid()
184 os
.kill(pid
, signal
.SIGWINCH
) # Try harder to wake up raw_input()
185 the_stdin_thread
.out_of_raw_input
.wait() # Wait for this return
186 the_stdin_thread
.interrupt_asked
= False # Restore sanity
187 os
.dup2(dupped_stdin
, 0) # Restore stdin
188 os
.close(dupped_stdin
) # Cleanup
193 if echo
!= echo_enabled
:
194 fd
= sys
.stdin
.fileno()
195 attr
= termios
.tcgetattr(fd
)
197 attr
[3] |
= termios
.ECHO
199 attr
[3] &= ~termios
.ECHO
200 termios
.tcsetattr(fd
, termios
.TCSANOW
, attr
)
203 class stdin_thread(Thread
):
204 """The stdin thread, used to call raw_input()"""
206 Thread
.__init
__(self
, name
='stdin thread')
207 completion
.install_completion_handler()
210 def activate(interactive
):
211 """Activate the thread at initialization time"""
212 the_stdin_thread
.input_buffer
= input_buffer()
214 the_stdin_thread
.raw_input_wanted
= Event()
215 the_stdin_thread
.in_raw_input
= Event()
216 the_stdin_thread
.out_of_raw_input
= Event()
217 the_stdin_thread
.out_of_raw_input
.set()
218 s1
, s2
= socket
.socketpair()
219 the_stdin_thread
.socket_read
, the_stdin_thread
.socket_write
= s1
, s2
220 the_stdin_thread
.interrupt_asked
= False
221 the_stdin_thread
.setDaemon(True)
222 the_stdin_thread
.start()
223 the_stdin_thread
.socket_notification
= socket_notification_reader()
224 the_stdin_thread
.prepend_text
= None
225 readline
.set_pre_input_hook(the_stdin_thread
.prepend_previous_text
)
227 def prepend_previous_text(self
):
228 if self
.prepend_text
:
229 readline
.insert_text(self
.prepend_text
)
231 self
.prepend_text
= None
233 def want_raw_input(self
):
234 nr
, total
= dispatchers
.count_awaited_processes()
236 prompt
= 'waiting (%d/%d)> ' % (nr
, total
)
238 prompt
= 'ready (%d)> ' % total
240 set_last_status_length(len(prompt
))
241 self
.raw_input_wanted
.set()
242 while not self
.in_raw_input
.isSet():
243 self
.socket_notification
.handle_read()
244 self
.in_raw_input
.wait(0.1)
245 self
.raw_input_wanted
.clear()
247 def no_raw_input(self
):
248 if not self
.out_of_raw_input
.isSet():
249 interrupt_stdin_thread()
254 self
.raw_input_wanted
.wait()
255 self
.out_of_raw_input
.set()
256 self
.in_raw_input
.set()
257 self
.out_of_raw_input
.clear()
260 cmd
= raw_input(self
.prompt
)
262 if self
.interrupt_asked
:
263 cmd
= readline
.get_line_buffer()
265 cmd
= chr(4) # Ctrl-D
266 if self
.interrupt_asked
:
267 self
.prepend_text
= cmd
269 self
.in_raw_input
.clear()
270 self
.out_of_raw_input
.set()
273 completion
.add_to_history(cmd
)
275 completion
.remove_last_history_item()
278 self
.input_buffer
.add(cmd
+ '\n')
279 write_main_socket('d')
281 the_stdin_thread
= stdin_thread()