Added the :print_read_buffer control command to see what gsh just read
[polysh.git] / gsh / remote_dispatcher.py
blobacb765f96fa2d3dd825bbe17753b61f0d0a6b568
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 pty
22 import signal
23 import sys
24 import termios
26 from gsh.buffered_dispatcher import buffered_dispatcher
27 from gsh import callbacks
28 from gsh.console import console_output
29 from gsh import file_transfer
31 # Either the remote shell is expecting a command or one is already running
32 STATE_NAMES = ['not_started', 'idle', 'running', 'terminated']
34 STATE_NOT_STARTED, \
35 STATE_IDLE, \
36 STATE_RUNNING, \
37 STATE_TERMINATED = range(len(STATE_NAMES))
39 # Count the total number of remote_dispatcher.handle_read() invocations
40 nr_handle_read = 0
42 def main_loop_iteration(timeout=None):
43 """Return the number of remote_dispatcher.handle_read() calls made by this
44 iteration"""
45 prev_nr_read = nr_handle_read
46 asyncore.loop(count=1, timeout=timeout, use_poll=True)
47 return nr_handle_read - prev_nr_read
49 def log(msg):
50 if options.log_file:
51 options.log_file.write(msg)
53 class remote_dispatcher(buffered_dispatcher):
54 """A remote_dispatcher is a ssh process we communicate with"""
56 def __init__(self, hostname):
57 self.pid, fd = pty.fork()
58 if self.pid == 0:
59 # Child
60 self.launch_ssh(hostname)
61 sys.exit(1)
63 # Parent
64 buffered_dispatcher.__init__(self, fd)
65 self.hostname = hostname
66 self.debug = options.debug
67 self.active = True # deactived shells are dead forever
68 self.enabled = True # shells can be enabled and disabled
69 self.state = STATE_NOT_STARTED
70 self.term_size = (-1, -1)
71 self.display_name = ''
72 self.change_name(hostname)
73 self.init_string = self.configure_tty() + self.set_prompt()
74 self.init_string_sent = False
75 self.read_in_state_not_started = ''
76 self.command = options.command
78 def launch_ssh(self, name):
79 """Launch the ssh command in the child process"""
80 evaluated = options.ssh % {'host': name}
81 if evaluated == options.ssh:
82 evaluated = '%s %s' % (evaluated, name)
83 os.execlp('/bin/sh', 'sh', '-c', evaluated)
85 def set_enabled(self, enabled):
86 from gsh.dispatchers import update_max_display_name_length
87 self.enabled = enabled
88 if options.interactive:
89 # In non-interactive mode, remote processes leave as soon
90 # as they are terminated, but we don't want to break the
91 # indentation if all the remaining processes have short names.
92 l = len(self.display_name)
93 if not enabled:
94 l = -l
95 update_max_display_name_length(l)
97 def change_state(self, state):
98 """Change the state of the remote process, logging the change"""
99 if state is not self.state:
100 if self.debug:
101 self.print_debug('state => %s' % (STATE_NAMES[state]))
102 if self.state == STATE_NOT_STARTED:
103 self.read_in_state_not_started = ''
104 self.state = state
106 def disconnect(self):
107 """We are no more interested in this remote process"""
108 try:
109 os.kill(self.pid, signal.SIGKILL)
110 except OSError:
111 # The process was already dead, no problem
112 pass
113 self.read_buffer = ''
114 self.write_buffer = ''
115 self.active = False
116 self.set_enabled(False)
117 if self.read_in_state_not_started:
118 self.print_lines(self.read_in_state_not_started)
119 self.read_in_state_not_started = ''
120 if options.abort_error and self.state is STATE_NOT_STARTED:
121 raise asyncore.ExitNow(1)
123 def reconnect(self):
124 """Relaunch and reconnect to this same remote process"""
125 self.disconnect()
126 self.close()
127 remote_dispatcher(self.hostname)
129 def configure_tty(self):
130 """We don't want \n to be replaced with \r\n, and we disable the echo"""
131 attr = termios.tcgetattr(self.fd)
132 attr[1] &= ~termios.ONLCR # oflag
133 attr[3] &= ~termios.ECHO # lflag
134 termios.tcsetattr(self.fd, termios.TCSANOW, attr)
135 # unsetopt zle prevents Zsh from resetting the tty
136 return 'unsetopt zle 2> /dev/null;stty -echo -onlcr;'
138 def seen_prompt_cb(self, unused):
139 if options.interactive:
140 self.change_state(STATE_IDLE)
141 elif self.command:
142 self.dispatch_command(self.command + '\n')
143 self.command = None
144 else:
145 self.change_state(STATE_TERMINATED)
146 self.disconnect()
148 def set_prompt(self):
149 """The prompt is important because we detect the readyness of a process
150 by waiting for its prompt."""
151 # No right prompt
152 command_line = 'RPS1=;RPROMPT=;'
153 command_line += 'TERM=ansi;'
154 command_line += 'unset HISTFILE;'
155 prompt1, prompt2 = callbacks.add('prompt', self.seen_prompt_cb, True)
156 command_line += 'PS1="%s""%s\n"\n' % (prompt1, prompt2)
157 return command_line
159 def readable(self):
160 """We are always interested in reading from active remote processes if
161 the buffer is OK"""
162 return self.active and buffered_dispatcher.readable(self)
164 def handle_error(self):
165 """An exception may or may not lead to a disconnection"""
166 if buffered_dispatcher.handle_error(self):
167 console_output('Error talking to %s\n' % self.display_name)
168 self.disconnect()
170 def print_lines(self, lines):
171 from gsh.dispatchers import max_display_name_length
172 lines = lines.strip('\n')
173 while True:
174 no_empty_lines = lines.replace('\n\n', '\n')
175 if len(no_empty_lines) == len(lines):
176 break
177 lines = no_empty_lines
178 if not lines:
179 return
180 indent = max_display_name_length - len(self.display_name)
181 prefix = self.display_name + indent * ' ' + ': '
182 console_output(prefix + lines.replace('\n', '\n' + prefix) + '\n')
184 def handle_read_fast_case(self, data):
185 """If we are in a fast case we'll avoid the long processing of each
186 line"""
187 if self.state is not STATE_RUNNING or callbacks.any_in(data):
188 # Slow case :-(
189 return False
191 last_nl = data.rfind('\n')
192 if last_nl == -1:
193 # No '\n' in data => slow case
194 return False
195 self.read_buffer = data[last_nl + 1:]
196 self.print_lines(data[:last_nl])
197 return True
199 def handle_read(self):
200 """We got some output from a remote shell, this is one of the state
201 machine"""
202 if not self.active:
203 return
204 global nr_handle_read
205 nr_handle_read += 1
206 new_data = buffered_dispatcher.handle_read(self)
207 if self.debug:
208 self.print_debug('==> ' + new_data)
209 if self.handle_read_fast_case(self.read_buffer):
210 return
211 lf_pos = new_data.find('\n')
212 if lf_pos >= 0:
213 # Optimization: we knew there were no '\n' in the previous read
214 # buffer, so we searched only in the new_data and we offset the
215 # found index by the length of the previous buffer
216 lf_pos += len(self.read_buffer) - len(new_data)
217 while lf_pos >= 0:
218 # For each line in the buffer
219 line = self.read_buffer[:lf_pos + 1]
220 if callbacks.process(line):
221 pass
222 elif self.state in (STATE_IDLE, STATE_RUNNING):
223 self.print_lines(line)
224 elif self.state is STATE_NOT_STARTED:
225 self.read_in_state_not_started += line
226 if 'The authenticity of host' in line:
227 msg = line.strip('\n') + ' Closing connection.'
228 self.disconnect()
229 elif 'REMOTE HOST IDENTIFICATION HAS CHANGED' in line:
230 msg = 'Remote host identification has changed.'
231 else:
232 msg = None
234 if msg:
235 self.print_lines(msg + ' Consider manually connecting or ' +
236 'using ssh-keyscan.')
238 # Go to the next line in the buffer
239 self.read_buffer = self.read_buffer[lf_pos + 1:]
240 if self.handle_read_fast_case(self.read_buffer):
241 return
242 lf_pos = self.read_buffer.find('\n')
243 if self.state is STATE_NOT_STARTED and not self.init_string_sent:
244 self.dispatch_write(self.init_string)
245 self.init_string_sent = True
247 def print_unfinished_line(self):
248 """The unfinished line stayed long enough in the buffer to be printed"""
249 if self.state is STATE_RUNNING:
250 self.print_lines(self.read_buffer)
251 self.read_buffer = ''
253 def writable(self):
254 """Do we want to write something?"""
255 return self.active and buffered_dispatcher.writable(self)
257 def handle_write(self):
258 """Let's write as much as we can"""
259 num_sent = self.send(self.write_buffer)
260 if self.debug:
261 self.print_debug('<== ' + self.write_buffer[:num_sent])
262 self.write_buffer = self.write_buffer[num_sent:]
264 def print_debug(self, msg):
265 """Log some debugging information to the console"""
266 state = STATE_NAMES[self.state]
267 msg = msg.encode('string_escape')
268 console_output('[dbg] %s[%s]: %s\n' % (self.display_name, state, msg))
270 def get_info(self):
271 """Return a list with all information available about this process"""
272 if self.active:
273 state = STATE_NAMES[self.state]
274 else:
275 state = ''
277 if self.debug:
278 debug = 'debug'
279 else:
280 debug = ''
282 return [self.display_name, 'fd:%d' % (self.fd),
283 'r:%d' % (len(self.read_buffer)),
284 'w:%d' % (len(self.write_buffer)),
285 self.active and 'active' or 'dead',
286 self.enabled and 'enabled' or 'disabled',
287 state,
288 debug]
290 def dispatch_write(self, buf):
291 """There is new stuff to write when possible"""
292 if self.active and self.enabled:
293 buffered_dispatcher.dispatch_write(self, buf)
294 return True
296 def dispatch_command(self, command):
297 if self.dispatch_write(command):
298 self.change_state(STATE_RUNNING)
300 def change_name(self, name):
301 """Change the name of the shell, possibly updating the maximum name
302 length"""
303 from gsh import dispatchers
304 if not name:
305 name = self.hostname
306 previous_name_len = len(self.display_name)
307 self.display_name = None
308 self.display_name = dispatchers.make_unique_name(name)
309 dispatchers.update_max_display_name_length(len(self.display_name))
310 dispatchers.update_max_display_name_length(-previous_name_len)
312 def rename(self, string):
313 """Send to the remote shell, its new name to be shell expanded"""
314 if string:
315 rename1, rename2 = callbacks.add('rename', self.change_name, False)
316 self.dispatch_command('/bin/echo "%s""%s"%s\n' %
317 (rename1, rename2, string))
318 else:
319 self.change_name(self.hostname)
321 def __str__(self):
322 return self.display_name