Detect leaked gsh processes
[gsh.git] / gsh / control_commands.py
blob61f55eadbeba3586cfaff71e9e633a0e1598a048
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, 2008 Guillaume Chazarain <guichaz@gmail.com>
19 import asyncore
20 import os
21 import shutil
22 import sys
23 import tempfile
25 from gsh.control_commands_helpers import complete_shells, selected_shells
26 from gsh.control_commands_helpers import list_control_commands
27 from gsh.control_commands_helpers import get_control_command, toggle_shells
28 from gsh.control_commands_helpers import expand_local_path
29 from gsh.completion import complete_local_path
30 from gsh.console import console_output
31 from gsh import dispatchers
32 from gsh import remote_dispatcher
33 from gsh import stdin
34 from gsh import file_transfer
36 def complete_help(line, text):
37 colon = text.startswith(':')
38 text = text.lstrip(':')
39 res = [cmd + ' ' for cmd in list_control_commands() if \
40 cmd.startswith(text) and ' ' + cmd + ' ' not in line]
41 if colon:
42 res = [':' + cmd for cmd in res]
43 return res
45 def do_help(command):
46 """
47 Usage: :help [COMMAND]
48 List control commands or show their documentations.
49 """
50 command = command.strip()
51 if command:
52 texts = []
53 for name in command.split():
54 try:
55 cmd = get_control_command(name.lstrip(':'))
56 except AttributeError:
57 console_output('Unknown control command: %s\n' % name)
58 else:
59 doc = [d.strip() for d in cmd.__doc__.split('\n') if d.strip()]
60 texts.append('\n'.join(doc))
61 if texts:
62 console_output('\n\n'.join(texts))
63 console_output('\n')
64 else:
65 names = list_control_commands()
66 max_name_len = max(map(len, names))
67 for i in xrange(len(names)):
68 name = names[i]
69 txt = (max_name_len - len(name)) * ' ' + ':' + name + ' - '
70 doc = get_control_command(name).__doc__
71 txt += doc.split('\n')[2].strip() + '\n'
72 console_output(txt)
74 def complete_list(line, text):
75 return complete_shells(line, text)
77 def do_list(command):
78 """
79 Usage: :list [SHELLS...]
80 List remote shells and their states.
81 The output consists of: <hostname> <enabled?> <state>: <last printed line>.
82 The special characters * ? and [] work as expected.
83 """
84 instances = [i.get_info() for i in selected_shells(command)]
85 dispatchers.format_info(instances)
86 console_output(''.join(instances))
88 def do_quit(command):
89 """
90 Usage: :quit
91 Quit gsh.
92 """
93 raise asyncore.ExitNow(0)
95 def complete_chdir(line, text):
96 return filter(os.path.isdir, complete_local_path(text))
98 def do_chdir(command):
99 """
100 Usage: :chdir LOCAL_PATH
101 Change the current directory of gsh (not the remote shells).
103 try:
104 os.chdir(expand_local_path(command))
105 except OSError, e:
106 console_output('%s\n' % str(e))
108 def complete_send_ctrl(line, text):
109 if len(line[:-1].split()) >= 2:
110 # Control letter already given in command line
111 return complete_shells(line, text, lambda i: i.enabled)
112 if text in ('c', 'd', 'z'):
113 return [text + ' ']
114 return ['c ', 'd ', 'z ']
116 def do_send_ctrl(command):
118 Usage: :send_ctrl LETTER [SHELLS...]
119 Send a control character to remote shells.
120 The first argument is the control character to send like c, d or z.
121 Note that these three control characters can be sent simply by typing them
122 into gsh.
123 The remaining optional arguments are the destination shells.
124 The special characters * ? and [] work as expected.
126 split = command.split()
127 if not split:
128 console_output('Expected at least a letter\n')
129 return
130 letter = split[0]
131 if len(letter) != 1:
132 console_output('Expected a single letter, got: %s\n' % letter)
133 return
134 control_letter = chr(ord(letter.lower()) - ord('a') + 1)
135 for i in selected_shells(' '.join(split[1:])):
136 if i.enabled:
137 i.dispatch_write(control_letter)
139 def complete_reset_prompt(line, text):
140 return complete_shells(line, text, lambda i: i.enabled)
142 def do_reset_prompt(command):
144 Usage: :reset_prompt [SHELLS...]
145 Change the prompt to be recognized by gsh.
146 The special characters * ? and [] work as expected.
148 for i in selected_shells(command):
149 i.dispatch_command(i.init_string)
151 def complete_enable(line, text):
152 return complete_shells(line, text, lambda i:
153 i.state != remote_dispatcher.STATE_DEAD and not i.enabled)
155 def do_enable(command):
157 Usage: :enable [SHELLS...]
158 Enable sending commands to remote shells.
159 The special characters * ? and [] work as expected.
161 toggle_shells(command, True)
163 def complete_disable(line, text):
164 return complete_shells(line, text, lambda i: i.enabled)
166 def do_disable(command):
168 Usage: :disable [SHELLS...]
169 Disable sending commands to remote shells.
170 The special characters * ? and [] work as expected.
172 toggle_shells(command, False)
174 def complete_reconnect(line, text):
175 return complete_shells(line, text, lambda i:
176 i.state == remote_dispatcher.STATE_DEAD)
178 def do_reconnect(command):
180 Usage: :reconnect [SHELLS...]
181 Try to reconnect to disconnected remote shells.
182 The special characters * ? and [] work as expected.
184 for i in selected_shells(command):
185 if i.state == remote_dispatcher.STATE_DEAD:
186 i.reconnect()
188 def do_add(command):
190 Usage: :add NAMES...
191 Add one or many remote shells.
193 for host in command.split():
194 remote_dispatcher.remote_dispatcher(host)
196 def complete_purge(line, text):
197 return complete_shells(line, text, lambda i: not i.enabled)
199 def do_purge(command):
201 Usage: :purge [SHELLS...]
202 Delete disabled remote shells.
203 This helps to have a shorter list.
204 The special characters * ? and [] work as expected.
206 to_delete = []
207 for i in selected_shells(command):
208 if not i.enabled:
209 to_delete.append(i)
210 for i in to_delete:
211 i.disconnect()
212 i.close()
214 def do_rename(command):
216 Usage: :rename [NEW_NAME]
217 Rename all enabled remote shells with the argument.
218 The argument will be shell expanded on the remote processes. With no
219 argument, the original hostname will be restored as the displayed name.
221 for i in dispatchers.all_instances():
222 if i.enabled:
223 i.rename(command)
225 def do_hide_password(command):
227 Usage: :hide_password
228 Do not echo the next typed line.
229 This is useful when entering password. If debugging or logging is enabled,
230 it will be disabled to avoid displaying a password. Therefore, you will have
231 to reenable logging or debugging afterwards if need be.
233 warned = False
234 for i in dispatchers.all_instances():
235 if i.enabled and i.debug:
236 i.debug = False
237 if not warned:
238 console_output('Debugging disabled to avoid displaying '
239 'passwords\n')
240 warned = True
241 stdin.set_echo(False)
243 if remote_dispatcher.options.log_file:
244 console_output('Logging disabled to avoid writing passwords\n')
245 remote_dispatcher.options.log_file = None
247 def complete_set_debug(line, text):
248 if len(line[:-1].split()) >= 2:
249 # Debug value already given in command line
250 return complete_shells(line, text)
251 if text.lower() in ('y', 'n'):
252 return [text + ' ']
253 return ['y ', 'n ']
255 def do_set_debug(command):
257 Usage: :set_debug y|n [SHELLS...]
258 Enable or disable debugging output for remote shells.
259 The first argument is 'y' to enable the debugging output, 'n' to
260 disable it.
261 The remaining optional arguments are the selected shells.
262 The special characters * ? and [] work as expected.
264 split = command.split()
265 if not split:
266 console_output('Expected at least a letter\n')
267 return
268 letter = split[0].lower()
269 if letter not in ('y', 'n'):
270 console_output("Expected 'y' or 'n', got: %s\n" % split[0])
271 return
272 debug = letter == 'y'
273 for i in selected_shells(' '.join(split[1:])):
274 i.debug = debug
276 def complete_replicate(line, text):
277 if ':' not in text:
278 enabled_shells = complete_shells(line, text, lambda i: i.enabled)
279 return [c[:-1] + ':' for c in enabled_shells]
280 shell, path = text.split(':')
281 return [shell + ':' + p for p in complete_local_path(path)]
283 def do_replicate(command):
285 Usage: :replicate SHELL:REMOTE_PATH
286 Copy a path from one remote shell to all others
288 if ':' not in command:
289 console_output('Usage: :replicate SHELL:REMOTE_PATH\n')
290 return
291 shell_name, path = command.split(':', 1)
292 if not path:
293 console_output('No remote path given\n')
294 return
295 for shell in dispatchers.all_instances():
296 if shell.display_name == shell_name:
297 if not shell.enabled:
298 console_output('%s is not enabled\n' % shell_name)
299 return
300 break
301 else:
302 console_output('%s not found\n' % shell_name)
303 return
304 file_transfer.replicate(shell, path)
306 def complete_upload(line, text):
307 return complete_local_path(text)
309 def do_upload(command):
311 Usage: :upload LOCAL_PATH
312 Upload the specified local path to enabled remote shells.
314 if command:
315 file_transfer.upload(command)
316 else:
317 console_output('No local path given\n')
319 def do_export_rank(command):
321 Usage: :export_rank
322 Set GSH_RANK and GSH_NR_SHELLS on enabled remote shells.
323 The GSH_RANK shell variable uniquely identifies each shell with a number
324 between 0 and GSH_NR_SHELLS - 1. GSH_NR_SHELLS is the total number of
325 enabled shells.
327 rank = 0
328 for shell in dispatchers.all_instances():
329 if shell.enabled:
330 shell.dispatch_command('export GSH_RANK=%d\n' % rank)
331 rank += 1
333 for shell in dispatchers.all_instances():
334 if shell.enabled:
335 shell.dispatch_command('export GSH_NR_SHELLS=%d\n' % rank)
337 def complete_set_log(line, text):
338 return complete_local_path(text)
340 def do_set_log(command):
342 Usage: :set_log [LOCAL_PATH]
343 Duplicate every console input/output into the given local file.
344 If LOCAL_PATH is not given, restore the default behaviour of not logging.
346 if command:
347 try:
348 remote_dispatcher.options.log_file = file(command, 'a')
349 except IOError, e:
350 console_output('%s\n' % str(e))
351 command = None
352 if not command:
353 remote_dispatcher.options.log_file = None
354 console_output('Logging disabled\n')
356 def complete_print_read_buffer(line, text):
357 return complete_shells(line, text, lambda i: i.read_buffer or
358 i.read_in_state_not_started)
360 def do_print_read_buffer(command):
362 Usage: :print_read_buffer [SHELLS...]
363 Print the data read by remote shells.
364 The special characters * ? and [] work as expected.
366 for i in selected_shells(command):
367 if i.read_in_state_not_started:
368 i.print_lines(i.read_in_state_not_started)
369 i.read_in_state_not_started = ''
371 def main():
373 Output a help text of each control command suitable for the man page
374 Run from the gsh top directory: python -m gsh.control_commands
376 try:
377 man_page = file('gsh.1', 'r')
378 except IOError, e:
379 print e
380 print 'Please run "python -m gsh.control_commands" from the gsh top' + \
381 ' directory'
382 sys.exit(1)
384 updated_man_page_fd, updated_man_page_path = tempfile.mkstemp()
385 updated_man_page = os.fdopen(updated_man_page_fd, 'w')
387 for line in man_page:
388 print >> updated_man_page, line,
389 if 'BEGIN AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
390 break
392 for name in list_control_commands():
393 print >> updated_man_page, '.TP'
394 unstripped = get_control_command(name).__doc__.split('\n')
395 lines = [l.strip() for l in unstripped]
396 usage = lines[1].strip()
397 print >> updated_man_page, '\\fB%s\\fR' % usage[7:]
398 help_text = ' '.join(lines[2:]).replace('gsh', '\\fIgsh\\fR').strip()
399 print >> updated_man_page, help_text
401 for line in man_page:
402 if 'END AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
403 print >> updated_man_page, line,
404 break
406 for line in man_page:
407 print >> updated_man_page, line,
409 man_page.close()
410 updated_man_page.close()
411 shutil.move(updated_man_page_path, 'gsh.1')
413 if __name__ == '__main__':
414 main()