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 dispatchers
, 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 the shells"""
55 for pattern
in (command
or '*').split():
57 for expanded_pattern
in expand_syntax(pattern
):
58 for i
in dispatchers
.all_instances():
59 if fnmatch(i
.display_name
, expanded_pattern
):
65 print pattern
, 'not found'
67 def complete_shells(text
, line
, predicate
=lambda i
: True):
68 """Return the shell names to include in the completion"""
69 res
= [i
.display_name
+ ' ' for i
in dispatchers
.all_instances() if \
70 i
.display_name
.startswith(text
) and \
72 ' ' + i
.display_name
+ ' ' not in line
]
76 # This file descriptor is used to interrupt readline in raw_input().
77 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
78 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
80 tempfile_fd
, tempfile_name
= tempfile
.mkstemp()
81 os
.remove(tempfile_name
)
82 os
.write(tempfile_fd
, chr(3))
84 def interrupt_stdin_thread():
85 """The stdin thread may be in raw_input(), get out of it"""
86 if the_stdin_thread
.ready_event
.isSet():
87 dupped_stdin
= os
.dup(0) # Backup the stdin fd
88 assert not the_stdin_thread
.wants_control_shell
89 the_stdin_thread
.wants_control_shell
= True # Not user triggered
90 os
.lseek(tempfile_fd
, 0, 0) # Rewind in the temp file
91 os
.dup2(tempfile_fd
, 0) # This will make raw_input() return
92 the_stdin_thread
.interrupted_event
.wait() # Wait for this return
93 the_stdin_thread
.wants_control_shell
= False
94 os
.dup2(dupped_stdin
, 0) # Restore stdin
95 os
.close(dupped_stdin
) # Cleanup
97 def switch_readline_history(new_histo
):
98 """Alternate between the command line history from the remote shells (gsh)
99 and the control shell"""
100 xhisto_idx
= xrange(1, get_current_history_length() + 1)
101 prev_histo
= map(get_history_item
, xhisto_idx
)
103 for line
in new_histo
:
107 def complete_control_command(text
, state
):
108 return singleton
.modified_cmd_complete(text
, state
)
110 def handle_control_command(line
):
111 singleton
.onecmd(line
)
113 class control_shell(cmd
.Cmd
):
114 """The little command line brought when a SIGINT is received"""
115 def __init__(self
, options
):
116 cmd
.Cmd
.__init
__(self
)
117 self
.options
= options
118 self
.prompt
= '[ctrl]> '
122 if not self
.options
.interactive
:
123 # A Ctrl-C was issued in a non-interactive gsh => exit
124 raise asyncore
.ExitNow(1)
125 interrupt_stdin_thread()
126 gsh_histo
= switch_readline_history(self
.history
)
131 cmd
.Cmd
.cmdloop(self
)
132 except KeyboardInterrupt:
137 self
.history
= switch_readline_history(gsh_histo
)
140 def completenames(self
, text
, *ignored
):
141 """Overriden to add the trailing space"""
142 return [c
+ ' ' for c
in cmd
.Cmd
.completenames(self
, text
, ignored
)]
144 def modified_cmd_complete(self
, text
, state
):
145 """Modified to ignore the leading ':'"""
148 origline
= readline
.get_line_buffer()
149 line
= origline
.lstrip(':')
150 stripped
= len(origline
) - len(line
)
151 begidx
= readline
.get_begidx() - stripped
152 endidx
= readline
.get_endidx() - stripped
154 cmd
, args
, foo
= self
.parseline(line
)
156 compfunc
= self
.completedefault
159 compfunc
= getattr(self
, 'complete_' + cmd
)
160 except AttributeError:
161 compfunc
= self
.completedefault
163 compfunc
= self
.completenames
164 self
.completion_matches
= compfunc(text
, line
, begidx
, endidx
)
166 return self
.completion_matches
[state
]
170 # We do this just to have 'help' in the 'Documented commands'
171 def do_help(self
, command
):
173 Usage: help [COMMAND]
174 List available commands or show the documentation of a specific command.
176 return cmd
.Cmd
.do_help(self
, command
)
178 def complete_list(self
, text
, line
, begidx
, endidx
):
179 return complete_shells(text
, line
)
181 def do_list(self
, command
):
183 Usage: list [SHELLS...]
184 List the specified or all remote shells and their states.
185 The special characters * ? and [] work as expected.
187 nr_active
= nr_dead
= 0
189 for i
in selected_shells(command
):
190 instances
.append(i
.get_info())
195 dispatchers
.format_info(instances
)
196 print '%s\n\n%d active shells, %d dead shells, total: %d' % \
197 ('\n'.join(instances
), nr_active
, nr_dead
, nr_active
+ nr_dead
)
199 def do_continue(self
, command
):
206 def do_EOF(self
, command
):
211 return self
.do_continue(command
)
213 def do_quit(self
, command
):
218 raise asyncore
.ExitNow(0)
220 def complete_send_control(self
, text
, line
, begidx
, endidx
):
221 if line
[len('send_control'):begidx
].strip():
222 # Control letter already given in command line
223 return complete_shells(text
, line
, lambda i
: i
.enabled
)
224 if text
in ('c', 'd', 'z'):
226 return ['c', 'd', 'z']
228 def do_send_control(self
, command
):
230 Usage: send_control LETTER [SHELLS...]
231 Send a control character to the specified or all enabled shells.
232 The first argument is the control character to send: c, d or z.
233 The remaining optional arguments are the destination shells.
234 The special characters * ? and [] work as expected.
236 splitted
= command
.split()
238 print 'Expected at least a letter'
242 print 'Expected a single letter, got:', letter
244 control_letter
= chr(ord(letter
.lower()) - ord('a') + 1)
245 for i
in selected_shells(' '.join(splitted
[1:])):
247 i
.dispatch_write(control_letter
)
249 def complete_enable(self
, text
, line
, begidx
, endidx
):
250 return complete_shells(text
, line
, lambda i
: i
.active
and not i
.enabled
)
252 def do_enable(self
, command
):
254 Usage: enable [SHELLS...]
255 Enable sending commands to all or the specified shells.
256 The special characters * ? and [] work as expected.
258 toggle_shells(command
, True)
260 def complete_disable(self
, text
, line
, begidx
, endidx
):
261 return complete_shells(text
, line
, lambda i
: i
.enabled
)
263 def do_disable(self
, command
):
265 Usage: disable [SHELLS...]
266 Disable sending commands to all or the specified shells.
267 The special characters * ? and [] work as expected.
269 toggle_shells(command
, False)
271 def complete_reconnect(self
, text
, line
, begidx
, endidx
):
272 return complete_shells(text
, line
, lambda i
: not i
.active
)
274 def do_reconnect(self
, command
):
276 Usage: reconnect [SHELLS...]
277 Try to reconnect to all or the specified remote shells that have been
279 The special characters * ? and [] work as expected.
281 for i
in selected_shells(command
):
285 def do_add(self
, command
):
288 Add one or many remote shells.
290 for host
in command
.split():
291 remote_dispatcher
.remote_dispatcher(self
.options
, host
)
293 def complete_delete_disabled(self
, text
, line
, begidx
, endidx
):
294 return complete_shells(text
, line
, lambda i
: not i
.enabled
)
296 def do_delete_disabled(self
, command
):
298 Usage: delete_disabled [SHELLS...]
299 Delete the specified or all remote processes that are disabled,
300 in order to have a shorter list.
301 The special characters * ? and [] work as expected.
304 for i
in selected_shells(command
):
311 def do_rename(self
, command
):
313 Usage: rename [NEW_NAME]
314 Rename all enabled remote processes with the argument. The argument will
315 be shell expanded on the remote processes. With no argument, the
316 original hostname will be restored as the displayed name.
318 for i
in dispatchers
.all_instances():
322 def complete_set_debug(self
, text
, line
, begidx
, endidx
):
323 if line
[len('set_debug'):begidx
].strip():
324 # Control letter already given in command line
325 return complete_shells(text
, line
)
326 if text
.lower() in ('y', 'n'):
330 def do_set_debug(self
, command
):
332 Usage: set_debug y|n [SHELLS...]
333 Enable or disable debugging output for all or the specified shells.
334 The first argument is 'y' to enable the debugging output, 'n' to
336 The remaining optional arguments are the selected shells.
337 The special characters * ? and [] work as expected.
339 splitted
= command
.split()
341 print 'Expected at least a letter'
343 letter
= splitted
[0].lower()
344 if letter
not in ('y', 'n'):
345 print "Expected 'y' or 'n', got:", splitted
[0]
347 debug
= letter
== 'y'
348 for i
in selected_shells(' '.join(splitted
[1:])):