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>
21 from readline
import get_current_history_length
, get_history_item
22 from readline
import add_history
, clear_history
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
36 def make_singleton(options
):
37 """Prepate the control shell at initialization time"""
39 singleton
= control_shell(options
)
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
]
51 def toggle_shells(command
, enable
):
52 """Enable or disable the specified shells"""
53 for i
in selected_shells(command
):
57 def selected_shells(command
):
58 """Iterator over the shells with names matching the patterns"""
59 for pattern
in command
.split():
61 for expanded_pattern
in expand_syntax(pattern
):
62 for i
in remote_dispatcher
.all_instances():
63 if fnmatch(i
.display_name
, expanded_pattern
):
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 \
74 ' ' + i
.display_name
+ ' ' not in line
]
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
)
104 for line
in new_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]> '
117 if not self
.options
.interactive
:
118 # A Ctrl-C was issued in a non-interactive gsh => exit
121 interrupt_stdin_thread()
122 gsh_histo
= switch_readline_history(self
.history
)
127 cmd
.Cmd
.cmdloop(self
)
128 except KeyboardInterrupt:
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
152 for i
in remote_dispatcher
.all_instances():
153 instances
.append(i
.get_info())
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
):
168 def do_EOF(self
, command
):
172 return self
.do_continue(command
)
174 def do_quit(self
, command
):
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
244 for i
in selected_shells(command
):
248 def do_add(self
, command
):
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
261 for i
in remote_dispatcher
.all_instances():
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():
277 def postcmd(self
, stop
, line
):