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, 2007, 2008 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 gsh
import dispatchers
, remote_dispatcher
32 from gsh
.console
import console_output
, set_last_status_length
33 from gsh
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 ignore_sigchld(ignore
):
61 """Typically we don't want to create zombie. But when executing a user
62 command (!command) the subprocess module relies on zombies not being
63 automatically reclaimed"""
65 signal
.signal(signal
.SIGCHLD
, signal
.SIG_IGN
)
66 # Reclaim previously created zombies
68 while os
.waitpid(-1, os
.WNOHANG
) != (0, 0):
71 if e
.errno
!= errno
.ECHILD
:
74 signal
.signal(signal
.SIGCHLD
, signal
.SIG_DFL
)
76 def process_input_buffer():
77 """Send the content of the input buffer to all remote processes, this must
78 be called in the main thread"""
79 from gsh
.control_commands_helpers
import handle_control_command
80 data
= the_stdin_thread
.input_buffer
.get()
84 remote_dispatcher
.log('> ' + data
)
86 if data
.startswith(':'):
87 handle_control_command(data
[1:-1])
90 if data
.startswith('!'):
93 retcode
= subprocess
.call(data
[1:], shell
=True)
95 if e
.errno
== errno
.EINTR
:
96 console_output('Child was interrupted\n')
102 console_output('Child returned %d\n' % retcode
)
104 console_output('Child was terminated by signal %d\n' % -retcode
)
107 for r
in dispatchers
.all_instances():
109 r
.dispatch_command(data
)
110 except asyncore
.ExitNow
, e
:
112 except Exception, msg
:
113 console_output('%s for %s, disconnecting\n' % (msg
, r
.display_name
))
116 if r
.enabled
and r
.state
is remote_dispatcher
.STATE_IDLE
:
117 r
.change_state(remote_dispatcher
.STATE_RUNNING
)
119 # The stdin thread uses a synchronous (with ACK) socket to communicate with the
120 # main thread, which is most of the time waiting in the poll() loop.
121 # Socket character protocol:
122 # d: there is new data to send
123 # A: ACK, same reply for every message, communications are synchronous, so the
124 # stdin thread sends a character to the socket, the main thread processes it,
125 # sends the ACK, and the stdin thread can go on.
127 class socket_notification_reader(asyncore
.dispatcher
):
128 """The socket reader in the main thread"""
130 asyncore
.dispatcher
.__init
__(self
, the_stdin_thread
.socket_read
)
134 process_input_buffer()
136 raise Exception, 'Unknown code: %s' % (c
)
138 def handle_read(self
):
139 """Handle all the available character commands in the socket"""
143 except socket
.error
, why
:
144 if why
[0] == errno
.EWOULDBLOCK
:
150 self
.socket
.setblocking(True)
152 self
.socket
.setblocking(False)
155 """Our writes are blocking"""
158 def write_main_socket(c
):
159 """Synchronous write to the main socket, wait for ACK"""
160 the_stdin_thread
.socket_write
.send(c
)
163 the_stdin_thread
.socket_write
.recv(1)
164 except socket
.error
, e
:
165 assert e
[0] == errno
.EINTR
170 # This file descriptor is used to interrupt readline in raw_input().
171 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
172 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
174 tempfile_fd
, tempfile_name
= tempfile
.mkstemp()
175 os
.remove(tempfile_name
)
176 os
.write(tempfile_fd
, chr(3))
178 def get_stdin_pid(cached_result
=None):
179 """Try to get the PID of the stdin thread, otherwise get the whole process
181 if cached_result
is None:
183 tasks
= os
.listdir('/proc/self/task')
185 if e
.errno
!= errno
.ENOENT
:
187 cached_result
= os
.getpid()
189 tasks
.remove(str(os
.getpid()))
190 assert len(tasks
) == 1
191 cached_result
= int(tasks
[0])
194 def interrupt_stdin_thread():
195 """The stdin thread may be in raw_input(), get out of it"""
196 dupped_stdin
= os
.dup(0) # Backup the stdin fd
197 assert not the_stdin_thread
.interrupt_asked
# Sanity check
198 the_stdin_thread
.interrupt_asked
= True # Not user triggered
199 os
.lseek(tempfile_fd
, 0, 0) # Rewind in the temp file
200 os
.dup2(tempfile_fd
, 0) # This will make raw_input() return
201 pid
= get_stdin_pid()
202 os
.kill(pid
, signal
.SIGWINCH
) # Try harder to wake up raw_input()
203 the_stdin_thread
.out_of_raw_input
.wait() # Wait for this return
204 the_stdin_thread
.interrupt_asked
= False # Restore sanity
205 os
.dup2(dupped_stdin
, 0) # Restore stdin
206 os
.close(dupped_stdin
) # Cleanup
211 if echo
!= echo_enabled
:
212 fd
= sys
.stdin
.fileno()
213 attr
= termios
.tcgetattr(fd
)
215 attr
[3] |
= termios
.ECHO
217 attr
[3] &= ~termios
.ECHO
218 termios
.tcsetattr(fd
, termios
.TCSANOW
, attr
)
221 class stdin_thread(Thread
):
222 """The stdin thread, used to call raw_input()"""
224 Thread
.__init
__(self
, name
='stdin thread')
225 completion
.install_completion_handler()
228 def activate(interactive
):
229 """Activate the thread at initialization time"""
230 the_stdin_thread
.input_buffer
= input_buffer()
232 the_stdin_thread
.raw_input_wanted
= Event()
233 the_stdin_thread
.in_raw_input
= Event()
234 the_stdin_thread
.out_of_raw_input
= Event()
235 the_stdin_thread
.out_of_raw_input
.set()
236 s1
, s2
= socket
.socketpair()
237 the_stdin_thread
.socket_read
, the_stdin_thread
.socket_write
= s1
, s2
238 the_stdin_thread
.interrupt_asked
= False
239 the_stdin_thread
.setDaemon(True)
240 the_stdin_thread
.start()
241 the_stdin_thread
.socket_notification
= socket_notification_reader()
242 the_stdin_thread
.prepend_text
= None
243 readline
.set_pre_input_hook(the_stdin_thread
.prepend_previous_text
)
245 def prepend_previous_text(self
):
246 if self
.prepend_text
:
247 readline
.insert_text(self
.prepend_text
)
249 self
.prepend_text
= None
251 def want_raw_input(self
):
252 nr
, total
= dispatchers
.count_awaited_processes()
254 prompt
= 'waiting (%d/%d)> ' % (nr
, total
)
256 prompt
= 'ready (%d)> ' % total
258 set_last_status_length(len(prompt
))
259 self
.raw_input_wanted
.set()
260 self
.socket_notification
.handle_read()
261 self
.in_raw_input
.wait()
262 self
.raw_input_wanted
.clear()
264 def no_raw_input(self
):
265 if not self
.out_of_raw_input
.isSet():
266 interrupt_stdin_thread()
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()
277 cmd
= raw_input(self
.prompt
)
279 if self
.interrupt_asked
:
280 cmd
= readline
.get_line_buffer()
282 cmd
= chr(4) # Ctrl-D
283 if self
.interrupt_asked
:
284 self
.prepend_text
= cmd
286 self
.in_raw_input
.clear()
287 self
.out_of_raw_input
.set()
290 completion
.add_to_history(cmd
)
292 completion
.remove_last_history_item()
295 self
.input_buffer
.add(cmd
+ '\n')
296 write_main_socket('d')
298 the_stdin_thread
= stdin_thread()