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>
27 from gsh
.buffered_dispatcher
import buffered_dispatcher
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']
37 STATE_TERMINATED
= range(len(STATE_NAMES
))
39 # Count the total number of remote_dispatcher.handle_read() invocations
42 def main_loop_iteration(timeout
=None):
43 """Return the number of remote_dispatcher.handle_read() calls made by this
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
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()
60 self
.launch_ssh(hostname
)
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
.termination
= None
71 self
.term_size
= (-1, -1)
72 self
.display_name
= ''
73 self
.change_name(hostname
)
74 self
.init_string
= self
.configure_tty() + self
.set_prompt()
75 self
.init_string_sent
= False
76 self
.pending_rename
= None
77 self
.file_transfer_cookie
= None
78 self
.command
= options
.command
80 def launch_ssh(self
, name
):
81 """Launch the ssh command in the child process"""
82 evaluated
= options
.ssh
% {'host': name
}
83 if evaluated
== options
.ssh
:
84 evaluated
= '%s %s' % (evaluated
, name
)
85 os
.execlp('/bin/sh', 'sh', '-c', evaluated
)
87 def set_enabled(self
, enabled
):
88 from gsh
.dispatchers
import update_max_display_name_length
89 self
.enabled
= enabled
90 if options
.interactive
:
91 # In non-interactive mode, remote processes leave as soon
92 # as they are terminated, but we don't want to break the
93 # indentation if all the remaining processes have short names.
94 l
= len(self
.display_name
)
97 update_max_display_name_length(l
)
99 def change_state(self
, state
):
100 """Change the state of the remote process, logging the change"""
101 if state
is not self
.state
:
103 self
.print_debug('state => %s' % (STATE_NAMES
[state
]))
106 def disconnect(self
):
107 """We are no more interested in this remote process"""
109 os
.kill(self
.pid
, signal
.SIGKILL
)
111 # The process was already dead, no problem
113 self
.read_buffer
= ''
114 self
.write_buffer
= ''
116 self
.set_enabled(False)
117 if options
.abort_error
and self
.state
is STATE_NOT_STARTED
:
118 raise asyncore
.ExitNow(1)
121 """Relaunch and reconnect to this same remote process"""
124 remote_dispatcher(self
.hostname
)
126 def dispatch_termination(self
):
127 """Start the termination procedure on this remote process, using the
128 same trick as the prompt to hide it"""
129 if not self
.termination
:
130 self
.term1
= '[gsh termination ' + str(random
.random())[2:]
131 self
.term2
= str(random
.random())[2:] + ']'
132 self
.termination
= self
.term1
+ self
.term2
133 self
.dispatch_command('/bin/echo "%s""%s"\n' %
134 (self
.term1
, self
.term2
))
135 if self
.state
is not STATE_NOT_STARTED
:
136 self
.change_state(STATE_RUNNING
)
138 def configure_tty(self
):
139 """We don't want \n to be replaced with \r\n, and we disable the echo"""
140 attr
= termios
.tcgetattr(self
.fd
)
141 attr
[1] &= ~termios
.ONLCR
# oflag
142 attr
[3] &= ~termios
.ECHO
# lflag
143 termios
.tcsetattr(self
.fd
, termios
.TCSANOW
, attr
)
144 # unsetopt zle prevents Zsh from resetting the tty
145 return 'unsetopt zle 2> /dev/null;stty -echo -onlcr;'
147 def set_prompt(self
):
148 """The prompt is important because we detect the readyness of a process
149 by waiting for its prompt. The prompt is built in two parts for it not
150 to appear in its building"""
152 command_line
= 'RPS1=;RPROMPT=;'
153 command_line
+= 'TERM=ansi;'
154 prompt1
= '[gsh prompt ' + str(random
.random())[2:]
155 prompt2
= str(random
.random())[2:] + ']'
156 self
.prompt
= prompt1
+ prompt2
157 command_line
+= 'PS1="%s""%s\n"\n' % (prompt1
, prompt2
)
161 """We are always interested in reading from active remote processes if
163 return self
.active
and buffered_dispatcher
.readable(self
)
165 def handle_error(self
):
166 """An exception may or may not lead to a disconnection"""
167 if buffered_dispatcher
.handle_error(self
):
168 console_output('Error talking to %s\n' % self
.display_name
)
171 def print_lines(self
, lines
):
172 from gsh
.dispatchers
import max_display_name_length
173 lines
= lines
.strip('\n')
175 no_empty_lines
= lines
.replace('\n\n', '\n')
176 if len(no_empty_lines
) == len(lines
):
178 lines
= no_empty_lines
181 indent
= max_display_name_length
- len(self
.display_name
)
182 prefix
= self
.display_name
+ indent
* ' ' + ': '
183 console_output(prefix
+ lines
.replace('\n', '\n' + prefix
) + '\n')
185 def handle_read_fast_case(self
, data
):
186 """If we are in a fast case we'll avoid the long processing of each
188 if self
.prompt
in data
or self
.state
is not STATE_RUNNING
or \
189 self
.termination
and (self
.term1
in data
or self
.term2
in data
) or \
190 self
.pending_rename
and self
.pending_rename
in data
or \
191 self
.file_transfer_cookie
and self
.file_transfer_cookie
in data
:
195 last_nl
= data
.rfind('\n')
197 # No '\n' in data => slow case
199 self
.read_buffer
= data
[last_nl
+ 1:]
200 self
.print_lines(data
[:last_nl
])
203 def handle_read(self
):
204 """We got some output from a remote shell, this is one of the state
208 global nr_handle_read
210 new_data
= buffered_dispatcher
.handle_read(self
)
212 self
.print_debug('==> ' + new_data
)
213 if self
.handle_read_fast_case(self
.read_buffer
):
215 lf_pos
= new_data
.find('\n')
217 # Optimization: we knew there were no '\n' in the previous read
218 # buffer, so we searched only in the new_data and we offset the
219 # found index by the length of the previous buffer
220 lf_pos
+= len(self
.read_buffer
) - len(new_data
)
222 # For each line in the buffer
223 line
= self
.read_buffer
[:lf_pos
+ 1]
224 if self
.prompt
in line
:
225 if options
.interactive
:
226 self
.change_state(STATE_IDLE
)
228 self
.dispatch_command(self
.command
+ '\n')
231 self
.dispatch_termination()
232 elif self
.termination
and self
.termination
in line
:
233 self
.change_state(STATE_TERMINATED
)
235 elif self
.termination
and self
.term1
in line
and self
.term2
in line
:
236 # Just ignore this line
238 elif self
.pending_rename
and self
.pending_rename
in line
:
239 self
.received_rename(line
)
240 elif self
.file_transfer_cookie
and self
.file_transfer_cookie
in line
:
241 file_transfer
.received_cookie(self
, line
)
242 elif self
.state
in (STATE_IDLE
, STATE_RUNNING
):
243 self
.print_lines(line
)
244 elif self
.state
is STATE_NOT_STARTED
:
245 if 'The authenticity of host' in line
:
246 msg
= line
.strip('\n')
247 elif 'REMOTE HOST IDENTIFICATION HAS CHANGED' in line
:
248 msg
= 'Remote host identification has changed.'
253 self
.print_lines(msg
+ ' Closing connection, consider ' +
254 'manually connecting or using ' +
258 # Go to the next line in the buffer
259 self
.read_buffer
= self
.read_buffer
[lf_pos
+ 1:]
260 if self
.handle_read_fast_case(self
.read_buffer
):
262 lf_pos
= self
.read_buffer
.find('\n')
263 if self
.state
is STATE_NOT_STARTED
and not self
.init_string_sent
:
264 self
.dispatch_write(self
.init_string
)
265 self
.init_string_sent
= True
267 def print_unfinished_line(self
):
268 """The unfinished line stayed long enough in the buffer to be printed"""
269 if self
.state
is STATE_RUNNING
:
270 self
.print_lines(self
.read_buffer
)
271 self
.read_buffer
= ''
274 """Do we want to write something?"""
275 return self
.active
and buffered_dispatcher
.writable(self
)
277 def handle_write(self
):
278 """Let's write as much as we can"""
279 num_sent
= self
.send(self
.write_buffer
)
281 self
.print_debug('<== ' + self
.write_buffer
[:num_sent
])
282 self
.write_buffer
= self
.write_buffer
[num_sent
:]
284 def print_debug(self
, msg
):
285 """Log some debugging information to the console"""
286 state
= STATE_NAMES
[self
.state
]
287 msg
= msg
.encode('string_escape')
288 console_output('[dbg] %s[%s]: %s\n' % (self
.display_name
, state
, msg
))
291 """Return a list with all information available about this process"""
293 state
= STATE_NAMES
[self
.state
]
302 return [self
.display_name
, 'fd:%d' % (self
.fd
),
303 'r:%d' % (len(self
.read_buffer
)),
304 'w:%d' % (len(self
.write_buffer
)),
305 self
.active
and 'active' or 'dead',
306 self
.enabled
and 'enabled' or 'disabled',
310 def dispatch_write(self
, buf
):
311 """There is new stuff to write when possible"""
312 if self
.active
and self
.enabled
:
313 buffered_dispatcher
.dispatch_write(self
, buf
)
316 def dispatch_command(self
, command
):
317 if self
.dispatch_write(command
):
318 self
.change_state(STATE_RUNNING
)
320 def change_name(self
, name
):
321 """Change the name of the shell, possibly updating the maximum name
323 from gsh
import dispatchers
326 previous_name_len
= len(self
.display_name
)
327 self
.display_name
= None
328 self
.display_name
= dispatchers
.make_unique_name(name
)
329 dispatchers
.update_max_display_name_length(len(self
.display_name
))
330 dispatchers
.update_max_display_name_length(-previous_name_len
)
332 def rename(self
, string
):
333 """Send to the remote shell, its new name to be shell expanded"""
335 pending_rename1
= str(random
.random())[2:] + ','
336 pending_rename2
= str(random
.random())[2:] + ':'
337 self
.pending_rename
= pending_rename1
+ pending_rename2
338 self
.dispatch_command('/bin/echo "%s""%s" %s\n' %
339 (pending_rename1
, pending_rename2
, string
))
341 self
.change_name(self
.hostname
)
343 def received_rename(self
, line
):
344 """The shell expanded name has been received"""
345 new_name
= line
[len(self
.pending_rename
) + 1:-1]
346 self
.change_name(new_name
)
347 self
.pending_rename
= None
350 return self
.display_name