setuptools-0.6c5-py2.4.egg is broken wrt RPM building so we provide a snapshot
[gsh.git] / gsh / control_shell.py
blob81a4f176b0e19a698de06c6d4789c489255afc89
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 Guillaume Chazarain <guichaz@yahoo.fr>
19 import cmd
20 import os
21 from readline import get_current_history_length, get_history_item
22 from readline import add_history, clear_history
23 import sys
24 import tempfile
25 import termios
26 from fnmatch import fnmatch
28 from gsh.console import console_output
29 from gsh.stdin import the_stdin_thread
30 from gsh.host_syntax import expand_syntax
31 from gsh import remote_dispatcher
33 # The controlling shell, accessible with Ctrl-C
34 singleton = None
36 def make_singleton(options):
37 """Prepate the control shell at initialization time"""
38 global singleton
39 singleton = control_shell(options)
41 def launch():
42 """Ctrl-C was pressed"""
43 return singleton.launch()
45 def send_termios_char(char):
46 """Used to send a special character to all processes"""
47 for i in remote_dispatcher.all_instances():
48 c = termios.tcgetattr(i.fd)[6][char]
49 i.dispatch_write(c)
51 def toggle_shells(command, enable):
52 """Enable or disable the specified shells"""
53 for i in selected_shells(command):
54 if i.active:
55 i.enabled = enable
57 def selected_shells(command):
58 """Iterator over the shells with names matching the patterns"""
59 for pattern in command.split():
60 found = False
61 for expanded_pattern in expand_syntax(pattern):
62 for i in remote_dispatcher.all_instances():
63 if fnmatch(i.display_name, expanded_pattern):
64 found = True
65 yield i
66 if not found:
67 print pattern, 'not found'
69 def complete_shells(text, line, predicate):
70 """Return the shell names to include in the completion"""
71 res = [i.display_name + ' ' for i in remote_dispatcher.all_instances() if \
72 i.display_name.startswith(text) and \
73 predicate(i) and \
74 ' ' + i.display_name + ' ' not in line]
75 return res
78 # This file descriptor is used to interrupt readline in raw_input().
79 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
80 # A simple '\n' seems to makes raw_input() return in all cases.
81 tempfile_fd, tempfile_name = tempfile.mkstemp()
82 os.remove(tempfile_name)
83 os.write(tempfile_fd, '\n')
85 def interrupt_stdin_thread():
86 """The stdin thread may be in raw_input(), get out of it"""
87 if the_stdin_thread.ready_event.isSet():
88 dupped_stdin = os.dup(0) # Backup the stdin fd
89 assert not the_stdin_thread.wants_control_shell
90 the_stdin_thread.wants_control_shell = True # Not user triggered
91 os.lseek(tempfile_fd, 0, 0) # Rewind in the temp file
92 os.dup2(tempfile_fd, 0) # This will make raw_input() return
93 the_stdin_thread.interrupted_event.wait() # Wait for this return
94 the_stdin_thread.wants_control_shell = False
95 os.dup2(dupped_stdin, 0) # Restore stdin
96 os.close(dupped_stdin) # Cleanup
98 def switch_readline_history(new_histo):
99 """Alternate between the command line history from the remote shells (gsh)
100 and the control shell"""
101 xhisto_idx = xrange(1, get_current_history_length() + 1)
102 prev_histo = map(get_history_item, xhisto_idx)
103 clear_history()
104 for line in new_histo:
105 add_history(line)
106 return prev_histo
108 class control_shell(cmd.Cmd):
109 """The little command line brought when a SIGINT is received"""
110 def __init__(self, options):
111 cmd.Cmd.__init__(self)
112 self.options = options
113 self.prompt = '[ctrl]> '
114 self.history = []
116 def launch(self):
117 if not self.options.interactive:
118 # A Ctrl-C was issued in a non-interactive gsh => exit
119 sys.exit(1)
120 self.stop = False
121 interrupt_stdin_thread()
122 gsh_histo = switch_readline_history(self.history)
123 console_output('\r')
124 try:
125 while True:
126 try:
127 cmd.Cmd.cmdloop(self)
128 except KeyboardInterrupt:
129 console_output('\n')
130 else:
131 break
132 finally:
133 self.history = switch_readline_history(gsh_histo)
135 def completenames(self, text, *ignored):
136 """Overriden to add the trailing space"""
137 return [c + ' ' for c in cmd.Cmd.completenames(self, text, ignored)]
139 # We do this just to have 'help' in the 'Documented commands'
140 def do_help(self, command):
142 List available commands
144 return cmd.Cmd.do_help(self, command)
146 def do_list(self, command):
148 List all remote shells and their states
150 nr_active = nr_dead = 0
151 instances = []
152 for i in remote_dispatcher.all_instances():
153 instances.append(i.get_info())
154 if i.active:
155 nr_active += 1
156 else:
157 nr_dead += 1
158 remote_dispatcher.format_info(instances)
159 print '%s\n\n%d active shells, %d dead shells, total: %d' % \
160 ('\n'.join(instances), nr_active, nr_dead, nr_active + nr_dead)
162 def do_continue(self, command):
164 Go back to gsh
166 self.stop = True
168 def do_EOF(self, command):
170 Go back to gsh
172 return self.do_continue(command)
174 def do_quit(self, command):
176 Quit gsh
178 sys.exit(0)
180 def do_get_print_first(self, command):
182 Check whether we only print the first line for each command output
184 print 'print_first = ' + str(not not self.options.print_first)
186 def do_set_print_first(self, command):
188 Print only the first line for each command output
190 self.options.print_first = True
192 def do_unset_print_first(self, command):
194 Print all lines for each command output
196 self.options.print_first = False
198 def do_send_sigint(self, command):
200 Send a Ctrl-C to all remote shells
202 send_termios_char(termios.VINTR)
204 def do_send_eof(self, command):
206 Send a Ctrl-D to all remote shells
208 send_termios_char(termios.VEOF)
210 def do_send_sigtstp(self, command):
212 Send a Ctrl-Z to all remote shells
214 send_termios_char(termios.VSUSP)
216 def complete_enable(self, text, line, begidx, endidx):
217 return complete_shells(text, line, lambda i: i.active and not i.enabled)
219 def do_enable(self, command):
221 Enable sending commands to the specified shells
222 * ? and [] work as expected
224 toggle_shells(command, True)
226 def complete_disable(self, text, line, begidx, endidx):
227 return complete_shells(text, line, lambda i: i.active and i.enabled)
229 def do_disable(self, command):
231 Disable sending commands to the specified shells
232 * ? and [] work as expected
234 toggle_shells(command, False)
236 def complete_reconnect(self, text, line, begidx, endidx):
237 return complete_shells(text, line, lambda i: not i.active)
239 def do_reconnect(self, command):
241 Try to reconnect to the specified remote shells that have been
242 disconnected
244 for i in selected_shells(command):
245 if not i.active:
246 i.reconnect()
248 def do_add(self, command):
250 Add a remote shell
252 for host in command.split():
253 remote_dispatcher.remote_dispatcher(self.options, host)
255 def do_delete_disabled(self, command):
257 Delete remote processes that are disabled, in order to have a shorter
258 list
260 to_delete = []
261 for i in remote_dispatcher.all_instances():
262 if not i.enabled:
263 to_delete.append(i)
264 for i in to_delete:
265 i.close()
267 def do_rename(self, command):
269 Rename all enabled remote processes with the argument. The argument will
270 be shell expanded on the remote processes. With no argument, the
271 original hostname will be restored as the displayed name.
273 for i in remote_dispatcher.all_instances():
274 if i.enabled:
275 i.rename(command)
277 def postcmd(self, stop, line):
278 return self.stop
280 def emptyline(self):
281 pass