Consistency
[polysh.git] / gsh / control_shell.py
blob20a7806097af686681bcdec449d7203f4fecb2b7
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 asyncore
20 import cmd
21 import os
22 from readline import get_current_history_length, get_history_item
23 from readline import add_history, clear_history
24 import sys
25 import tempfile
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 toggle_shells(command, enable):
46 """Enable or disable the specified shells"""
47 for i in selected_shells(command):
48 if i.active:
49 i.set_enabled(enable)
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():
55 found = False
56 for expanded_pattern in expand_syntax(pattern):
57 for i in remote_dispatcher.all_instances():
58 if fnmatch(i.display_name, expanded_pattern):
59 found = True
60 yield i
61 if not found:
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 \
68 predicate(i) and \
69 ' ' + i.display_name + ' ' not in line]
70 return res
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
76 # a newline
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)
99 clear_history()
100 for line in new_histo:
101 add_history(line)
102 return prev_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]> '
110 self.history = []
112 def launch(self):
113 if not self.options.interactive:
114 # A Ctrl-C was issued in a non-interactive gsh => exit
115 raise asyncore.ExitNow(1)
116 self.stop = False
117 interrupt_stdin_thread()
118 gsh_histo = switch_readline_history(self.history)
119 print ''
120 console_output('')
121 try:
122 while True:
123 try:
124 cmd.Cmd.cmdloop(self)
125 except KeyboardInterrupt:
126 console_output('\n')
127 else:
128 break
129 finally:
130 self.history = switch_readline_history(gsh_histo)
131 console_output('\r')
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
155 instances = []
156 for i in selected_shells(command):
157 instances.append(i.get_info())
158 if i.active:
159 nr_active += 1
160 else:
161 nr_dead += 1
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):
168 Usage: continue
169 Go back to gsh
171 self.stop = True
173 def do_EOF(self, command):
175 Usage: Ctrl-D
176 Go back to gsh
178 return self.do_continue(command)
180 def do_quit(self, command):
182 Usage: quit
183 Quit gsh
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'):
192 return [text + ' ']
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()
204 if not splitted:
205 print 'Expected at least a letter'
206 return
207 letter = splitted[0]
208 if len(letter) != 1:
209 print 'Expected a single letter, got:', letter
210 return
211 control_letter = chr(ord(letter.lower()) - ord('a') + 1)
212 for i in selected_shells(' '.join(splitted[1:])):
213 if i.enabled:
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
245 disconnected
246 The special characters * ? and [] work as expected
248 for i in selected_shells(command):
249 if not i.active:
250 i.reconnect()
252 def do_add(self, command):
254 Usage: add NAMES...
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
270 to_delete = []
271 for i in selected_shells(command):
272 if not i.enabled:
273 to_delete.append(i)
274 for i in to_delete:
275 i.close()
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():
285 if i.enabled:
286 i.rename(command)
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'):
293 return [text + ' ']
294 return ['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
301 disable it.
302 The remaining optional arguments are the selected shells.
303 The special characters * ? and [] work as expected
305 splitted = command.split()
306 if not splitted:
307 print 'Expected at least a letter'
308 return
309 letter = splitted[0].lower()
310 if letter not in ('y', 'n'):
311 print "Expected 'y' or 'n', got:", splitted[0]
312 return
313 debug = letter == 'y'
314 for i in selected_shells(' '.join(splitted[1:])):
315 i.debug = debug
317 def postcmd(self, stop, line):
318 return self.stop
320 def emptyline(self):
321 pass