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>
22 from readline
import get_current_history_length
, get_history_item
23 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 toggle_shells(command
, enable
):
46 """Enable or disable the specified shells"""
47 for i
in selected_shells(command
):
51 def selected_shells(command
):
52 """Iterator over the shells with names matching the patterns.
53 An empty patterns matches all shells"""
54 for pattern
in (command
or '*').split():
56 for expanded_pattern
in expand_syntax(pattern
):
57 for i
in remote_dispatcher
.all_instances():
58 if fnmatch(i
.display_name
, expanded_pattern
):
62 print pattern
, 'not found'
64 def complete_shells(text
, line
, predicate
=lambda i
: True):
65 """Return the shell names to include in the completion"""
66 res
= [i
.display_name
+ ' ' for i
in remote_dispatcher
.all_instances() if \
67 i
.display_name
.startswith(text
) and \
69 ' ' + i
.display_name
+ ' ' not in line
]
73 # This file descriptor is used to interrupt readline in raw_input().
74 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
75 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
77 tempfile_fd
, tempfile_name
= tempfile
.mkstemp()
78 os
.remove(tempfile_name
)
79 os
.write(tempfile_fd
, chr(3))
81 def interrupt_stdin_thread():
82 """The stdin thread may be in raw_input(), get out of it"""
83 if the_stdin_thread
.ready_event
.isSet():
84 dupped_stdin
= os
.dup(0) # Backup the stdin fd
85 assert not the_stdin_thread
.wants_control_shell
86 the_stdin_thread
.wants_control_shell
= True # Not user triggered
87 os
.lseek(tempfile_fd
, 0, 0) # Rewind in the temp file
88 os
.dup2(tempfile_fd
, 0) # This will make raw_input() return
89 the_stdin_thread
.interrupted_event
.wait() # Wait for this return
90 the_stdin_thread
.wants_control_shell
= False
91 os
.dup2(dupped_stdin
, 0) # Restore stdin
92 os
.close(dupped_stdin
) # Cleanup
94 def switch_readline_history(new_histo
):
95 """Alternate between the command line history from the remote shells (gsh)
96 and the control shell"""
97 xhisto_idx
= xrange(1, get_current_history_length() + 1)
98 prev_histo
= map(get_history_item
, xhisto_idx
)
100 for line
in new_histo
:
104 class control_shell(cmd
.Cmd
):
105 """The little command line brought when a SIGINT is received"""
106 def __init__(self
, options
):
107 cmd
.Cmd
.__init
__(self
)
108 self
.options
= options
109 self
.prompt
= '[ctrl]> '
113 if not self
.options
.interactive
:
114 # A Ctrl-C was issued in a non-interactive gsh => exit
115 raise asyncore
.ExitNow(1)
117 interrupt_stdin_thread()
118 gsh_histo
= switch_readline_history(self
.history
)
124 cmd
.Cmd
.cmdloop(self
)
125 except KeyboardInterrupt:
130 self
.history
= switch_readline_history(gsh_histo
)
133 def completenames(self
, text
, *ignored
):
134 """Overriden to add the trailing space"""
135 return [c
+ ' ' for c
in cmd
.Cmd
.completenames(self
, text
, ignored
)]
137 # We do this just to have 'help' in the 'Documented commands'
138 def do_help(self
, command
):
140 Usage: help [COMMAND]
141 List available commands or show the documentation of a specific command.
143 return cmd
.Cmd
.do_help(self
, command
)
145 def complete_list(self
, text
, line
, begidx
, endidx
):
146 return complete_shells(text
, line
)
148 def do_list(self
, command
):
150 Usage: list [SHELLS...]
151 List the specified or all remote shells and their states.
152 The special characters * ? and [] work as expected.
154 nr_active
= nr_dead
= 0
156 for i
in selected_shells(command
):
157 instances
.append(i
.get_info())
162 remote_dispatcher
.format_info(instances
)
163 print '%s\n\n%d active shells, %d dead shells, total: %d' % \
164 ('\n'.join(instances
), nr_active
, nr_dead
, nr_active
+ nr_dead
)
166 def do_continue(self
, command
):
173 def do_EOF(self
, command
):
178 return self
.do_continue(command
)
180 def do_quit(self
, command
):
185 raise asyncore
.ExitNow(0)
187 def complete_send_control(self
, text
, line
, begidx
, endidx
):
188 if line
[len('send_control'):begidx
].strip():
189 # Control letter already given in command line
190 return complete_shells(text
, line
, lambda i
: i
.enabled
)
191 if text
in ('c', 'd', 'z'):
193 return ['c', 'd', 'z']
195 def do_send_control(self
, command
):
197 Usage: send_control LETTER [SHELLS...]
198 Send a control character to the specified or all enabled shells.
199 The first argument is the control character to send: c, d or z.
200 The remaining optional arguments are the destination shells.
201 The special characters * ? and [] work as expected.
203 splitted
= command
.split()
205 print 'Expected at least a letter'
209 print 'Expected a single letter, got:', letter
211 control_letter
= chr(ord(letter
.lower()) - ord('a') + 1)
212 for i
in selected_shells(' '.join(splitted
[1:])):
214 i
.dispatch_write(control_letter
)
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 Usage: enable [SHELLS...]
222 Enable sending commands to all or the specified shells.
223 The special characters * ? and [] work as expected.
225 toggle_shells(command
, True)
227 def complete_disable(self
, text
, line
, begidx
, endidx
):
228 return complete_shells(text
, line
, lambda i
: i
.enabled
)
230 def do_disable(self
, command
):
232 Usage: disable [SHELLS...]
233 Disable sending commands to all or the specified shells.
234 The special characters * ? and [] work as expected.
236 toggle_shells(command
, False)
238 def complete_reconnect(self
, text
, line
, begidx
, endidx
):
239 return complete_shells(text
, line
, lambda i
: not i
.active
)
241 def do_reconnect(self
, command
):
243 Usage: reconnect [SHELLS...]
244 Try to reconnect to all or the specified remote shells that have been
246 The special characters * ? and [] work as expected.
248 for i
in selected_shells(command
):
252 def do_add(self
, command
):
255 Add one or many remote shells.
257 for host
in command
.split():
258 remote_dispatcher
.remote_dispatcher(self
.options
, host
)
260 def complete_delete_disabled(self
, text
, line
, begidx
, endidx
):
261 return complete_shells(text
, line
, lambda i
: not i
.enabled
)
263 def do_delete_disabled(self
, command
):
265 Usage: delete_disabled [SHELLS...]
266 Delete the specified or all remote processes that are disabled,
267 in order to have a shorter list.
268 The special characters * ? and [] work as expected.
271 for i
in selected_shells(command
):
277 def do_rename(self
, command
):
279 Usage: rename [NEW_NAME]
280 Rename all enabled remote processes with the argument. The argument will
281 be shell expanded on the remote processes. With no argument, the
282 original hostname will be restored as the displayed name.
284 for i
in remote_dispatcher
.all_instances():
288 def complete_set_debug(self
, text
, line
, begidx
, endidx
):
289 if line
[len('set_debug'):begidx
].strip():
290 # Control letter already given in command line
291 return complete_shells(text
, line
)
292 if text
in ('y', 'n'):
296 def do_set_debug(self
, command
):
298 Usage: set_debug y|n [SHELLS...]
299 Enable or disable debugging output for all or the specified shells.
300 The first argument is 'y' to enable the debugging output, 'n' to
302 The remaining optional arguments are the selected shells.
303 The special characters * ? and [] work as expected.
305 splitted
= command
.split()
307 print 'Expected at least a letter'
309 letter
= splitted
[0].lower()
310 if letter
not in ('y', 'n'):
311 print "Expected 'y' or 'n', got:", splitted
[0]
313 debug
= letter
== 'y'
314 for i
in selected_shells(' '.join(splitted
[1:])):
317 def postcmd(self
, stop
, line
):