Merge lp:~ed.so/duplicity/0.6-readd_sshpexpect into lp:duplicity/0.6
- 0.6-readd_sshpexpect
- Merge into 0.6-series
Proposed by
edso
Status: | Merged |
---|---|
Merged at revision: | 851 |
Proposed branch: | lp:~ed.so/duplicity/0.6-readd_sshpexpect |
Merge into: | lp:duplicity/0.6 |
Diff against target: |
3287 lines (+2709/-411) 8 files modified
Changelog.GNU (+0/-21) bin/duplicity.1 (+158/-33) duplicity/backends/ssh_paramiko.py (+361/-0) duplicity/backends/ssh_pexpect.py (+317/-0) duplicity/backends/sshbackend.py (+15/-342) duplicity/commandline.py (+7/-12) duplicity/globals.py (+6/-3) duplicity/pexpect.py (+1845/-0) |
To merge this branch: | bzr merge lp:~ed.so/duplicity/0.6-readd_sshpexpect |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
duplicity-team | Pending | ||
Review via email: mp+97297@code.launchpad.net |
Commit message
Description of the change
- readd ssh pexpect backend as alternative
- added --ssh-backend parameter to switch between paramiko,pexpect
- manpage
-- update to reflect above changes
-- added more backend requirements
- Changelog.GNU removed double entries
To post a comment you must log in.
Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Changelog.GNU' |
2 | --- Changelog.GNU 2012-03-13 11:39:53 +0000 |
3 | +++ Changelog.GNU 2012-03-13 20:58:22 +0000 |
4 | @@ -4,14 +4,6 @@ |
5 | |
6 | add missing_host_key prompt to new sshbackend similar to ssh procedure |
7 | |
8 | -2012-03-12 edso |
9 | - |
10 | - changelog entry |
11 | - |
12 | -2012-03-12 edso |
13 | - |
14 | - add missing_host_key prompt similar to ssh procedure |
15 | - |
16 | 2012-03-13 Kenneth Loafman <kenneth@loafman.com> |
17 | |
18 | Merged in lp:~carlos-abalde/duplicity/gdocs-backend-gdata-2.0.16.-upgrade. |
19 | @@ -28,14 +20,6 @@ |
20 | @Ken: would you please announce that sshbackend is paramiko based native python now in the Changelog for the next release? |
21 | this was missing in 0.6.18's Changelog |
22 | |
23 | -2012-03-08 edso |
24 | - |
25 | - changelog entry |
26 | - |
27 | -2012-03-08 edso |
28 | - |
29 | - add ssh_config support (/etc/ssh/ssh_config + ~/.ssh/config) to paramiko sshbackend |
30 | - |
31 | 2012-03-08 edso <edgar.soldin@web.de> |
32 | |
33 | Merged in lp:~ed.so/duplicity/0.6-webdav_fixes. |
34 | @@ -48,10 +32,6 @@ |
35 | 2012-03-08 Kenneth Loafman <kenneth@loafman.com> |
36 | |
37 | Merged in lp:~ed.so/duplicity/0.6-manpage |
38 | - |
39 | - - added REQUIREMENTS section |
40 | - |
41 | -2012-03-08 edso |
42 | |
43 | - added REQUIREMENTS section |
44 | - restructure SYNOPSIS/ACTIONS to have commands sorted by backup lifecycle |
45 | @@ -82,7 +62,6 @@ |
46 | |
47 | 2012-02-29 kenneth@loafman.com |
48 | |
49 | - Changes for 0.6.18. |
50 | |
51 | 2012-02-29 kenneth@loafman.com |
52 | |
53 | |
54 | === modified file 'bin/duplicity.1' |
55 | --- bin/duplicity.1 2012-03-08 12:59:08 +0000 |
56 | +++ bin/duplicity.1 2012-03-13 20:58:22 +0000 |
57 | @@ -4,13 +4,54 @@ |
58 | duplicity \- Encrypted incremental backup to local or remote storage. |
59 | |
60 | .SH REQUIREMENTS |
61 | -Duplicity requires a POSIX-like operating system. |
62 | +Duplicity requires a POSIX-like operating system with a |
63 | +.B python |
64 | +interpreter version 2.4+ installed. |
65 | It is best used under GNU/Linux. |
66 | |
67 | -Some backends also require additional components: |
68 | -.IP * 2 |
69 | -.B "ssh backend" |
70 | -(scp/sftp/ssh) |
71 | +Some backends also require additional components (probably available as packages for your specific platform): |
72 | +.IP * 2 |
73 | +.B "boto backend" |
74 | +(S3 Amazon Web Services) |
75 | +.RS |
76 | +.IP - 2 |
77 | +.B boto |
78 | +- http://github.com/boto/boto |
79 | +.RE |
80 | +.IP * 2 |
81 | +.B "ftp backend" |
82 | +.RS |
83 | +.IP - 2 |
84 | +.B NcFTP Client |
85 | +- http://www.ncftp.com/ |
86 | +.RE |
87 | +.IP * 2 |
88 | +.B "ftps backend" |
89 | +.RS |
90 | +.IP - 2 |
91 | +.B LFTP Client |
92 | +- http://lftp.yar.ru/ |
93 | +.RE |
94 | +.IP * 2 |
95 | +.B "gio backend" |
96 | +(Gnome VFS API) |
97 | +.RS |
98 | +.IP - 2 |
99 | +.B PyGObject |
100 | +- http://live.gnome.org/PyGObject |
101 | +.IP - 2 |
102 | +.B D-Bus |
103 | +(dbus)- http://www.freedesktop.org/wiki/Software/dbus |
104 | +.RE |
105 | +.IP * 2 |
106 | +.B "ssh backends" |
107 | +(scp/sftp/ssh see |
108 | +.B --ssh-backend |
109 | +) |
110 | +.RS |
111 | +.IP * 2 |
112 | +.B ssh paramiko backend |
113 | +(default) |
114 | .RS |
115 | .IP - 2 |
116 | .B paramiko |
117 | @@ -20,12 +61,12 @@ |
118 | Python Cryptography Toolkit - http://www.dlitz.net/software/pycrypto/ |
119 | .RE |
120 | .IP * 2 |
121 | -.B "boto backend" |
122 | -(S3 Amazon Web Services) |
123 | +.B ssh pexpect backend |
124 | .RS |
125 | .IP - 2 |
126 | -.B boto |
127 | -- http://github.com/boto/boto |
128 | +.B sftp/scp client binaries |
129 | +OpenSSH - http://www.openssh.com/ |
130 | +.RE |
131 | .RE |
132 | |
133 | .SH SYNOPSIS |
134 | @@ -644,13 +685,33 @@ |
135 | |
136 | .TP |
137 | .BI "--scp-command " command |
138 | -Deprecated and ignored. The ssh backend does no longer use an external |
139 | -scp client program. |
140 | +.B (only ssh pexpect backend) |
141 | +The |
142 | +.I command |
143 | +will be used instead of scp to send or receive files. The default command |
144 | +is "scp". To list and delete existing files, the sftp command is used. See |
145 | +.BR --ssh-options |
146 | +and |
147 | +.BR --sftp-command . |
148 | |
149 | .TP |
150 | .BI "--sftp-command " command |
151 | -Deprecated and ignored. The ssh backend does no longer use an external |
152 | -sftp client program. |
153 | +.B (only ssh pexpect backend) |
154 | +The |
155 | +.I command |
156 | +will be used instead of sftp for listing and deleting files. The |
157 | +default is "sftp". File transfers are done using the sftp command. See |
158 | +.BR --ssh-options , |
159 | +.BR --use-scp , |
160 | +and |
161 | +.BR --scp-command . |
162 | + |
163 | +.TP |
164 | +.BI --short-filenames |
165 | +If this option is specified, the names of the files duplicity writes |
166 | +will be shorter (about 30 chars) but less understandable. This may be |
167 | +useful when backing up to MacOS or another OS or FS that doesn't |
168 | +support long filenames. |
169 | |
170 | .TP |
171 | .BI "--sign-key " key |
172 | @@ -675,28 +736,38 @@ |
173 | This password is also used for passphrase-protected ssh keys. |
174 | |
175 | .TP |
176 | +.BI "--ssh-backend " backend |
177 | +Allows the explicit selection of a ssh backend. Defaults to |
178 | +.BR paramiko . |
179 | +Alternatively you might choose |
180 | +.BR pexpect . |
181 | +see also |
182 | +.B "A NOTE ON SSH BACKENDS" |
183 | + |
184 | +.TP |
185 | .BI "--ssh-options " options |
186 | -Allows you to pass options to the ssh/scp/sftp backend. The |
187 | +Allows you to pass options to the ssh backend. The |
188 | .I options |
189 | -list should be of the form "-oopt1=parm1 -oopt2=parm2" where the option string is |
190 | -quoted and the only spaces allowed are between options. Options must |
191 | -be given in the long option format described in |
192 | -.BR ssh_config(5) . |
193 | -The sftp/scp backend currently supports only one ssh option, IdentityFile |
194 | +list should be of the form "-oOpt1=parm1 -oOpt2=parm2" where the option string is |
195 | +quoted and the only spaces allowed are between options. The option string |
196 | +will be passed verbatim to both scp and sftp, whose command line syntax |
197 | +differs slightly hence the options should therefore be given in the long option format described in |
198 | +.BR ssh_config(5) , |
199 | like in this example: |
200 | -.PP |
201 | .RS |
202 | -duplicity --ssh-options="-oIdentityFile=/my/backup/id" /home/me sftp://uid@other.host/some_dir |
203 | .PP |
204 | +.ad l |
205 | +duplicity --ssh-options="-oProtocol=2 -oIdentityFile=/my/backup/id" /home/me scp://uid@other.host/some_dir |
206 | +.TP |
207 | +.B NOTE: |
208 | +.I ssh paramiko backend |
209 | +currently supports only the |
210 | +.nh |
211 | +.B -oIdentityFile |
212 | +.hy |
213 | +setting. |
214 | .RE |
215 | - |
216 | - |
217 | -.TP |
218 | -.BI --short-filenames |
219 | -If this option is specified, the names of the files duplicity writes |
220 | -will be shorter (about 30 chars) but less understandable. This may be |
221 | -useful when backing up to MacOS or another OS or FS that doesn't |
222 | -support long filenames. |
223 | +.ad n |
224 | |
225 | .TP |
226 | .BI "--tempdir " directory |
227 | @@ -740,9 +811,10 @@ |
228 | |
229 | .TP |
230 | .BI --use-scp |
231 | -If this option is specified, then the sftp/scp backend will use the |
232 | -scp protocol rather than sftp for backend operations. The default is to use |
233 | -sftp, because it does not suffer from shell quoting issues like scp. |
234 | +If this option is specified, then the ssh backend will use the |
235 | +scp protocol rather than sftp for backend operations. |
236 | +see also |
237 | +.B "A NOTE ON SSH BACKENDS" |
238 | |
239 | .TP |
240 | .BI "--verbosity " level ", -v" level |
241 | @@ -862,7 +934,10 @@ |
242 | sftp://user[:password]@other.host[:port]/[/]some_dir |
243 | .br |
244 | see also |
245 | -.BI "--use-scp" |
246 | +.BR --ssh-backend , |
247 | +.B --use-scp |
248 | +and |
249 | +.B "A NOTE ON SSH BACKENDS" |
250 | .PP |
251 | tahoe://alias/directory |
252 | .PP |
253 | @@ -1216,6 +1291,56 @@ |
254 | .B from_address_prefix |
255 | will distinguish between different backups. |
256 | |
257 | +.SH A NOTE ON SSH BACKENDS |
258 | +The |
259 | +.I ssh backends |
260 | +support sftp and scp transports. This is a known user-confusing issue. |
261 | +These are fundamentally different. If you plan to access your backend via one |
262 | +of those please inform yourself about the requirements for a server to support |
263 | +scp or sftp. |
264 | +To make it even more confusing the user can choose between two ssh backends |
265 | +see |
266 | +.nh |
267 | +.B --ssh\-backend |
268 | +.hy |
269 | +for details. |
270 | +.PP |
271 | +.BR "SSH paramiko backend " "(selected by default)" |
272 | +is complete reimplementation of ssh protocols natively in python. Advantages |
273 | +are speed and maintainability. Minor disadvantage is that extra packages are |
274 | +needed see |
275 | +.nh |
276 | +.B REQUIREMENTS |
277 | +.hy |
278 | +above. In |
279 | +.I sftp |
280 | +(default) mode all operations are done via the according sftp commands. In |
281 | +.I scp |
282 | +mode ( |
283 | +.I --use-scp |
284 | +) though scp access is used for put/get operations but listing is done via ssh remote shell. |
285 | +.PP |
286 | +.B SSH pexpect backend |
287 | +is the legacy ssh backend using the command line ssh binaries via pexpect. |
288 | +Older versions used |
289 | +.I scp |
290 | +for get and put operations and |
291 | +.I sftp |
292 | +for list and |
293 | +delete operations. The current version uses |
294 | +.I sftp |
295 | +for all four supported |
296 | +operations, unless the |
297 | +.I --use-scp |
298 | +option is used to revert to old behavior. |
299 | +.PP |
300 | +.B Why use sftp instead of scp? |
301 | +The change to sftp was made in order to allow the remote system to chroot the backup, |
302 | +thus providing better security and because it does not suffer from shell quoting issues like scp. |
303 | +Scp also does not support any kind of file listing, so sftp or ssh access will always be needed |
304 | +in addition for this backend mode to work properly. Sftp does not have these limitations but needs |
305 | +an sftp service running on the backend server, which is sometimes not an option. |
306 | + |
307 | .SH A NOTE ON UBUNTU ONE |
308 | Connecting to Ubuntu One requires that you be running duplicity inside of an X |
309 | session so that you can be prompted for your credentials if necessary by the |
310 | |
311 | === added file 'duplicity/backends/ssh_paramiko.py' |
312 | --- duplicity/backends/ssh_paramiko.py 1970-01-01 00:00:00 +0000 |
313 | +++ duplicity/backends/ssh_paramiko.py 2012-03-13 20:58:22 +0000 |
314 | @@ -0,0 +1,361 @@ |
315 | +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- |
316 | +# |
317 | +# Copyright 2002 Ben Escoto <ben@emerose.org> |
318 | +# Copyright 2007 Kenneth Loafman <kenneth@loafman.com> |
319 | +# Copyright 2011 Alexander Zangerl <az@snafu.priv.at> |
320 | +# Copyright 2012 edso (ssh_config added) |
321 | +# |
322 | +# $Id: sshbackend.py,v 1.2 2011/12/31 04:44:12 az Exp $ |
323 | +# |
324 | +# This file is part of duplicity. |
325 | +# |
326 | +# Duplicity is free software; you can redistribute it and/or modify it |
327 | +# under the terms of the GNU General Public License as published by the |
328 | +# Free Software Foundation; either version 2 of the License, or (at your |
329 | +# option) any later version. |
330 | +# |
331 | +# Duplicity is distributed in the hope that it will be useful, but |
332 | +# WITHOUT ANY WARRANTY; without even the implied warranty of |
333 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
334 | +# General Public License for more details. |
335 | +# |
336 | +# You should have received a copy of the GNU General Public License |
337 | +# along with duplicity; if not, write to the Free Software Foundation, |
338 | +# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
339 | + |
340 | +import re |
341 | +import string |
342 | +import os |
343 | +import errno |
344 | +import sys |
345 | +import getpass |
346 | +from binascii import hexlify |
347 | + |
348 | +# debian squeeze's paramiko is a bit old, so we silence randompool depreciation warning |
349 | +# note also: passphrased private keys work with squeeze's paramiko only if done with DES, not AES |
350 | +import warnings |
351 | +warnings.simplefilter("ignore") |
352 | +import paramiko |
353 | +warnings.resetwarnings() |
354 | + |
355 | +import duplicity.backend |
356 | +from duplicity import globals |
357 | +from duplicity import log |
358 | +from duplicity.errors import * |
359 | + |
360 | +read_blocksize=65635 # for doing scp retrievals, where we need to read ourselves |
361 | + |
362 | +class SSHParamikoBackend(duplicity.backend.Backend): |
363 | + """This backend accesses files using the sftp protocol, or scp when the --use-scp option is given. |
364 | + It does not need any local client programs, but an ssh server and the sftp program must be installed on the remote |
365 | + side (or with --use-scp, the programs scp, ls, mkdir, rm and a POSIX-compliant shell). |
366 | + |
367 | + Authentication keys are requested from an ssh agent if present, then ~/.ssh/id_rsa/dsa are tried. |
368 | + If -oIdentityFile=path is present in --ssh-options, then that file is also tried. |
369 | + The passphrase for any of these keys is taken from the URI or FTP_PASSWORD. |
370 | + If none of the above are available, password authentication is attempted (using the URI or FTP_PASSWORD). |
371 | + |
372 | + Missing directories on the remote side will be created. |
373 | + |
374 | + If --use-scp is active then all operations on the remote side require passing arguments through a shell, |
375 | + which introduces unavoidable quoting issues: directory and file names that contain single quotes will not work. |
376 | + This problem does not exist with sftp. |
377 | + """ |
378 | + def __init__(self, parsed_url): |
379 | + duplicity.backend.Backend.__init__(self, parsed_url) |
380 | + |
381 | + if parsed_url.path: |
382 | + # remove first leading '/' |
383 | + self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1) |
384 | + else: |
385 | + self.remote_dir = '.' |
386 | + |
387 | + self.client = paramiko.SSHClient() |
388 | + self.client.set_missing_host_key_policy(AgreedAddPolicy()) |
389 | + # load known_hosts files |
390 | + # paramiko is very picky wrt format and bails out on any problem... |
391 | + try: |
392 | + if os.path.isfile("/etc/ssh/ssh_known_hosts"): |
393 | + self.client.load_system_host_keys("/etc/ssh/ssh_known_hosts") |
394 | + except Exception, e: |
395 | + raise BackendException("could not load /etc/ssh/ssh_known_hosts, maybe corrupt?") |
396 | + try: |
397 | + # use load_host_keys() to signal it's writable to paramiko |
398 | + # load if file exists or add filename to create it if needed |
399 | + file = os.path.expanduser('~/.ssh/known_hosts') |
400 | + if os.path.isfile(file): |
401 | + self.client.load_host_keys(file) |
402 | + else: |
403 | + self.client._host_keys_filename = file |
404 | + except Exception, e: |
405 | + raise BackendException("could not load ~/.ssh/known_hosts, maybe corrupt?") |
406 | + |
407 | + """ the next block reorganizes all host parameters into a |
408 | + dictionary like SSHConfig does. this dictionary 'self.config' |
409 | + becomes the authorative source for these values from here on. |
410 | + rationale is that it is easiest to deal wrt overwriting multiple |
411 | + values from ssh_config file. (ede 03/2012) |
412 | + """ |
413 | + self.config={'hostname':parsed_url.hostname} |
414 | + # get system host config entries |
415 | + self.config.update(self.gethostconfig('/etc/ssh/ssh_config',parsed_url.hostname)) |
416 | + # update with user's config file |
417 | + self.config.update(self.gethostconfig('~/.ssh/config',parsed_url.hostname)) |
418 | + # update with url values |
419 | + ## username from url |
420 | + if parsed_url.username: |
421 | + self.config.update({'user':parsed_url.username}) |
422 | + ## username from input |
423 | + if not 'user' in self.config: |
424 | + self.config.update({'user':getpass.getuser()}) |
425 | + ## port from url |
426 | + if parsed_url.port: |
427 | + self.config.update({'port':parsed_url.port}) |
428 | + ## ensure there is deafult 22 or an int value |
429 | + if 'port' in self.config: |
430 | + self.config.update({'port':int(self.config['port'])}) |
431 | + else: |
432 | + self.config.update({'port':22}) |
433 | + ## alternative ssh private key, identity file |
434 | + m=re.search("-oidentityfile=(\S+)",globals.ssh_options,re.I) |
435 | + if (m!=None): |
436 | + keyfilename=m.group(1) |
437 | + self.config['identityfile'] = keyfilename |
438 | + ## ensure ~ is expanded and identity exists in dictionary |
439 | + if 'identityfile' in self.config: |
440 | + self.config['identityfile'] = os.path.expanduser( |
441 | + self.config['identityfile']) |
442 | + else: |
443 | + self.config['identityfile'] = None |
444 | + |
445 | + # get password, enable prompt if askpass is set |
446 | + self.use_getpass = globals.ssh_askpass |
447 | + ## set url values for beautiful login prompt |
448 | + parsed_url.username = self.config['user'] |
449 | + parsed_url.hostname = self.config['hostname'] |
450 | + password = self.get_password() |
451 | + |
452 | + try: |
453 | + self.client.connect(hostname=self.config['hostname'], |
454 | + port=self.config['port'], |
455 | + username=self.config['user'], |
456 | + password=password, |
457 | + allow_agent=True, |
458 | + look_for_keys=True, |
459 | + key_filename=self.config['identityfile']) |
460 | + except Exception, e: |
461 | + raise BackendException("ssh connection to %s@%s:%d failed: %s" % ( |
462 | + self.config['user'], |
463 | + self.config['hostname'], |
464 | + self.config['port'],e)) |
465 | + self.client.get_transport().set_keepalive((int)(globals.timeout / 2)) |
466 | + |
467 | + # scp or sftp? |
468 | + if (globals.use_scp): |
469 | + # sanity-check the directory name |
470 | + if (re.search("'",self.remote_dir)): |
471 | + raise BackendException("cannot handle directory names with single quotes with --use-scp!") |
472 | + |
473 | + # make directory if needed |
474 | + self.runremote("test -d '%s' || mkdir -p '%s'" % (self.remote_dir,self.remote_dir),False,"scp mkdir ") |
475 | + else: |
476 | + try: |
477 | + self.sftp=self.client.open_sftp() |
478 | + except Exception, e: |
479 | + raise BackendException("sftp negotiation failed: %s" % e) |
480 | + |
481 | + |
482 | + # move to the appropriate directory, possibly after creating it and its parents |
483 | + dirs = self.remote_dir.split(os.sep) |
484 | + if len(dirs) > 0: |
485 | + if not dirs[0]: |
486 | + dirs = dirs[1:] |
487 | + dirs[0]= '/' + dirs[0] |
488 | + for d in dirs: |
489 | + if (d == ''): |
490 | + continue |
491 | + try: |
492 | + attrs=self.sftp.stat(d) |
493 | + except IOError, e: |
494 | + if e.errno == errno.ENOENT: |
495 | + try: |
496 | + self.sftp.mkdir(d) |
497 | + except Exception, e: |
498 | + raise BackendException("sftp mkdir %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
499 | + else: |
500 | + raise BackendException("sftp stat %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
501 | + try: |
502 | + self.sftp.chdir(d) |
503 | + except Exception, e: |
504 | + raise BackendException("sftp chdir to %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
505 | + |
506 | + def put(self, source_path, remote_filename = None): |
507 | + """transfers a single file to the remote side. |
508 | + In scp mode unavoidable quoting issues will make this fail if the remote directory or file name |
509 | + contain single quotes.""" |
510 | + if not remote_filename: |
511 | + remote_filename = source_path.get_filename() |
512 | + if (globals.use_scp): |
513 | + f=file(source_path.name,'rb') |
514 | + try: |
515 | + chan=self.client.get_transport().open_session() |
516 | + chan.settimeout(globals.timeout) |
517 | + chan.exec_command("scp -t '%s'" % self.remote_dir) # scp in sink mode uses the arg as base directory |
518 | + except Exception, e: |
519 | + raise BackendException("scp execution failed: %s" % e) |
520 | + # scp protocol: one 0x0 after startup, one after the Create meta, one after saving |
521 | + # if there's a problem: 0x1 or 0x02 and some error text |
522 | + response=chan.recv(1) |
523 | + if (response!="\0"): |
524 | + raise BackendException("scp remote error: %s" % chan.recv(-1)) |
525 | + fstat=os.stat(source_path.name) |
526 | + chan.send('C%s %d %s\n' %(oct(fstat.st_mode)[-4:], fstat.st_size, remote_filename)) |
527 | + response=chan.recv(1) |
528 | + if (response!="\0"): |
529 | + raise BackendException("scp remote error: %s" % chan.recv(-1)) |
530 | + chan.sendall(f.read()+'\0') |
531 | + f.close() |
532 | + response=chan.recv(1) |
533 | + if (response!="\0"): |
534 | + raise BackendException("scp remote error: %s" % chan.recv(-1)) |
535 | + chan.close() |
536 | + else: |
537 | + try: |
538 | + self.sftp.put(source_path.name,remote_filename) |
539 | + except Exception, e: |
540 | + raise BackendException("sftp put of %s (as %s) failed: %s" % (source_path.name,remote_filename,e)) |
541 | + |
542 | + |
543 | + def get(self, remote_filename, local_path): |
544 | + """retrieves a single file from the remote side. |
545 | + In scp mode unavoidable quoting issues will make this fail if the remote directory or file names |
546 | + contain single quotes.""" |
547 | + if (globals.use_scp): |
548 | + try: |
549 | + chan=self.client.get_transport().open_session() |
550 | + chan.settimeout(globals.timeout) |
551 | + chan.exec_command("scp -f '%s/%s'" % (self.remote_dir,remote_filename)) |
552 | + except Exception, e: |
553 | + raise BackendException("scp execution failed: %s" % e) |
554 | + |
555 | + chan.send('\0') # overall ready indicator |
556 | + msg=chan.recv(-1) |
557 | + m=re.match(r"C([0-7]{4})\s+(\d+)\s+(\S.*)$",msg) |
558 | + if (m==None or m.group(3)!=remote_filename): |
559 | + raise BackendException("scp get %s failed: incorrect response '%s'" % (remote_filename,msg)) |
560 | + chan.recv(1) # dispose of the newline trailing the C message |
561 | + |
562 | + size=int(m.group(2)) |
563 | + togo=size |
564 | + f=file(local_path.name,'wb') |
565 | + chan.send('\0') # ready for data |
566 | + try: |
567 | + while togo>0: |
568 | + if togo>read_blocksize: |
569 | + blocksize = read_blocksize |
570 | + else: |
571 | + blocksize = togo |
572 | + buff=chan.recv(blocksize) |
573 | + f.write(buff) |
574 | + togo-=len(buff) |
575 | + except Exception, e: |
576 | + raise BackendException("scp get %s failed: %s" % (remote_filename,e)) |
577 | + |
578 | + msg=chan.recv(1) # check the final status |
579 | + if msg!='\0': |
580 | + raise BackendException("scp get %s failed: %s" % (remote_filename,chan.recv(-1))) |
581 | + f.close() |
582 | + chan.send('\0') # send final done indicator |
583 | + chan.close() |
584 | + else: |
585 | + try: |
586 | + self.sftp.get(remote_filename,local_path.name) |
587 | + except Exception, e: |
588 | + raise BackendException("sftp get of %s (to %s) failed: %s" % (remote_filename,local_path.name,e)) |
589 | + local_path.setdata() |
590 | + |
591 | + def list(self): |
592 | + """lists the contents of the one-and-only duplicity dir on the remote side. |
593 | + In scp mode unavoidable quoting issues will make this fail if the directory name |
594 | + contains single quotes.""" |
595 | + if (globals.use_scp): |
596 | + output=self.runremote("ls -1 '%s'" % self.remote_dir,False,"scp dir listing ") |
597 | + return output.splitlines() |
598 | + else: |
599 | + try: |
600 | + return self.sftp.listdir() |
601 | + except Exception, e: |
602 | + raise BackendException("sftp listing of %s failed: %s" % (self.sftp.getcwd(),e)) |
603 | + |
604 | + def delete(self, filename_list): |
605 | + """deletes all files in the list on the remote side. In scp mode unavoidable quoting issues |
606 | + will cause failures if filenames containing single quotes are encountered.""" |
607 | + for fn in filename_list: |
608 | + if (globals.use_scp): |
609 | + self.runremote("rm '%s/%s'" % (self.remote_dir,fn),False,"scp rm ") |
610 | + else: |
611 | + try: |
612 | + self.sftp.remove(fn) |
613 | + except Exception, e: |
614 | + raise BackendException("sftp rm %s failed: %s" % (fn,e)) |
615 | + |
616 | + def runremote(self,cmd,ignoreexitcode=False,errorprefix=""): |
617 | + """small convenience function that opens a shell channel, runs remote command and returns |
618 | + stdout of command. throws an exception if exit code!=0 and not ignored""" |
619 | + try: |
620 | + chan=self.client.get_transport().open_session() |
621 | + chan.settimeout(globals.timeout) |
622 | + chan.exec_command(cmd) |
623 | + except Exception, e: |
624 | + raise BackendException("%sexecution failed: %s" % (errorprefix,e)) |
625 | + output=chan.recv(-1) |
626 | + res=chan.recv_exit_status() |
627 | + if (res!=0 and not ignoreexitcode): |
628 | + raise BackendException("%sfailed(%d): %s" % (errorprefix,res,chan.recv_stderr(4096))) |
629 | + return output |
630 | + |
631 | + def gethostconfig(self, file, host): |
632 | + file = os.path.expanduser(file) |
633 | + if not os.path.isfile(file): |
634 | + return {} |
635 | + |
636 | + sshconfig = paramiko.SSHConfig() |
637 | + try: |
638 | + sshconfig.parse(open(file)) |
639 | + except Exception, e: |
640 | + raise BackendException("could not load '%s', maybe corrupt?" % (file)) |
641 | + |
642 | + return sshconfig.lookup(host) |
643 | + |
644 | +class AgreedAddPolicy (paramiko.AutoAddPolicy): |
645 | + """ |
646 | + Policy for showing a yes/no prompt and adding the hostname and new |
647 | + host key to the known host file accordingly. |
648 | + |
649 | + This class simply extends the AutoAddPolicy class with a yes/no prompt. |
650 | + """ |
651 | + def missing_host_key(self, client, hostname, key): |
652 | + fp = hexlify(key.get_fingerprint()) |
653 | + fingerprint = ':'.join(a+b for a,b in zip(fp[::2], fp[1::2])) |
654 | + question = """The authenticity of host '%s' can't be established. |
655 | +%s key fingerprint is %s. |
656 | +Are you sure you want to continue connecting (yes/no)? """ % (hostname, key.get_name().upper(), fingerprint) |
657 | + while True: |
658 | + sys.stdout.write(question) |
659 | + choice = raw_input().lower() |
660 | + if choice in ['yes','y']: |
661 | + super(AgreedAddPolicy, self).missing_host_key(client, hostname, key) |
662 | + return |
663 | + elif choice in ['no','n']: |
664 | + raise AuthenticityException( hostname ) |
665 | + else: |
666 | + question = "Please type 'yes' or 'no': " |
667 | + |
668 | +class AuthenticityException (paramiko.SSHException): |
669 | + def __init__(self, hostname): |
670 | + paramiko.SSHException.__init__(self, 'Host key verification for server %s failed.' % hostname) |
671 | + |
672 | + |
673 | +duplicity.backend.register_backend("sftp", SSHParamikoBackend) |
674 | +duplicity.backend.register_backend("scp", SSHParamikoBackend) |
675 | +duplicity.backend.register_backend("ssh", SSHParamikoBackend) |
676 | |
677 | === added file 'duplicity/backends/ssh_pexpect.py' |
678 | --- duplicity/backends/ssh_pexpect.py 1970-01-01 00:00:00 +0000 |
679 | +++ duplicity/backends/ssh_pexpect.py 2012-03-13 20:58:22 +0000 |
680 | @@ -0,0 +1,317 @@ |
681 | +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- |
682 | +# |
683 | +# Copyright 2002 Ben Escoto <ben@emerose.org> |
684 | +# Copyright 2007 Kenneth Loafman <kenneth@loafman.com> |
685 | +# |
686 | +# This file is part of duplicity. |
687 | +# |
688 | +# Duplicity is free software; you can redistribute it and/or modify it |
689 | +# under the terms of the GNU General Public License as published by the |
690 | +# Free Software Foundation; either version 2 of the License, or (at your |
691 | +# option) any later version. |
692 | +# |
693 | +# Duplicity is distributed in the hope that it will be useful, but |
694 | +# WITHOUT ANY WARRANTY; without even the implied warranty of |
695 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
696 | +# General Public License for more details. |
697 | +# |
698 | +# You should have received a copy of the GNU General Public License |
699 | +# along with duplicity; if not, write to the Free Software Foundation, |
700 | +# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
701 | + |
702 | +# The following can be redefined to use different shell commands from |
703 | +# ssh or scp or to add more arguments. However, the replacements must |
704 | +# have the same syntax. Also these strings will be executed by the |
705 | +# shell, so shouldn't have strange characters in them. |
706 | + |
707 | +import re |
708 | +import string |
709 | +import time |
710 | +import os |
711 | + |
712 | +import duplicity.backend |
713 | +from duplicity import globals |
714 | +from duplicity import log |
715 | +from duplicity import pexpect |
716 | +from duplicity.errors import * #@UnusedWildImport |
717 | + |
718 | +class SSHPExpectBackend(duplicity.backend.Backend): |
719 | + """This backend copies files using scp. List not supported""" |
720 | + def __init__(self, parsed_url): |
721 | + """scpBackend initializer""" |
722 | + duplicity.backend.Backend.__init__(self, parsed_url) |
723 | + |
724 | + self.scp_command = "scp" |
725 | + if globals.scp_command: self.scp_command = globals.scp_command |
726 | + |
727 | + self.sftp_command = "sftp" |
728 | + if globals.sftp_command: self.sftp_command = globals.sftp_command |
729 | + |
730 | + # host string of form [user@]hostname |
731 | + if parsed_url.username: |
732 | + self.host_string = parsed_url.username + "@" + parsed_url.hostname |
733 | + else: |
734 | + self.host_string = parsed_url.hostname |
735 | + # make sure remote_dir is always valid |
736 | + if parsed_url.path: |
737 | + # remove leading '/' |
738 | + self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1) |
739 | + else: |
740 | + self.remote_dir = '.' |
741 | + self.remote_prefix = self.remote_dir + '/' |
742 | + # maybe use different ssh port |
743 | + if parsed_url.port: |
744 | + globals.ssh_options = globals.ssh_options + " -oPort=%s" % parsed_url.port |
745 | + # set some defaults if user has not specified already. |
746 | + if "ServerAliveInterval" not in globals.ssh_options: |
747 | + globals.ssh_options += " -oServerAliveInterval=%d" % ((int)(globals.timeout / 2)) |
748 | + if "ServerAliveCountMax" not in globals.ssh_options: |
749 | + globals.ssh_options += " -oServerAliveCountMax=2" |
750 | + # set up password |
751 | + if globals.ssh_askpass: |
752 | + self.password = self.get_password() |
753 | + else: |
754 | + if parsed_url.password: |
755 | + self.password = parsed_url.password |
756 | + globals.ssh_askpass = True |
757 | + else: |
758 | + self.password = '' |
759 | + |
760 | + def run_scp_command(self, commandline): |
761 | + """ Run an scp command, responding to password prompts """ |
762 | + for n in range(1, globals.num_retries+1): |
763 | + if n > 1: |
764 | + # sleep before retry |
765 | + time.sleep(30) |
766 | + log.Info("Running '%s' (attempt #%d)" % (commandline, n)) |
767 | + child = pexpect.spawn(commandline, timeout = None) |
768 | + if globals.ssh_askpass: |
769 | + state = "authorizing" |
770 | + else: |
771 | + state = "copying" |
772 | + while 1: |
773 | + if state == "authorizing": |
774 | + match = child.expect([pexpect.EOF, |
775 | + "(?i)timeout, server not responding", |
776 | + "(?i)pass(word|phrase .*):", |
777 | + "(?i)permission denied", |
778 | + "authenticity"]) |
779 | + log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) |
780 | + if match == 0: |
781 | + log.Warn("Failed to authenticate") |
782 | + break |
783 | + elif match == 1: |
784 | + log.Warn("Timeout waiting to authenticate") |
785 | + break |
786 | + elif match == 2: |
787 | + child.sendline(self.password) |
788 | + state = "copying" |
789 | + elif match == 3: |
790 | + log.Warn("Invalid SSH password") |
791 | + break |
792 | + elif match == 4: |
793 | + log.Warn("Remote host authentication failed (missing known_hosts entry?)") |
794 | + break |
795 | + elif state == "copying": |
796 | + match = child.expect([pexpect.EOF, |
797 | + "(?i)timeout, server not responding", |
798 | + "stalled", |
799 | + "authenticity", |
800 | + "ETA"]) |
801 | + log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) |
802 | + if match == 0: |
803 | + break |
804 | + elif match == 1: |
805 | + log.Warn("Timeout waiting for response") |
806 | + break |
807 | + elif match == 2: |
808 | + state = "stalled" |
809 | + elif match == 3: |
810 | + log.Warn("Remote host authentication failed (missing known_hosts entry?)") |
811 | + break |
812 | + elif state == "stalled": |
813 | + match = child.expect([pexpect.EOF, |
814 | + "(?i)timeout, server not responding", |
815 | + "ETA"]) |
816 | + log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) |
817 | + if match == 0: |
818 | + break |
819 | + elif match == 1: |
820 | + log.Warn("Stalled for too long, aborted copy") |
821 | + break |
822 | + elif match == 2: |
823 | + state = "copying" |
824 | + child.close(force = True) |
825 | + if child.exitstatus == 0: |
826 | + return |
827 | + log.Warn("Running '%s' failed (attempt #%d)" % (commandline, n)) |
828 | + log.Warn("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries)) |
829 | + raise BackendException("Error running '%s'" % commandline) |
830 | + |
831 | + def run_sftp_command(self, commandline, commands): |
832 | + """ Run an sftp command, responding to password prompts, passing commands from list """ |
833 | + maxread = 2000 # expected read buffer size |
834 | + responses = [pexpect.EOF, |
835 | + "(?i)timeout, server not responding", |
836 | + "sftp>", |
837 | + "(?i)pass(word|phrase .*):", |
838 | + "(?i)permission denied", |
839 | + "authenticity", |
840 | + "(?i)no such file or directory", |
841 | + "Couldn't delete file: No such file or directory", |
842 | + "Couldn't delete file", |
843 | + "open(.*): Failure"] |
844 | + max_response_len = max([len(p) for p in responses[1:]]) |
845 | + for n in range(1, globals.num_retries+1): |
846 | + if n > 1: |
847 | + # sleep before retry |
848 | + time.sleep(30) |
849 | + log.Info("Running '%s' (attempt #%d)" % (commandline, n)) |
850 | + child = pexpect.spawn(commandline, timeout = None, maxread=maxread) |
851 | + cmdloc = 0 |
852 | + while 1: |
853 | + match = child.expect(responses, |
854 | + searchwindowsize=maxread+max_response_len) |
855 | + log.Debug("State = sftp, Before = '%s'" % (child.before.strip())) |
856 | + if match == 0: |
857 | + break |
858 | + elif match == 1: |
859 | + log.Info("Timeout waiting for response") |
860 | + break |
861 | + if match == 2: |
862 | + if cmdloc < len(commands): |
863 | + command = commands[cmdloc] |
864 | + log.Info("sftp command: '%s'" % (command,)) |
865 | + child.sendline(command) |
866 | + cmdloc += 1 |
867 | + else: |
868 | + command = 'quit' |
869 | + child.sendline(command) |
870 | + res = child.before |
871 | + elif match == 3: |
872 | + child.sendline(self.password) |
873 | + elif match == 4: |
874 | + if not child.before.strip().startswith("mkdir"): |
875 | + log.Warn("Invalid SSH password") |
876 | + break |
877 | + elif match == 5: |
878 | + log.Warn("Host key authenticity could not be verified (missing known_hosts entry?)") |
879 | + break |
880 | + elif match == 6: |
881 | + if not child.before.strip().startswith("rm"): |
882 | + log.Warn("Remote file or directory does not exist in command='%s'" % (commandline,)) |
883 | + break |
884 | + elif match == 7: |
885 | + if not child.before.strip().startswith("Removing"): |
886 | + log.Warn("Could not delete file in command='%s'" % (commandline,)) |
887 | + break; |
888 | + elif match == 8: |
889 | + log.Warn("Could not delete file in command='%s'" % (commandline,)) |
890 | + break |
891 | + elif match == 9: |
892 | + log.Warn("Could not open file in command='%s'" % (commandline,)) |
893 | + break |
894 | + child.close(force = True) |
895 | + if child.exitstatus == 0: |
896 | + return res |
897 | + log.Warn("Running '%s' failed (attempt #%d)" % (commandline, n)) |
898 | + log.Warn("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries)) |
899 | + raise BackendException("Error running '%s'" % commandline) |
900 | + |
901 | + def put(self, source_path, remote_filename = None): |
902 | + if globals.use_scp: |
903 | + self.put_scp(source_path, remote_filename = remote_filename) |
904 | + else: |
905 | + self.put_sftp(source_path, remote_filename = remote_filename) |
906 | + |
907 | + def put_sftp(self, source_path, remote_filename = None): |
908 | + """Use sftp to copy source_dir/filename to remote computer""" |
909 | + if not remote_filename: |
910 | + remote_filename = source_path.get_filename() |
911 | + commands = ["put \"%s\" \"%s.%s.part\"" % |
912 | + (source_path.name, self.remote_prefix, remote_filename), |
913 | + "rename \"%s.%s.part\" \"%s%s\"" % |
914 | + (self.remote_prefix, remote_filename,self.remote_prefix, remote_filename)] |
915 | + commandline = ("%s %s %s" % (self.sftp_command, |
916 | + globals.ssh_options, |
917 | + self.host_string)) |
918 | + self.run_sftp_command(commandline, commands) |
919 | + |
920 | + def put_scp(self, source_path, remote_filename = None): |
921 | + """Use scp to copy source_dir/filename to remote computer""" |
922 | + if not remote_filename: |
923 | + remote_filename = source_path.get_filename() |
924 | + commandline = "%s %s %s %s:%s%s" % \ |
925 | + (self.scp_command, globals.ssh_options, source_path.name, self.host_string, |
926 | + self.remote_prefix, remote_filename) |
927 | + self.run_scp_command(commandline) |
928 | + |
929 | + def get(self, remote_filename, local_path): |
930 | + if globals.use_scp: |
931 | + self.get_scp(remote_filename, local_path) |
932 | + else: |
933 | + self.get_sftp(remote_filename, local_path) |
934 | + |
935 | + def get_sftp(self, remote_filename, local_path): |
936 | + """Use sftp to get a remote file""" |
937 | + commands = ["get \"%s%s\" \"%s\"" % |
938 | + (self.remote_prefix, remote_filename, local_path.name)] |
939 | + commandline = ("%s %s %s" % (self.sftp_command, |
940 | + globals.ssh_options, |
941 | + self.host_string)) |
942 | + self.run_sftp_command(commandline, commands) |
943 | + local_path.setdata() |
944 | + if not local_path.exists(): |
945 | + raise BackendException("File %s not found locally after get " |
946 | + "from backend" % local_path.name) |
947 | + |
948 | + def get_scp(self, remote_filename, local_path): |
949 | + """Use scp to get a remote file""" |
950 | + commandline = "%s %s %s:%s%s %s" % \ |
951 | + (self.scp_command, globals.ssh_options, self.host_string, self.remote_prefix, |
952 | + remote_filename, local_path.name) |
953 | + self.run_scp_command(commandline) |
954 | + local_path.setdata() |
955 | + if not local_path.exists(): |
956 | + raise BackendException("File %s not found locally after get " |
957 | + "from backend" % local_path.name) |
958 | + |
959 | + def list(self): |
960 | + """ |
961 | + List files available for scp |
962 | + |
963 | + Note that this command can get confused when dealing with |
964 | + files with newlines in them, as the embedded newlines cannot |
965 | + be distinguished from the file boundaries. |
966 | + """ |
967 | + dirs = self.remote_dir.split(os.sep) |
968 | + if len(dirs) > 0: |
969 | + if not dirs[0] : |
970 | + dirs = dirs[1:] |
971 | + dirs[0]= '/' + dirs[0] |
972 | + mkdir_commands = []; |
973 | + for d in dirs: |
974 | + mkdir_commands += ["mkdir \"%s\"" % (d)] + ["cd \"%s\"" % (d)] |
975 | + |
976 | + commands = mkdir_commands + ["ls -1"] |
977 | + commandline = ("%s %s %s" % (self.sftp_command, |
978 | + globals.ssh_options, |
979 | + self.host_string)) |
980 | + |
981 | + l = self.run_sftp_command(commandline, commands).split('\n')[1:] |
982 | + |
983 | + return filter(lambda x: x, map(string.strip, l)) |
984 | + |
985 | + def delete(self, filename_list): |
986 | + """ |
987 | + Runs sftp rm to delete files. Files must not require quoting. |
988 | + """ |
989 | + commands = ["cd \"%s\"" % (self.remote_dir,)] |
990 | + for fn in filename_list: |
991 | + commands.append("rm \"%s\"" % fn) |
992 | + commandline = ("%s %s %s" % (self.sftp_command, globals.ssh_options, self.host_string)) |
993 | + self.run_sftp_command(commandline, commands) |
994 | + |
995 | +duplicity.backend.register_backend("ssh", SSHPExpectBackend) |
996 | +duplicity.backend.register_backend("scp", SSHPExpectBackend) |
997 | +duplicity.backend.register_backend("sftp", SSHPExpectBackend) |
998 | |
999 | === modified file 'duplicity/backends/sshbackend.py' |
1000 | --- duplicity/backends/sshbackend.py 2012-03-12 18:29:42 +0000 |
1001 | +++ duplicity/backends/sshbackend.py 2012-03-13 20:58:22 +0000 |
1002 | @@ -1,11 +1,6 @@ |
1003 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- |
1004 | # |
1005 | -# Copyright 2002 Ben Escoto <ben@emerose.org> |
1006 | -# Copyright 2007 Kenneth Loafman <kenneth@loafman.com> |
1007 | -# Copyright 2011 Alexander Zangerl <az@snafu.priv.at> |
1008 | -# Copyright 2012 edso (ssh_config added) |
1009 | -# |
1010 | -# $Id: sshbackend.py,v 1.2 2011/12/31 04:44:12 az Exp $ |
1011 | +# Copyright 2012 edso |
1012 | # |
1013 | # This file is part of duplicity. |
1014 | # |
1015 | @@ -23,339 +18,17 @@ |
1016 | # along with duplicity; if not, write to the Free Software Foundation, |
1017 | # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
1018 | |
1019 | -import re |
1020 | -import string |
1021 | -import os |
1022 | -import errno |
1023 | -import sys |
1024 | -import getpass |
1025 | -from binascii import hexlify |
1026 | - |
1027 | -# debian squeeze's paramiko is a bit old, so we silence randompool depreciation warning |
1028 | -# note also: passphrased private keys work with squeeze's paramiko only if done with DES, not AES |
1029 | -import warnings |
1030 | -warnings.simplefilter("ignore") |
1031 | -import paramiko |
1032 | -warnings.resetwarnings() |
1033 | - |
1034 | -import duplicity.backend |
1035 | -from duplicity import globals |
1036 | -from duplicity import log |
1037 | -from duplicity.errors import * |
1038 | - |
1039 | -read_blocksize=65635 # for doing scp retrievals, where we need to read ourselves |
1040 | - |
1041 | -class SftpBackend(duplicity.backend.Backend): |
1042 | - """This backend accesses files using the sftp protocol, or scp when the --use-scp option is given. |
1043 | - It does not need any local client programs, but an ssh server and the sftp program must be installed on the remote |
1044 | - side (or with --use-scp, the programs scp, ls, mkdir, rm and a POSIX-compliant shell). |
1045 | - |
1046 | - Authentication keys are requested from an ssh agent if present, then ~/.ssh/id_rsa/dsa are tried. |
1047 | - If -oIdentityFile=path is present in --ssh-options, then that file is also tried. |
1048 | - The passphrase for any of these keys is taken from the URI or FTP_PASSWORD. |
1049 | - If none of the above are available, password authentication is attempted (using the URI or FTP_PASSWORD). |
1050 | - |
1051 | - Missing directories on the remote side will be created. |
1052 | - |
1053 | - If --use-scp is active then all operations on the remote side require passing arguments through a shell, |
1054 | - which introduces unavoidable quoting issues: directory and file names that contain single quotes will not work. |
1055 | - This problem does not exist with sftp. |
1056 | - """ |
1057 | - def __init__(self, parsed_url): |
1058 | - duplicity.backend.Backend.__init__(self, parsed_url) |
1059 | - |
1060 | - if parsed_url.path: |
1061 | - # remove first leading '/' |
1062 | - self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1) |
1063 | - else: |
1064 | - self.remote_dir = '.' |
1065 | - |
1066 | - self.client = paramiko.SSHClient() |
1067 | - self.client.set_missing_host_key_policy(AgreedAddPolicy()) |
1068 | - # load known_hosts files |
1069 | - # paramiko is very picky wrt format and bails out on any problem... |
1070 | - try: |
1071 | - if os.path.isfile("/etc/ssh/ssh_known_hosts"): |
1072 | - self.client.load_system_host_keys("/etc/ssh/ssh_known_hosts") |
1073 | - except Exception, e: |
1074 | - raise BackendException("could not load /etc/ssh/ssh_known_hosts, maybe corrupt?") |
1075 | - try: |
1076 | - # use load_host_keys() to signal it's writable to paramiko |
1077 | - # load if file exists or add filename to create it if needed |
1078 | - file = os.path.expanduser('~/.ssh/known_hosts') |
1079 | - if os.path.isfile(file): |
1080 | - self.client.load_host_keys(file) |
1081 | - else: |
1082 | - self.client._host_keys_filename = file |
1083 | - except Exception, e: |
1084 | - raise BackendException("could not load ~/.ssh/known_hosts, maybe corrupt?") |
1085 | - |
1086 | - """ the next block reorganizes all host parameters into a |
1087 | - dictionary like SSHConfig does. this dictionary 'self.config' |
1088 | - becomes the authorative source for these values from here on. |
1089 | - rationale is that it is easiest to deal wrt overwriting multiple |
1090 | - values from ssh_config file. (ede 03/2012) |
1091 | - """ |
1092 | - self.config={'hostname':parsed_url.hostname} |
1093 | - # get system host config entries |
1094 | - self.config.update(self.gethostconfig('/etc/ssh/ssh_config',parsed_url.hostname)) |
1095 | - # update with user's config file |
1096 | - self.config.update(self.gethostconfig('~/.ssh/config',parsed_url.hostname)) |
1097 | - # update with url values |
1098 | - ## username from url |
1099 | - if parsed_url.username: |
1100 | - self.config.update({'user':parsed_url.username}) |
1101 | - ## username from input |
1102 | - if not 'user' in self.config: |
1103 | - self.config.update({'user':getpass.getuser()}) |
1104 | - ## port from url |
1105 | - if parsed_url.port: |
1106 | - self.config.update({'port':parsed_url.port}) |
1107 | - ## ensure there is deafult 22 or an int value |
1108 | - if 'port' in self.config: |
1109 | - self.config.update({'port':int(self.config['port'])}) |
1110 | - else: |
1111 | - self.config.update({'port':22}) |
1112 | - ## alternative ssh private key, identity file |
1113 | - m=re.search("-oidentityfile=(\S+)",globals.ssh_options,re.I) |
1114 | - if (m!=None): |
1115 | - keyfilename=m.group(1) |
1116 | - self.config['identityfile'] = keyfilename |
1117 | - ## ensure ~ is expanded and identity exists in dictionary |
1118 | - if 'identityfile' in self.config: |
1119 | - self.config['identityfile'] = os.path.expanduser( |
1120 | - self.config['identityfile']) |
1121 | - else: |
1122 | - self.config['identityfile'] = None |
1123 | - |
1124 | - # get password, enable prompt if askpass is set |
1125 | - self.use_getpass = globals.ssh_askpass |
1126 | - ## set url values for beautiful login prompt |
1127 | - parsed_url.username = self.config['user'] |
1128 | - parsed_url.hostname = self.config['hostname'] |
1129 | - password = self.get_password() |
1130 | - |
1131 | - try: |
1132 | - self.client.connect(hostname=self.config['hostname'], |
1133 | - port=self.config['port'], |
1134 | - username=self.config['user'], |
1135 | - password=password, |
1136 | - allow_agent=True, |
1137 | - look_for_keys=True, |
1138 | - key_filename=self.config['identityfile']) |
1139 | - except Exception, e: |
1140 | - raise BackendException("ssh connection to %s@%s:%d failed: %s" % ( |
1141 | - self.config['user'], |
1142 | - self.config['hostname'], |
1143 | - self.config['port'],e)) |
1144 | - self.client.get_transport().set_keepalive((int)(globals.timeout / 2)) |
1145 | - |
1146 | - # scp or sftp? |
1147 | - if (globals.use_scp): |
1148 | - # sanity-check the directory name |
1149 | - if (re.search("'",self.remote_dir)): |
1150 | - raise BackendException("cannot handle directory names with single quotes with --use-scp!") |
1151 | - |
1152 | - # make directory if needed |
1153 | - self.runremote("test -d '%s' || mkdir -p '%s'" % (self.remote_dir,self.remote_dir),False,"scp mkdir ") |
1154 | - else: |
1155 | - try: |
1156 | - self.sftp=self.client.open_sftp() |
1157 | - except Exception, e: |
1158 | - raise BackendException("sftp negotiation failed: %s" % e) |
1159 | - |
1160 | - |
1161 | - # move to the appropriate directory, possibly after creating it and its parents |
1162 | - dirs = self.remote_dir.split(os.sep) |
1163 | - if len(dirs) > 0: |
1164 | - if not dirs[0]: |
1165 | - dirs = dirs[1:] |
1166 | - dirs[0]= '/' + dirs[0] |
1167 | - for d in dirs: |
1168 | - if (d == ''): |
1169 | - continue |
1170 | - try: |
1171 | - attrs=self.sftp.stat(d) |
1172 | - except IOError, e: |
1173 | - if e.errno == errno.ENOENT: |
1174 | - try: |
1175 | - self.sftp.mkdir(d) |
1176 | - except Exception, e: |
1177 | - raise BackendException("sftp mkdir %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
1178 | - else: |
1179 | - raise BackendException("sftp stat %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
1180 | - try: |
1181 | - self.sftp.chdir(d) |
1182 | - except Exception, e: |
1183 | - raise BackendException("sftp chdir to %s failed: %s" % (self.sftp.normalize(".")+"/"+d,e)) |
1184 | - |
1185 | - def put(self, source_path, remote_filename = None): |
1186 | - """transfers a single file to the remote side. |
1187 | - In scp mode unavoidable quoting issues will make this fail if the remote directory or file name |
1188 | - contain single quotes.""" |
1189 | - if not remote_filename: |
1190 | - remote_filename = source_path.get_filename() |
1191 | - if (globals.use_scp): |
1192 | - f=file(source_path.name,'rb') |
1193 | - try: |
1194 | - chan=self.client.get_transport().open_session() |
1195 | - chan.settimeout(globals.timeout) |
1196 | - chan.exec_command("scp -t '%s'" % self.remote_dir) # scp in sink mode uses the arg as base directory |
1197 | - except Exception, e: |
1198 | - raise BackendException("scp execution failed: %s" % e) |
1199 | - # scp protocol: one 0x0 after startup, one after the Create meta, one after saving |
1200 | - # if there's a problem: 0x1 or 0x02 and some error text |
1201 | - response=chan.recv(1) |
1202 | - if (response!="\0"): |
1203 | - raise BackendException("scp remote error: %s" % chan.recv(-1)) |
1204 | - fstat=os.stat(source_path.name) |
1205 | - chan.send('C%s %d %s\n' %(oct(fstat.st_mode)[-4:], fstat.st_size, remote_filename)) |
1206 | - response=chan.recv(1) |
1207 | - if (response!="\0"): |
1208 | - raise BackendException("scp remote error: %s" % chan.recv(-1)) |
1209 | - chan.sendall(f.read()+'\0') |
1210 | - f.close() |
1211 | - response=chan.recv(1) |
1212 | - if (response!="\0"): |
1213 | - raise BackendException("scp remote error: %s" % chan.recv(-1)) |
1214 | - chan.close() |
1215 | - else: |
1216 | - try: |
1217 | - self.sftp.put(source_path.name,remote_filename) |
1218 | - except Exception, e: |
1219 | - raise BackendException("sftp put of %s (as %s) failed: %s" % (source_path.name,remote_filename,e)) |
1220 | - |
1221 | - |
1222 | - def get(self, remote_filename, local_path): |
1223 | - """retrieves a single file from the remote side. |
1224 | - In scp mode unavoidable quoting issues will make this fail if the remote directory or file names |
1225 | - contain single quotes.""" |
1226 | - if (globals.use_scp): |
1227 | - try: |
1228 | - chan=self.client.get_transport().open_session() |
1229 | - chan.settimeout(globals.timeout) |
1230 | - chan.exec_command("scp -f '%s/%s'" % (self.remote_dir,remote_filename)) |
1231 | - except Exception, e: |
1232 | - raise BackendException("scp execution failed: %s" % e) |
1233 | - |
1234 | - chan.send('\0') # overall ready indicator |
1235 | - msg=chan.recv(-1) |
1236 | - m=re.match(r"C([0-7]{4})\s+(\d+)\s+(\S.*)$",msg) |
1237 | - if (m==None or m.group(3)!=remote_filename): |
1238 | - raise BackendException("scp get %s failed: incorrect response '%s'" % (remote_filename,msg)) |
1239 | - chan.recv(1) # dispose of the newline trailing the C message |
1240 | - |
1241 | - size=int(m.group(2)) |
1242 | - togo=size |
1243 | - f=file(local_path.name,'wb') |
1244 | - chan.send('\0') # ready for data |
1245 | - try: |
1246 | - while togo>0: |
1247 | - if togo>read_blocksize: |
1248 | - blocksize = read_blocksize |
1249 | - else: |
1250 | - blocksize = togo |
1251 | - buff=chan.recv(blocksize) |
1252 | - f.write(buff) |
1253 | - togo-=len(buff) |
1254 | - except Exception, e: |
1255 | - raise BackendException("scp get %s failed: %s" % (remote_filename,e)) |
1256 | - |
1257 | - msg=chan.recv(1) # check the final status |
1258 | - if msg!='\0': |
1259 | - raise BackendException("scp get %s failed: %s" % (remote_filename,chan.recv(-1))) |
1260 | - f.close() |
1261 | - chan.send('\0') # send final done indicator |
1262 | - chan.close() |
1263 | - else: |
1264 | - try: |
1265 | - self.sftp.get(remote_filename,local_path.name) |
1266 | - except Exception, e: |
1267 | - raise BackendException("sftp get of %s (to %s) failed: %s" % (remote_filename,local_path.name,e)) |
1268 | - local_path.setdata() |
1269 | - |
1270 | - def list(self): |
1271 | - """lists the contents of the one-and-only duplicity dir on the remote side. |
1272 | - In scp mode unavoidable quoting issues will make this fail if the directory name |
1273 | - contains single quotes.""" |
1274 | - if (globals.use_scp): |
1275 | - output=self.runremote("ls -1 '%s'" % self.remote_dir,False,"scp dir listing ") |
1276 | - return output.splitlines() |
1277 | - else: |
1278 | - try: |
1279 | - return self.sftp.listdir() |
1280 | - except Exception, e: |
1281 | - raise BackendException("sftp listing of %s failed: %s" % (self.sftp.getcwd(),e)) |
1282 | - |
1283 | - def delete(self, filename_list): |
1284 | - """deletes all files in the list on the remote side. In scp mode unavoidable quoting issues |
1285 | - will cause failures if filenames containing single quotes are encountered.""" |
1286 | - for fn in filename_list: |
1287 | - if (globals.use_scp): |
1288 | - self.runremote("rm '%s/%s'" % (self.remote_dir,fn),False,"scp rm ") |
1289 | - else: |
1290 | - try: |
1291 | - self.sftp.remove(fn) |
1292 | - except Exception, e: |
1293 | - raise BackendException("sftp rm %s failed: %s" % (fn,e)) |
1294 | - |
1295 | - def runremote(self,cmd,ignoreexitcode=False,errorprefix=""): |
1296 | - """small convenience function that opens a shell channel, runs remote command and returns |
1297 | - stdout of command. throws an exception if exit code!=0 and not ignored""" |
1298 | - try: |
1299 | - chan=self.client.get_transport().open_session() |
1300 | - chan.settimeout(globals.timeout) |
1301 | - chan.exec_command(cmd) |
1302 | - except Exception, e: |
1303 | - raise BackendException("%sexecution failed: %s" % (errorprefix,e)) |
1304 | - output=chan.recv(-1) |
1305 | - res=chan.recv_exit_status() |
1306 | - if (res!=0 and not ignoreexitcode): |
1307 | - raise BackendException("%sfailed(%d): %s" % (errorprefix,res,chan.recv_stderr(4096))) |
1308 | - return output |
1309 | - |
1310 | - def gethostconfig(self, file, host): |
1311 | - file = os.path.expanduser(file) |
1312 | - if not os.path.isfile(file): |
1313 | - return {} |
1314 | - |
1315 | - sshconfig = paramiko.SSHConfig() |
1316 | - try: |
1317 | - sshconfig.parse(open(file)) |
1318 | - except Exception, e: |
1319 | - raise BackendException("could not load '%s', maybe corrupt?" % (file)) |
1320 | - |
1321 | - return sshconfig.lookup(host) |
1322 | - |
1323 | -class AgreedAddPolicy (paramiko.AutoAddPolicy): |
1324 | - """ |
1325 | - Policy for showing a yes/no prompt and adding the hostname and new |
1326 | - host key to the known host file accordingly. |
1327 | - |
1328 | - This class simply extends the AutoAddPolicy class with a yes/no prompt. |
1329 | - """ |
1330 | - def missing_host_key(self, client, hostname, key): |
1331 | - fp = hexlify(key.get_fingerprint()) |
1332 | - fingerprint = ':'.join(a+b for a,b in zip(fp[::2], fp[1::2])) |
1333 | - question = """The authenticity of host '%s' can't be established. |
1334 | -%s key fingerprint is %s. |
1335 | -Are you sure you want to continue connecting (yes/no)? """ % (hostname, key.get_name().upper(), fingerprint) |
1336 | - while True: |
1337 | - sys.stdout.write(question) |
1338 | - choice = raw_input().lower() |
1339 | - if choice in ['yes','y']: |
1340 | - super(AgreedAddPolicy, self).missing_host_key(client, hostname, key) |
1341 | - return |
1342 | - elif choice in ['no','n']: |
1343 | - raise AuthenticityException( hostname ) |
1344 | - else: |
1345 | - question = "Please type 'yes' or 'no': " |
1346 | - |
1347 | -class AuthenticityException (paramiko.SSHException): |
1348 | - def __init__(self, hostname): |
1349 | - paramiko.SSHException.__init__(self, 'Host key verification for server %s failed.' % hostname) |
1350 | - |
1351 | - |
1352 | -duplicity.backend.register_backend("sftp", SftpBackend) |
1353 | -duplicity.backend.register_backend("scp", SftpBackend) |
1354 | -duplicity.backend.register_backend("ssh", SftpBackend) |
1355 | +from duplicity import globals, log |
1356 | + |
1357 | +def warn_option(option,optionvar): |
1358 | + if optionvar: |
1359 | + log.Warn("Warning: Option %s is supported by ssh pexpect backend only and will be ignored. " % option ) |
1360 | + |
1361 | +if globals.ssh_backend and \ |
1362 | + globals.ssh_backend.lower().strip() == 'pexpect': |
1363 | + import ssh_pexpect |
1364 | +else: |
1365 | + warn_option("--scp-command",globals.scp_command) |
1366 | + warn_option("--sftp-command",globals.sftp_command) |
1367 | + import ssh_paramiko |
1368 | + |
1369 | |
1370 | === modified file 'duplicity/commandline.py' |
1371 | --- duplicity/commandline.py 2012-02-05 19:07:35 +0000 |
1372 | +++ duplicity/commandline.py 2012-03-13 20:58:22 +0000 |
1373 | @@ -68,10 +68,6 @@ |
1374 | "and will be removed in a future release.\n" |
1375 | "Use of default filenames is strongly suggested.") % opt |
1376 | |
1377 | -def scp_deprecation(o,s,v,p): |
1378 | - print >>sys.stderr, "Warning: Option %s is deprecated and ignored. Use --ssh-options instead." % o |
1379 | - |
1380 | - |
1381 | def expand_fn(filename): |
1382 | return os.path.expanduser(os.path.expandvars(filename)) |
1383 | |
1384 | @@ -473,15 +469,11 @@ |
1385 | if sys.version_info[:2] >= (2,6): |
1386 | parser.add_option("--s3-use-multiprocessing", action="store_true") |
1387 | |
1388 | - # scp command to use |
1389 | - # TRANSL: noun |
1390 | - parser.add_option("--scp-command", nargs=1, type="string", |
1391 | - action="callback", callback=scp_deprecation) |
1392 | + # scp command to use (ssh pexpect backend) |
1393 | + parser.add_option("--scp-command", metavar=_("command")) |
1394 | |
1395 | - # sftp command to use |
1396 | - # TRANSL: noun |
1397 | - parser.add_option("--sftp-command", nargs=1, type="string", |
1398 | - action="callback", callback=scp_deprecation) |
1399 | + # sftp command to use (ssh pexpect backend) |
1400 | + parser.add_option("--sftp-command", metavar=_("command")) |
1401 | |
1402 | # If set, use short (< 30 char) filenames for all the remote files. |
1403 | parser.add_option("--short-filenames", action="callback", |
1404 | @@ -498,6 +490,9 @@ |
1405 | # default to batch mode using public-key encryption |
1406 | parser.add_option("--ssh-askpass", action="store_true") |
1407 | |
1408 | + # allow the user to switch ssh backend |
1409 | + parser.add_option("--ssh-backend", metavar=_("paramiko|pexpect")) |
1410 | + |
1411 | # user added ssh options |
1412 | parser.add_option("--ssh-options", action="extend", metavar=_("options")) |
1413 | |
1414 | |
1415 | === modified file 'duplicity/globals.py' |
1416 | --- duplicity/globals.py 2012-02-05 18:13:40 +0000 |
1417 | +++ duplicity/globals.py 2012-03-13 20:58:22 +0000 |
1418 | @@ -198,13 +198,16 @@ |
1419 | # Wheter to specify --use-agent in GnuPG options |
1420 | use_agent = False |
1421 | |
1422 | -# ssh commands to use |
1423 | -scp_command = "scp" |
1424 | -sftp_command = "sftp" |
1425 | +# ssh commands to use, used by ssh_pexpect (defaults to sftp, scp) |
1426 | +scp_command = None |
1427 | +sftp_command = None |
1428 | |
1429 | # default to batch mode using public-key encryption |
1430 | ssh_askpass = False |
1431 | |
1432 | +# default ssh backend is paramiko |
1433 | +ssh_backend = "paramiko" |
1434 | + |
1435 | # user added ssh options |
1436 | ssh_options = "" |
1437 | |
1438 | |
1439 | === added file 'duplicity/pexpect.py' |
1440 | --- duplicity/pexpect.py 1970-01-01 00:00:00 +0000 |
1441 | +++ duplicity/pexpect.py 2012-03-13 20:58:22 +0000 |
1442 | @@ -0,0 +1,1845 @@ |
1443 | +"""Pexpect is a Python module for spawning child applications and controlling |
1444 | +them automatically. Pexpect can be used for automating interactive applications |
1445 | +such as ssh, ftp, passwd, telnet, etc. It can be used to a automate setup |
1446 | +scripts for duplicating software package installations on different servers. It |
1447 | +can be used for automated software testing. Pexpect is in the spirit of Don |
1448 | +Libes' Expect, but Pexpect is pure Python. Other Expect-like modules for Python |
1449 | +require TCL and Expect or require C extensions to be compiled. Pexpect does not |
1450 | +use C, Expect, or TCL extensions. It should work on any platform that supports |
1451 | +the standard Python pty module. The Pexpect interface focuses on ease of use so |
1452 | +that simple tasks are easy. |
1453 | + |
1454 | +There are two main interfaces to Pexpect -- the function, run() and the class, |
1455 | +spawn. You can call the run() function to execute a command and return the |
1456 | +output. This is a handy replacement for os.system(). |
1457 | + |
1458 | +For example:: |
1459 | + |
1460 | + pexpect.run('ls -la') |
1461 | + |
1462 | +The more powerful interface is the spawn class. You can use this to spawn an |
1463 | +external child command and then interact with the child by sending lines and |
1464 | +expecting responses. |
1465 | + |
1466 | +For example:: |
1467 | + |
1468 | + child = pexpect.spawn('scp foo myname@host.example.com:.') |
1469 | + child.expect ('Password:') |
1470 | + child.sendline (mypassword) |
1471 | + |
1472 | +This works even for commands that ask for passwords or other input outside of |
1473 | +the normal stdio streams. |
1474 | + |
1475 | +Credits: Noah Spurrier, Richard Holden, Marco Molteni, Kimberley Burchett, |
1476 | +Robert Stone, Hartmut Goebel, Chad Schroeder, Erick Tryzelaar, Dave Kirby, Ids |
1477 | +vander Molen, George Todd, Noel Taylor, Nicolas D. Cesar, Alexander Gattin, |
1478 | +Geoffrey Marshall, Francisco Lourenco, Glen Mabey, Karthik Gurusamy, Fernando |
1479 | +Perez, Corey Minyard, Jon Cohen, Guillaume Chazarain, Andrew Ryan, Nick |
1480 | +Craig-Wood, Andrew Stone, Jorgen Grahn (Let me know if I forgot anyone.) |
1481 | + |
1482 | +Free, open source, and all that good stuff. |
1483 | + |
1484 | +Permission is hereby granted, free of charge, to any person obtaining a copy of |
1485 | +this software and associated documentation files (the "Software"), to deal in |
1486 | +the Software without restriction, including without limitation the rights to |
1487 | +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
1488 | +of the Software, and to permit persons to whom the Software is furnished to do |
1489 | +so, subject to the following conditions: |
1490 | + |
1491 | +The above copyright notice and this permission notice shall be included in all |
1492 | +copies or substantial portions of the Software. |
1493 | + |
1494 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
1495 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
1496 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
1497 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
1498 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
1499 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
1500 | +SOFTWARE. |
1501 | + |
1502 | +Pexpect Copyright (c) 2008 Noah Spurrier |
1503 | +http://pexpect.sourceforge.net/ |
1504 | + |
1505 | +$Id: pexpect.py,v 1.1 2009/01/06 22:11:37 loafman Exp $ |
1506 | +""" |
1507 | + |
1508 | +try: |
1509 | + import os, sys, time |
1510 | + import select |
1511 | + import string |
1512 | + import re |
1513 | + import struct |
1514 | + import resource |
1515 | + import types |
1516 | + import pty |
1517 | + import tty |
1518 | + import termios |
1519 | + import fcntl |
1520 | + import errno |
1521 | + import traceback |
1522 | + import signal |
1523 | +except ImportError, e: |
1524 | + raise ImportError (str(e) + """ |
1525 | + |
1526 | +A critical module was not found. Probably this operating system does not |
1527 | +support it. Pexpect is intended for UNIX-like operating systems.""") |
1528 | + |
1529 | +__version__ = '2.3' |
1530 | +__revision__ = '$Revision: 1.1 $' |
1531 | +__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'run', 'which', |
1532 | + 'split_command_line', '__version__', '__revision__'] |
1533 | + |
1534 | +# Exception classes used by this module. |
1535 | +class ExceptionPexpect(Exception): |
1536 | + |
1537 | + """Base class for all exceptions raised by this module. |
1538 | + """ |
1539 | + |
1540 | + def __init__(self, value): |
1541 | + |
1542 | + self.value = value |
1543 | + |
1544 | + def __str__(self): |
1545 | + |
1546 | + return str(self.value) |
1547 | + |
1548 | + def get_trace(self): |
1549 | + |
1550 | + """This returns an abbreviated stack trace with lines that only concern |
1551 | + the caller. In other words, the stack trace inside the Pexpect module |
1552 | + is not included. """ |
1553 | + |
1554 | + tblist = traceback.extract_tb(sys.exc_info()[2]) |
1555 | + #tblist = filter(self.__filter_not_pexpect, tblist) |
1556 | + tblist = [item for item in tblist if self.__filter_not_pexpect(item)] |
1557 | + tblist = traceback.format_list(tblist) |
1558 | + return ''.join(tblist) |
1559 | + |
1560 | + def __filter_not_pexpect(self, trace_list_item): |
1561 | + |
1562 | + """This returns True if list item 0 the string 'pexpect.py' in it. """ |
1563 | + |
1564 | + if trace_list_item[0].find('pexpect.py') == -1: |
1565 | + return True |
1566 | + else: |
1567 | + return False |
1568 | + |
1569 | +class EOF(ExceptionPexpect): |
1570 | + |
1571 | + """Raised when EOF is read from a child. This usually means the child has exited.""" |
1572 | + |
1573 | +class TIMEOUT(ExceptionPexpect): |
1574 | + |
1575 | + """Raised when a read time exceeds the timeout. """ |
1576 | + |
1577 | +##class TIMEOUT_PATTERN(TIMEOUT): |
1578 | +## """Raised when the pattern match time exceeds the timeout. |
1579 | +## This is different than a read TIMEOUT because the child process may |
1580 | +## give output, thus never give a TIMEOUT, but the output |
1581 | +## may never match a pattern. |
1582 | +## """ |
1583 | +##class MAXBUFFER(ExceptionPexpect): |
1584 | +## """Raised when a scan buffer fills before matching an expected pattern.""" |
1585 | + |
1586 | +def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None, cwd=None, env=None): |
1587 | + |
1588 | + """ |
1589 | + This function runs the given command; waits for it to finish; then |
1590 | + returns all output as a string. STDERR is included in output. If the full |
1591 | + path to the command is not given then the path is searched. |
1592 | + |
1593 | + Note that lines are terminated by CR/LF (\\r\\n) combination even on |
1594 | + UNIX-like systems because this is the standard for pseudo ttys. If you set |
1595 | + 'withexitstatus' to true, then run will return a tuple of (command_output, |
1596 | + exitstatus). If 'withexitstatus' is false then this returns just |
1597 | + command_output. |
1598 | + |
1599 | + The run() function can often be used instead of creating a spawn instance. |
1600 | + For example, the following code uses spawn:: |
1601 | + |
1602 | + from pexpect import * #@UnusedWildImport |
1603 | + child = spawn('scp foo myname@host.example.com:.') |
1604 | + child.expect ('(?i)password') |
1605 | + child.sendline (mypassword) |
1606 | + |
1607 | + The previous code can be replace with the following:: |
1608 | + |
1609 | + from pexpect import * #@UnusedWildImport |
1610 | + run ('scp foo myname@host.example.com:.', events={'(?i)password': mypassword}) |
1611 | + |
1612 | + Examples |
1613 | + ======== |
1614 | + |
1615 | + Start the apache daemon on the local machine:: |
1616 | + |
1617 | + from pexpect import * #@UnusedWildImport |
1618 | + run ("/usr/local/apache/bin/apachectl start") |
1619 | + |
1620 | + Check in a file using SVN:: |
1621 | + |
1622 | + from pexpect import * #@UnusedWildImport |
1623 | + run ("svn ci -m 'automatic commit' my_file.py") |
1624 | + |
1625 | + Run a command and capture exit status:: |
1626 | + |
1627 | + from pexpect import * #@UnusedWildImport |
1628 | + (command_output, exitstatus) = run ('ls -l /bin', withexitstatus=1) |
1629 | + |
1630 | + Tricky Examples |
1631 | + =============== |
1632 | + |
1633 | + The following will run SSH and execute 'ls -l' on the remote machine. The |
1634 | + password 'secret' will be sent if the '(?i)password' pattern is ever seen:: |
1635 | + |
1636 | + run ("ssh username@machine.example.com 'ls -l'", events={'(?i)password':'secret\\n'}) |
1637 | + |
1638 | + This will start mencoder to rip a video from DVD. This will also display |
1639 | + progress ticks every 5 seconds as it runs. For example:: |
1640 | + |
1641 | + from pexpect import * #@UnusedWildImport |
1642 | + def print_ticks(d): |
1643 | + print d['event_count'], |
1644 | + run ("mencoder dvd://1 -o video.avi -oac copy -ovc copy", events={TIMEOUT:print_ticks}, timeout=5) |
1645 | + |
1646 | + The 'events' argument should be a dictionary of patterns and responses. |
1647 | + Whenever one of the patterns is seen in the command out run() will send the |
1648 | + associated response string. Note that you should put newlines in your |
1649 | + string if Enter is necessary. The responses may also contain callback |
1650 | + functions. Any callback is function that takes a dictionary as an argument. |
1651 | + The dictionary contains all the locals from the run() function, so you can |
1652 | + access the child spawn object or any other variable defined in run() |
1653 | + (event_count, child, and extra_args are the most useful). A callback may |
1654 | + return True to stop the current run process otherwise run() continues until |
1655 | + the next event. A callback may also return a string which will be sent to |
1656 | + the child. 'extra_args' is not used by directly run(). It provides a way to |
1657 | + pass data to a callback function through run() through the locals |
1658 | + dictionary passed to a callback. """ |
1659 | + |
1660 | + if timeout == -1: |
1661 | + child = spawn(command, maxread=2000, logfile=logfile, cwd=cwd, env=env) |
1662 | + else: |
1663 | + child = spawn(command, timeout=timeout, maxread=2000, logfile=logfile, cwd=cwd, env=env) |
1664 | + if events is not None: |
1665 | + patterns = events.keys() |
1666 | + responses = events.values() |
1667 | + else: |
1668 | + patterns=None # We assume that EOF or TIMEOUT will save us. |
1669 | + responses=None |
1670 | + child_result_list = [] |
1671 | + event_count = 0 |
1672 | + while 1: |
1673 | + try: |
1674 | + index = child.expect (patterns) |
1675 | + if type(child.after) in types.StringTypes: |
1676 | + child_result_list.append(child.before + child.after) |
1677 | + else: # child.after may have been a TIMEOUT or EOF, so don't cat those. |
1678 | + child_result_list.append(child.before) |
1679 | + if type(responses[index]) in types.StringTypes: |
1680 | + child.send(responses[index]) |
1681 | + elif type(responses[index]) is types.FunctionType: |
1682 | + callback_result = responses[index](locals()) |
1683 | + sys.stdout.flush() |
1684 | + if type(callback_result) in types.StringTypes: |
1685 | + child.send(callback_result) |
1686 | + elif callback_result: |
1687 | + break |
1688 | + else: |
1689 | + raise TypeError ('The callback must be a string or function type.') |
1690 | + event_count = event_count + 1 |
1691 | + except TIMEOUT, e: |
1692 | + child_result_list.append(child.before) |
1693 | + break |
1694 | + except EOF, e: |
1695 | + child_result_list.append(child.before) |
1696 | + break |
1697 | + child_result = ''.join(child_result_list) |
1698 | + if withexitstatus: |
1699 | + child.close() |
1700 | + return (child_result, child.exitstatus) |
1701 | + else: |
1702 | + return child_result |
1703 | + |
1704 | +class spawn (object): |
1705 | + |
1706 | + """This is the main class interface for Pexpect. Use this class to start |
1707 | + and control child applications. """ |
1708 | + |
1709 | + def __init__(self, command, args=[], timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None): |
1710 | + |
1711 | + """This is the constructor. The command parameter may be a string that |
1712 | + includes a command and any arguments to the command. For example:: |
1713 | + |
1714 | + child = pexpect.spawn ('/usr/bin/ftp') |
1715 | + child = pexpect.spawn ('/usr/bin/ssh user@example.com') |
1716 | + child = pexpect.spawn ('ls -latr /tmp') |
1717 | + |
1718 | + You may also construct it with a list of arguments like so:: |
1719 | + |
1720 | + child = pexpect.spawn ('/usr/bin/ftp', []) |
1721 | + child = pexpect.spawn ('/usr/bin/ssh', ['user@example.com']) |
1722 | + child = pexpect.spawn ('ls', ['-latr', '/tmp']) |
1723 | + |
1724 | + After this the child application will be created and will be ready to |
1725 | + talk to. For normal use, see expect() and send() and sendline(). |
1726 | + |
1727 | + Remember that Pexpect does NOT interpret shell meta characters such as |
1728 | + redirect, pipe, or wild cards (>, |, or *). This is a common mistake. |
1729 | + If you want to run a command and pipe it through another command then |
1730 | + you must also start a shell. For example:: |
1731 | + |
1732 | + child = pexpect.spawn('/bin/bash -c "ls -l | grep LOG > log_list.txt"') |
1733 | + child.expect(pexpect.EOF) |
1734 | + |
1735 | + The second form of spawn (where you pass a list of arguments) is useful |
1736 | + in situations where you wish to spawn a command and pass it its own |
1737 | + argument list. This can make syntax more clear. For example, the |
1738 | + following is equivalent to the previous example:: |
1739 | + |
1740 | + shell_cmd = 'ls -l | grep LOG > log_list.txt' |
1741 | + child = pexpect.spawn('/bin/bash', ['-c', shell_cmd]) |
1742 | + child.expect(pexpect.EOF) |
1743 | + |
1744 | + The maxread attribute sets the read buffer size. This is maximum number |
1745 | + of bytes that Pexpect will try to read from a TTY at one time. Setting |
1746 | + the maxread size to 1 will turn off buffering. Setting the maxread |
1747 | + value higher may help performance in cases where large amounts of |
1748 | + output are read back from the child. This feature is useful in |
1749 | + conjunction with searchwindowsize. |
1750 | + |
1751 | + The searchwindowsize attribute sets the how far back in the incomming |
1752 | + seach buffer Pexpect will search for pattern matches. Every time |
1753 | + Pexpect reads some data from the child it will append the data to the |
1754 | + incomming buffer. The default is to search from the beginning of the |
1755 | + imcomming buffer each time new data is read from the child. But this is |
1756 | + very inefficient if you are running a command that generates a large |
1757 | + amount of data where you want to match The searchwindowsize does not |
1758 | + effect the size of the incomming data buffer. You will still have |
1759 | + access to the full buffer after expect() returns. |
1760 | + |
1761 | + The logfile member turns on or off logging. All input and output will |
1762 | + be copied to the given file object. Set logfile to None to stop |
1763 | + logging. This is the default. Set logfile to sys.stdout to echo |
1764 | + everything to standard output. The logfile is flushed after each write. |
1765 | + |
1766 | + Example log input and output to a file:: |
1767 | + |
1768 | + child = pexpect.spawn('some_command') |
1769 | + fout = file('mylog.txt','w') |
1770 | + child.logfile = fout |
1771 | + |
1772 | + Example log to stdout:: |
1773 | + |
1774 | + child = pexpect.spawn('some_command') |
1775 | + child.logfile = sys.stdout |
1776 | + |
1777 | + The logfile_read and logfile_send members can be used to separately log |
1778 | + the input from the child and output sent to the child. Sometimes you |
1779 | + don't want to see everything you write to the child. You only want to |
1780 | + log what the child sends back. For example:: |
1781 | + |
1782 | + child = pexpect.spawn('some_command') |
1783 | + child.logfile_read = sys.stdout |
1784 | + |
1785 | + To separately log output sent to the child use logfile_send:: |
1786 | + |
1787 | + self.logfile_send = fout |
1788 | + |
1789 | + The delaybeforesend helps overcome a weird behavior that many users |
1790 | + were experiencing. The typical problem was that a user would expect() a |
1791 | + "Password:" prompt and then immediately call sendline() to send the |
1792 | + password. The user would then see that their password was echoed back |
1793 | + to them. Passwords don't normally echo. The problem is caused by the |
1794 | + fact that most applications print out the "Password" prompt and then |
1795 | + turn off stdin echo, but if you send your password before the |
1796 | + application turned off echo, then you get your password echoed. |
1797 | + Normally this wouldn't be a problem when interacting with a human at a |
1798 | + real keyboard. If you introduce a slight delay just before writing then |
1799 | + this seems to clear up the problem. This was such a common problem for |
1800 | + many users that I decided that the default pexpect behavior should be |
1801 | + to sleep just before writing to the child application. 1/20th of a |
1802 | + second (50 ms) seems to be enough to clear up the problem. You can set |
1803 | + delaybeforesend to 0 to return to the old behavior. Most Linux machines |
1804 | + don't like this to be below 0.03. I don't know why. |
1805 | + |
1806 | + Note that spawn is clever about finding commands on your path. |
1807 | + It uses the same logic that "which" uses to find executables. |
1808 | + |
1809 | + If you wish to get the exit status of the child you must call the |
1810 | + close() method. The exit or signal status of the child will be stored |
1811 | + in self.exitstatus or self.signalstatus. If the child exited normally |
1812 | + then exitstatus will store the exit return code and signalstatus will |
1813 | + be None. If the child was terminated abnormally with a signal then |
1814 | + signalstatus will store the signal value and exitstatus will be None. |
1815 | + If you need more detail you can also read the self.status member which |
1816 | + stores the status returned by os.waitpid. You can interpret this using |
1817 | + os.WIFEXITED/os.WEXITSTATUS or os.WIFSIGNALED/os.TERMSIG. """ |
1818 | + |
1819 | + self.STDIN_FILENO = pty.STDIN_FILENO |
1820 | + self.STDOUT_FILENO = pty.STDOUT_FILENO |
1821 | + self.STDERR_FILENO = pty.STDERR_FILENO |
1822 | + self.stdin = sys.stdin |
1823 | + self.stdout = sys.stdout |
1824 | + self.stderr = sys.stderr |
1825 | + |
1826 | + self.searcher = None |
1827 | + self.ignorecase = False |
1828 | + self.before = None |
1829 | + self.after = None |
1830 | + self.match = None |
1831 | + self.match_index = None |
1832 | + self.terminated = True |
1833 | + self.exitstatus = None |
1834 | + self.signalstatus = None |
1835 | + self.status = None # status returned by os.waitpid |
1836 | + self.flag_eof = False |
1837 | + self.pid = None |
1838 | + self.child_fd = -1 # initially closed |
1839 | + self.timeout = timeout |
1840 | + self.delimiter = EOF |
1841 | + self.logfile = logfile |
1842 | + self.logfile_read = None # input from child (read_nonblocking) |
1843 | + self.logfile_send = None # output to send (send, sendline) |
1844 | + self.maxread = maxread # max bytes to read at one time into buffer |
1845 | + self.buffer = '' # This is the read buffer. See maxread. |
1846 | + self.searchwindowsize = searchwindowsize # Anything before searchwindowsize point is preserved, but not searched. |
1847 | + # Most Linux machines don't like delaybeforesend to be below 0.03 (30 ms). |
1848 | + self.delaybeforesend = 0.05 # Sets sleep time used just before sending data to child. Time in seconds. |
1849 | + self.delayafterclose = 0.1 # Sets delay in close() method to allow kernel time to update process status. Time in seconds. |
1850 | + self.delayafterterminate = 0.1 # Sets delay in terminate() method to allow kernel time to update process status. Time in seconds. |
1851 | + self.softspace = False # File-like object. |
1852 | + self.name = '<' + repr(self) + '>' # File-like object. |
1853 | + self.encoding = None # File-like object. |
1854 | + self.closed = True # File-like object. |
1855 | + self.cwd = cwd |
1856 | + self.env = env |
1857 | + self.__irix_hack = (sys.platform.lower().find('irix')>=0) # This flags if we are running on irix |
1858 | + # Solaris uses internal __fork_pty(). All others use pty.fork(). |
1859 | + if (sys.platform.lower().find('solaris')>=0) or (sys.platform.lower().find('sunos5')>=0): |
1860 | + self.use_native_pty_fork = False |
1861 | + else: |
1862 | + self.use_native_pty_fork = True |
1863 | + |
1864 | + |
1865 | + # allow dummy instances for subclasses that may not use command or args. |
1866 | + if command is None: |
1867 | + self.command = None |
1868 | + self.args = None |
1869 | + self.name = '<pexpect factory incomplete>' |
1870 | + else: |
1871 | + self._spawn (command, args) |
1872 | + |
1873 | + def __del__(self): |
1874 | + |
1875 | + """This makes sure that no system resources are left open. Python only |
1876 | + garbage collects Python objects. OS file descriptors are not Python |
1877 | + objects, so they must be handled explicitly. If the child file |
1878 | + descriptor was opened outside of this class (passed to the constructor) |
1879 | + then this does not close it. """ |
1880 | + |
1881 | + if not self.closed: |
1882 | + # It is possible for __del__ methods to execute during the |
1883 | + # teardown of the Python VM itself. Thus self.close() may |
1884 | + # trigger an exception because os.close may be None. |
1885 | + # -- Fernando Perez |
1886 | + try: |
1887 | + self.close() |
1888 | + except AttributeError: |
1889 | + pass |
1890 | + |
1891 | + def __str__(self): |
1892 | + |
1893 | + """This returns a human-readable string that represents the state of |
1894 | + the object. """ |
1895 | + |
1896 | + s = [] |
1897 | + s.append(repr(self)) |
1898 | + s.append('version: ' + __version__ + ' (' + __revision__ + ')') |
1899 | + s.append('command: ' + str(self.command)) |
1900 | + s.append('args: ' + str(self.args)) |
1901 | + s.append('searcher: ' + str(self.searcher)) |
1902 | + s.append('buffer (last 100 chars): ' + str(self.buffer)[-100:]) |
1903 | + s.append('before (last 100 chars): ' + str(self.before)[-100:]) |
1904 | + s.append('after: ' + str(self.after)) |
1905 | + s.append('match: ' + str(self.match)) |
1906 | + s.append('match_index: ' + str(self.match_index)) |
1907 | + s.append('exitstatus: ' + str(self.exitstatus)) |
1908 | + s.append('flag_eof: ' + str(self.flag_eof)) |
1909 | + s.append('pid: ' + str(self.pid)) |
1910 | + s.append('child_fd: ' + str(self.child_fd)) |
1911 | + s.append('closed: ' + str(self.closed)) |
1912 | + s.append('timeout: ' + str(self.timeout)) |
1913 | + s.append('delimiter: ' + str(self.delimiter)) |
1914 | + s.append('logfile: ' + str(self.logfile)) |
1915 | + s.append('logfile_read: ' + str(self.logfile_read)) |
1916 | + s.append('logfile_send: ' + str(self.logfile_send)) |
1917 | + s.append('maxread: ' + str(self.maxread)) |
1918 | + s.append('ignorecase: ' + str(self.ignorecase)) |
1919 | + s.append('searchwindowsize: ' + str(self.searchwindowsize)) |
1920 | + s.append('delaybeforesend: ' + str(self.delaybeforesend)) |
1921 | + s.append('delayafterclose: ' + str(self.delayafterclose)) |
1922 | + s.append('delayafterterminate: ' + str(self.delayafterterminate)) |
1923 | + return '\n'.join(s) |
1924 | + |
1925 | + def _spawn(self,command,args=[]): |
1926 | + |
1927 | + """This starts the given command in a child process. This does all the |
1928 | + fork/exec type of stuff for a pty. This is called by __init__. If args |
1929 | + is empty then command will be parsed (split on spaces) and args will be |
1930 | + set to parsed arguments. """ |
1931 | + |
1932 | + # The pid and child_fd of this object get set by this method. |
1933 | + # Note that it is difficult for this method to fail. |
1934 | + # You cannot detect if the child process cannot start. |
1935 | + # So the only way you can tell if the child process started |
1936 | + # or not is to try to read from the file descriptor. If you get |
1937 | + # EOF immediately then it means that the child is already dead. |
1938 | + # That may not necessarily be bad because you may haved spawned a child |
1939 | + # that performs some task; creates no stdout output; and then dies. |
1940 | + |
1941 | + # If command is an int type then it may represent a file descriptor. |
1942 | + if type(command) == type(0): |
1943 | + raise ExceptionPexpect ('Command is an int type. If this is a file descriptor then maybe you want to use fdpexpect.fdspawn which takes an existing file descriptor instead of a command string.') |
1944 | + |
1945 | + if type (args) != type([]): |
1946 | + raise TypeError ('The argument, args, must be a list.') |
1947 | + |
1948 | + if args == []: |
1949 | + self.args = split_command_line(command) |
1950 | + self.command = self.args[0] |
1951 | + else: |
1952 | + self.args = args[:] # work with a copy |
1953 | + self.args.insert (0, command) |
1954 | + self.command = command |
1955 | + |
1956 | + command_with_path = which(self.command) |
1957 | + if command_with_path is None: |
1958 | + raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command) |
1959 | + self.command = command_with_path |
1960 | + self.args[0] = self.command |
1961 | + |
1962 | + self.name = '<' + ' '.join (self.args) + '>' |
1963 | + |
1964 | + assert self.pid is None, 'The pid member should be None.' |
1965 | + assert self.command is not None, 'The command member should not be None.' |
1966 | + |
1967 | + if self.use_native_pty_fork: |
1968 | + try: |
1969 | + self.pid, self.child_fd = pty.fork() |
1970 | + except OSError, e: |
1971 | + raise ExceptionPexpect('Error! pty.fork() failed: ' + str(e)) |
1972 | + else: # Use internal __fork_pty |
1973 | + self.pid, self.child_fd = self.__fork_pty() |
1974 | + |
1975 | + if self.pid == 0: # Child |
1976 | + try: |
1977 | + self.child_fd = sys.stdout.fileno() # used by setwinsize() |
1978 | + self.setwinsize(24, 80) |
1979 | + except Exception: |
1980 | + # Some platforms do not like setwinsize (Cygwin). |
1981 | + # This will cause problem when running applications that |
1982 | + # are very picky about window size. |
1983 | + # This is a serious limitation, but not a show stopper. |
1984 | + pass |
1985 | + # Do not allow child to inherit open file descriptors from parent. |
1986 | + max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0] |
1987 | + for i in range (3, max_fd): |
1988 | + try: |
1989 | + os.close (i) |
1990 | + except OSError: |
1991 | + pass |
1992 | + |
1993 | + # I don't know why this works, but ignoring SIGHUP fixes a |
1994 | + # problem when trying to start a Java daemon with sudo |
1995 | + # (specifically, Tomcat). |
1996 | + signal.signal(signal.SIGHUP, signal.SIG_IGN) |
1997 | + |
1998 | + if self.cwd is not None: |
1999 | + os.chdir(self.cwd) |
2000 | + if self.env is None: |
2001 | + os.execv(self.command, self.args) |
2002 | + else: |
2003 | + os.execvpe(self.command, self.args, self.env) |
2004 | + |
2005 | + # Parent |
2006 | + self.terminated = False |
2007 | + self.closed = False |
2008 | + |
2009 | + def __fork_pty(self): |
2010 | + |
2011 | + """This implements a substitute for the forkpty system call. This |
2012 | + should be more portable than the pty.fork() function. Specifically, |
2013 | + this should work on Solaris. |
2014 | + |
2015 | + Modified 10.06.05 by Geoff Marshall: Implemented __fork_pty() method to |
2016 | + resolve the issue with Python's pty.fork() not supporting Solaris, |
2017 | + particularly ssh. Based on patch to posixmodule.c authored by Noah |
2018 | + Spurrier:: |
2019 | + |
2020 | + http://mail.python.org/pipermail/python-dev/2003-May/035281.html |
2021 | + |
2022 | + """ |
2023 | + |
2024 | + parent_fd, child_fd = os.openpty() |
2025 | + if parent_fd < 0 or child_fd < 0: |
2026 | + raise ExceptionPexpect, "Error! Could not open pty with os.openpty()." |
2027 | + |
2028 | + pid = os.fork() |
2029 | + if pid < 0: |
2030 | + raise ExceptionPexpect, "Error! Failed os.fork()." |
2031 | + elif pid == 0: |
2032 | + # Child. |
2033 | + os.close(parent_fd) |
2034 | + self.__pty_make_controlling_tty(child_fd) |
2035 | + |
2036 | + os.dup2(child_fd, 0) |
2037 | + os.dup2(child_fd, 1) |
2038 | + os.dup2(child_fd, 2) |
2039 | + |
2040 | + if child_fd > 2: |
2041 | + os.close(child_fd) |
2042 | + else: |
2043 | + # Parent. |
2044 | + os.close(child_fd) |
2045 | + |
2046 | + return pid, parent_fd |
2047 | + |
2048 | + def __pty_make_controlling_tty(self, tty_fd): |
2049 | + |
2050 | + """This makes the pseudo-terminal the controlling tty. This should be |
2051 | + more portable than the pty.fork() function. Specifically, this should |
2052 | + work on Solaris. """ |
2053 | + |
2054 | + child_name = os.ttyname(tty_fd) |
2055 | + |
2056 | + # Disconnect from controlling tty if still connected. |
2057 | + fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY); |
2058 | + if fd >= 0: |
2059 | + os.close(fd) |
2060 | + |
2061 | + os.setsid() |
2062 | + |
2063 | + # Verify we are disconnected from controlling tty |
2064 | + try: |
2065 | + fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY); |
2066 | + if fd >= 0: |
2067 | + os.close(fd) |
2068 | + raise ExceptionPexpect, "Error! We are not disconnected from a controlling tty." |
2069 | + except Exception: |
2070 | + # Good! We are disconnected from a controlling tty. |
2071 | + pass |
2072 | + |
2073 | + # Verify we can open child pty. |
2074 | + fd = os.open(child_name, os.O_RDWR); |
2075 | + if fd < 0: |
2076 | + raise ExceptionPexpect, "Error! Could not open child pty, " + child_name |
2077 | + else: |
2078 | + os.close(fd) |
2079 | + |
2080 | + # Verify we now have a controlling tty. |
2081 | + fd = os.open("/dev/tty", os.O_WRONLY) |
2082 | + if fd < 0: |
2083 | + raise ExceptionPexpect, "Error! Could not open controlling tty, /dev/tty" |
2084 | + else: |
2085 | + os.close(fd) |
2086 | + |
2087 | + def fileno (self): # File-like object. |
2088 | + |
2089 | + """This returns the file descriptor of the pty for the child. |
2090 | + """ |
2091 | + |
2092 | + return self.child_fd |
2093 | + |
2094 | + def close (self, force=True): # File-like object. |
2095 | + |
2096 | + """This closes the connection with the child application. Note that |
2097 | + calling close() more than once is valid. This emulates standard Python |
2098 | + behavior with files. Set force to True if you want to make sure that |
2099 | + the child is terminated (SIGKILL is sent if the child ignores SIGHUP |
2100 | + and SIGINT). """ |
2101 | + |
2102 | + if not self.closed: |
2103 | + self.flush() |
2104 | + os.close (self.child_fd) |
2105 | + time.sleep(self.delayafterclose) # Give kernel time to update process status. |
2106 | + if self.isalive(): |
2107 | + if not self.terminate(force): |
2108 | + raise ExceptionPexpect ('close() could not terminate the child using terminate()') |
2109 | + self.child_fd = -1 |
2110 | + self.closed = True |
2111 | + #self.pid = None |
2112 | + |
2113 | + def flush (self): # File-like object. |
2114 | + |
2115 | + """This does nothing. It is here to support the interface for a |
2116 | + File-like object. """ |
2117 | + |
2118 | + pass |
2119 | + |
2120 | + def isatty (self): # File-like object. |
2121 | + |
2122 | + """This returns True if the file descriptor is open and connected to a |
2123 | + tty(-like) device, else False. """ |
2124 | + |
2125 | + return os.isatty(self.child_fd) |
2126 | + |
2127 | + def waitnoecho (self, timeout=-1): |
2128 | + |
2129 | + """This waits until the terminal ECHO flag is set False. This returns |
2130 | + True if the echo mode is off. This returns False if the ECHO flag was |
2131 | + not set False before the timeout. This can be used to detect when the |
2132 | + child is waiting for a password. Usually a child application will turn |
2133 | + off echo mode when it is waiting for the user to enter a password. For |
2134 | + example, instead of expecting the "password:" prompt you can wait for |
2135 | + the child to set ECHO off:: |
2136 | + |
2137 | + p = pexpect.spawn ('ssh user@example.com') |
2138 | + p.waitnoecho() |
2139 | + p.sendline(mypassword) |
2140 | + |
2141 | + If timeout is None then this method to block forever until ECHO flag is |
2142 | + False. |
2143 | + |
2144 | + """ |
2145 | + |
2146 | + if timeout == -1: |
2147 | + timeout = self.timeout |
2148 | + if timeout is not None: |
2149 | + end_time = time.time() + timeout |
2150 | + while True: |
2151 | + if not self.getecho(): |
2152 | + return True |
2153 | + if timeout < 0 and timeout is not None: |
2154 | + return False |
2155 | + if timeout is not None: |
2156 | + timeout = end_time - time.time() |
2157 | + time.sleep(0.1) |
2158 | + |
2159 | + def getecho (self): |
2160 | + |
2161 | + """This returns the terminal echo mode. This returns True if echo is |
2162 | + on or False if echo is off. Child applications that are expecting you |
2163 | + to enter a password often set ECHO False. See waitnoecho(). """ |
2164 | + |
2165 | + attr = termios.tcgetattr(self.child_fd) |
2166 | + if attr[3] & termios.ECHO: |
2167 | + return True |
2168 | + return False |
2169 | + |
2170 | + def setecho (self, state): |
2171 | + |
2172 | + """This sets the terminal echo mode on or off. Note that anything the |
2173 | + child sent before the echo will be lost, so you should be sure that |
2174 | + your input buffer is empty before you call setecho(). For example, the |
2175 | + following will work as expected:: |
2176 | + |
2177 | + p = pexpect.spawn('cat') |
2178 | + p.sendline ('1234') # We will see this twice (once from tty echo and again from cat). |
2179 | + p.expect (['1234']) |
2180 | + p.expect (['1234']) |
2181 | + p.setecho(False) # Turn off tty echo |
2182 | + p.sendline ('abcd') # We will set this only once (echoed by cat). |
2183 | + p.sendline ('wxyz') # We will set this only once (echoed by cat) |
2184 | + p.expect (['abcd']) |
2185 | + p.expect (['wxyz']) |
2186 | + |
2187 | + The following WILL NOT WORK because the lines sent before the setecho |
2188 | + will be lost:: |
2189 | + |
2190 | + p = pexpect.spawn('cat') |
2191 | + p.sendline ('1234') # We will see this twice (once from tty echo and again from cat). |
2192 | + p.setecho(False) # Turn off tty echo |
2193 | + p.sendline ('abcd') # We will set this only once (echoed by cat). |
2194 | + p.sendline ('wxyz') # We will set this only once (echoed by cat) |
2195 | + p.expect (['1234']) |
2196 | + p.expect (['1234']) |
2197 | + p.expect (['abcd']) |
2198 | + p.expect (['wxyz']) |
2199 | + """ |
2200 | + |
2201 | + self.child_fd |
2202 | + attr = termios.tcgetattr(self.child_fd) |
2203 | + if state: |
2204 | + attr[3] = attr[3] | termios.ECHO |
2205 | + else: |
2206 | + attr[3] = attr[3] & ~termios.ECHO |
2207 | + # I tried TCSADRAIN and TCSAFLUSH, but these were inconsistent |
2208 | + # and blocked on some platforms. TCSADRAIN is probably ideal if it worked. |
2209 | + termios.tcsetattr(self.child_fd, termios.TCSANOW, attr) |
2210 | + |
2211 | + def read_nonblocking (self, size = 1, timeout = -1): |
2212 | + |
2213 | + """This reads at most size characters from the child application. It |
2214 | + includes a timeout. If the read does not complete within the timeout |
2215 | + period then a TIMEOUT exception is raised. If the end of file is read |
2216 | + then an EOF exception will be raised. If a log file was set using |
2217 | + setlog() then all data will also be written to the log file. |
2218 | + |
2219 | + If timeout is None then the read may block indefinitely. If timeout is -1 |
2220 | + then the self.timeout value is used. If timeout is 0 then the child is |
2221 | + polled and if there was no data immediately ready then this will raise |
2222 | + a TIMEOUT exception. |
2223 | + |
2224 | + The timeout refers only to the amount of time to read at least one |
2225 | + character. This is not effected by the 'size' parameter, so if you call |
2226 | + read_nonblocking(size=100, timeout=30) and only one character is |
2227 | + available right away then one character will be returned immediately. |
2228 | + It will not wait for 30 seconds for another 99 characters to come in. |
2229 | + |
2230 | + This is a wrapper around os.read(). It uses select.select() to |
2231 | + implement the timeout. """ |
2232 | + |
2233 | + if self.closed: |
2234 | + raise ValueError ('I/O operation on closed file in read_nonblocking().') |
2235 | + |
2236 | + if timeout == -1: |
2237 | + timeout = self.timeout |
2238 | + |
2239 | + # Note that some systems such as Solaris do not give an EOF when |
2240 | + # the child dies. In fact, you can still try to read |
2241 | + # from the child_fd -- it will block forever or until TIMEOUT. |
2242 | + # For this case, I test isalive() before doing any reading. |
2243 | + # If isalive() is false, then I pretend that this is the same as EOF. |
2244 | + if not self.isalive(): |
2245 | + r,w,e = self.__select([self.child_fd], [], [], 0) # timeout of 0 means "poll" @UnusedVariable |
2246 | + if not r: |
2247 | + self.flag_eof = True |
2248 | + raise EOF ('End Of File (EOF) in read_nonblocking(). Braindead platform.') |
2249 | + elif self.__irix_hack: |
2250 | + # This is a hack for Irix. It seems that Irix requires a long delay before checking isalive. |
2251 | + # This adds a 2 second delay, but only when the child is terminated. |
2252 | + r, w, e = self.__select([self.child_fd], [], [], 2) #@UnusedVariable |
2253 | + if not r and not self.isalive(): |
2254 | + self.flag_eof = True |
2255 | + raise EOF ('End Of File (EOF) in read_nonblocking(). Pokey platform.') |
2256 | + |
2257 | + r,w,e = self.__select([self.child_fd], [], [], timeout) #@UnusedVariable |
2258 | + |
2259 | + if not r: |
2260 | + if not self.isalive(): |
2261 | + # Some platforms, such as Irix, will claim that their processes are alive; |
2262 | + # then timeout on the select; and then finally admit that they are not alive. |
2263 | + self.flag_eof = True |
2264 | + raise EOF ('End of File (EOF) in read_nonblocking(). Very pokey platform.') |
2265 | + else: |
2266 | + raise TIMEOUT ('Timeout exceeded in read_nonblocking().') |
2267 | + |
2268 | + if self.child_fd in r: |
2269 | + try: |
2270 | + s = os.read(self.child_fd, size) |
2271 | + except OSError, e: # Linux does this |
2272 | + self.flag_eof = True |
2273 | + raise EOF ('End Of File (EOF) in read_nonblocking(). Exception style platform.') |
2274 | + if s == '': # BSD style |
2275 | + self.flag_eof = True |
2276 | + raise EOF ('End Of File (EOF) in read_nonblocking(). Empty string style platform.') |
2277 | + |
2278 | + if self.logfile is not None: |
2279 | + self.logfile.write (s) |
2280 | + self.logfile.flush() |
2281 | + if self.logfile_read is not None: |
2282 | + self.logfile_read.write (s) |
2283 | + self.logfile_read.flush() |
2284 | + |
2285 | + return s |
2286 | + |
2287 | + raise ExceptionPexpect ('Reached an unexpected state in read_nonblocking().') |
2288 | + |
2289 | + def read (self, size = -1): # File-like object. |
2290 | + |
2291 | + """This reads at most "size" bytes from the file (less if the read hits |
2292 | + EOF before obtaining size bytes). If the size argument is negative or |
2293 | + omitted, read all data until EOF is reached. The bytes are returned as |
2294 | + a string object. An empty string is returned when EOF is encountered |
2295 | + immediately. """ |
2296 | + |
2297 | + if size == 0: |
2298 | + return '' |
2299 | + if size < 0: |
2300 | + self.expect (self.delimiter) # delimiter default is EOF |
2301 | + return self.before |
2302 | + |
2303 | + # I could have done this more directly by not using expect(), but |
2304 | + # I deliberately decided to couple read() to expect() so that |
2305 | + # I would catch any bugs early and ensure consistant behavior. |
2306 | + # It's a little less efficient, but there is less for me to |
2307 | + # worry about if I have to later modify read() or expect(). |
2308 | + # Note, it's OK if size==-1 in the regex. That just means it |
2309 | + # will never match anything in which case we stop only on EOF. |
2310 | + cre = re.compile('.{%d}' % size, re.DOTALL) |
2311 | + index = self.expect ([cre, self.delimiter]) # delimiter default is EOF |
2312 | + if index == 0: |
2313 | + return self.after ### self.before should be ''. Should I assert this? |
2314 | + return self.before |
2315 | + |
2316 | + def readline (self, size = -1): # File-like object. |
2317 | + |
2318 | + """This reads and returns one entire line. A trailing newline is kept |
2319 | + in the string, but may be absent when a file ends with an incomplete |
2320 | + line. Note: This readline() looks for a \\r\\n pair even on UNIX |
2321 | + because this is what the pseudo tty device returns. So contrary to what |
2322 | + you may expect you will receive the newline as \\r\\n. An empty string |
2323 | + is returned when EOF is hit immediately. Currently, the size argument is |
2324 | + mostly ignored, so this behavior is not standard for a file-like |
2325 | + object. If size is 0 then an empty string is returned. """ |
2326 | + |
2327 | + if size == 0: |
2328 | + return '' |
2329 | + index = self.expect (['\r\n', self.delimiter]) # delimiter default is EOF |
2330 | + if index == 0: |
2331 | + return self.before + '\r\n' |
2332 | + else: |
2333 | + return self.before |
2334 | + |
2335 | + def __iter__ (self): # File-like object. |
2336 | + |
2337 | + """This is to support iterators over a file-like object. |
2338 | + """ |
2339 | + |
2340 | + return self |
2341 | + |
2342 | + def next (self): # File-like object. |
2343 | + |
2344 | + """This is to support iterators over a file-like object. |
2345 | + """ |
2346 | + |
2347 | + result = self.readline() |
2348 | + if result == "": |
2349 | + raise StopIteration |
2350 | + return result |
2351 | + |
2352 | + def readlines (self, sizehint = -1): # File-like object. |
2353 | + |
2354 | + """This reads until EOF using readline() and returns a list containing |
2355 | + the lines thus read. The optional "sizehint" argument is ignored. """ |
2356 | + |
2357 | + lines = [] |
2358 | + while True: |
2359 | + line = self.readline() |
2360 | + if not line: |
2361 | + break |
2362 | + lines.append(line) |
2363 | + return lines |
2364 | + |
2365 | + def write(self, s): # File-like object. |
2366 | + |
2367 | + """This is similar to send() except that there is no return value. |
2368 | + """ |
2369 | + |
2370 | + self.send (s) |
2371 | + |
2372 | + def writelines (self, sequence): # File-like object. |
2373 | + |
2374 | + """This calls write() for each element in the sequence. The sequence |
2375 | + can be any iterable object producing strings, typically a list of |
2376 | + strings. This does not add line separators There is no return value. |
2377 | + """ |
2378 | + |
2379 | + for s in sequence: |
2380 | + self.write (s) |
2381 | + |
2382 | + def send(self, s): |
2383 | + |
2384 | + """This sends a string to the child process. This returns the number of |
2385 | + bytes written. If a log file was set then the data is also written to |
2386 | + the log. """ |
2387 | + |
2388 | + time.sleep(self.delaybeforesend) |
2389 | + if self.logfile is not None: |
2390 | + self.logfile.write (s) |
2391 | + self.logfile.flush() |
2392 | + if self.logfile_send is not None: |
2393 | + self.logfile_send.write (s) |
2394 | + self.logfile_send.flush() |
2395 | + c = os.write(self.child_fd, s) |
2396 | + return c |
2397 | + |
2398 | + def sendline(self, s=''): |
2399 | + |
2400 | + """This is like send(), but it adds a line feed (os.linesep). This |
2401 | + returns the number of bytes written. """ |
2402 | + |
2403 | + n = self.send(s) |
2404 | + n = n + self.send (os.linesep) |
2405 | + return n |
2406 | + |
2407 | + def sendcontrol(self, char): |
2408 | + |
2409 | + """This sends a control character to the child such as Ctrl-C or |
2410 | + Ctrl-D. For example, to send a Ctrl-G (ASCII 7):: |
2411 | + |
2412 | + child.sendcontrol('g') |
2413 | + |
2414 | + See also, sendintr() and sendeof(). |
2415 | + """ |
2416 | + |
2417 | + char = char.lower() |
2418 | + a = ord(char) |
2419 | + if a>=97 and a<=122: |
2420 | + a = a - ord('a') + 1 |
2421 | + return self.send (chr(a)) |
2422 | + d = {'@':0, '`':0, |
2423 | + '[':27, '{':27, |
2424 | + '\\':28, '|':28, |
2425 | + ']':29, '}': 29, |
2426 | + '^':30, '~':30, |
2427 | + '_':31, |
2428 | + '?':127} |
2429 | + if char not in d: |
2430 | + return 0 |
2431 | + return self.send (chr(d[char])) |
2432 | + |
2433 | + def sendeof(self): |
2434 | + |
2435 | + """This sends an EOF to the child. This sends a character which causes |
2436 | + the pending parent output buffer to be sent to the waiting child |
2437 | + program without waiting for end-of-line. If it is the first character |
2438 | + of the line, the read() in the user program returns 0, which signifies |
2439 | + end-of-file. This means to work as expected a sendeof() has to be |
2440 | + called at the beginning of a line. This method does not send a newline. |
2441 | + It is the responsibility of the caller to ensure the eof is sent at the |
2442 | + beginning of a line. """ |
2443 | + |
2444 | + ### Hmmm... how do I send an EOF? |
2445 | + ###C if ((m = write(pty, *buf, p - *buf)) < 0) |
2446 | + ###C return (errno == EWOULDBLOCK) ? n : -1; |
2447 | + #fd = sys.stdin.fileno() |
2448 | + #old = termios.tcgetattr(fd) # remember current state |
2449 | + #attr = termios.tcgetattr(fd) |
2450 | + #attr[3] = attr[3] | termios.ICANON # ICANON must be set to recognize EOF |
2451 | + #try: # use try/finally to ensure state gets restored |
2452 | + # termios.tcsetattr(fd, termios.TCSADRAIN, attr) |
2453 | + # if hasattr(termios, 'CEOF'): |
2454 | + # os.write (self.child_fd, '%c' % termios.CEOF) |
2455 | + # else: |
2456 | + # # Silly platform does not define CEOF so assume CTRL-D |
2457 | + # os.write (self.child_fd, '%c' % 4) |
2458 | + #finally: # restore state |
2459 | + # termios.tcsetattr(fd, termios.TCSADRAIN, old) |
2460 | + if hasattr(termios, 'VEOF'): |
2461 | + char = termios.tcgetattr(self.child_fd)[6][termios.VEOF] |
2462 | + else: |
2463 | + # platform does not define VEOF so assume CTRL-D |
2464 | + char = chr(4) |
2465 | + self.send(char) |
2466 | + |
2467 | + def sendintr(self): |
2468 | + |
2469 | + """This sends a SIGINT to the child. It does not require |
2470 | + the SIGINT to be the first character on a line. """ |
2471 | + |
2472 | + if hasattr(termios, 'VINTR'): |
2473 | + char = termios.tcgetattr(self.child_fd)[6][termios.VINTR] |
2474 | + else: |
2475 | + # platform does not define VINTR so assume CTRL-C |
2476 | + char = chr(3) |
2477 | + self.send (char) |
2478 | + |
2479 | + def eof (self): |
2480 | + |
2481 | + """This returns True if the EOF exception was ever raised. |
2482 | + """ |
2483 | + |
2484 | + return self.flag_eof |
2485 | + |
2486 | + def terminate(self, force=False): |
2487 | + |
2488 | + """This forces a child process to terminate. It starts nicely with |
2489 | + SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This |
2490 | + returns True if the child was terminated. This returns False if the |
2491 | + child could not be terminated. """ |
2492 | + |
2493 | + if not self.isalive(): |
2494 | + return True |
2495 | + try: |
2496 | + self.kill(signal.SIGHUP) |
2497 | + time.sleep(self.delayafterterminate) |
2498 | + if not self.isalive(): |
2499 | + return True |
2500 | + self.kill(signal.SIGCONT) |
2501 | + time.sleep(self.delayafterterminate) |
2502 | + if not self.isalive(): |
2503 | + return True |
2504 | + self.kill(signal.SIGINT) |
2505 | + time.sleep(self.delayafterterminate) |
2506 | + if not self.isalive(): |
2507 | + return True |
2508 | + if force: |
2509 | + self.kill(signal.SIGKILL) |
2510 | + time.sleep(self.delayafterterminate) |
2511 | + if not self.isalive(): |
2512 | + return True |
2513 | + else: |
2514 | + return False |
2515 | + return False |
2516 | + except OSError, e: |
2517 | + # I think there are kernel timing issues that sometimes cause |
2518 | + # this to happen. I think isalive() reports True, but the |
2519 | + # process is dead to the kernel. |
2520 | + # Make one last attempt to see if the kernel is up to date. |
2521 | + time.sleep(self.delayafterterminate) |
2522 | + if not self.isalive(): |
2523 | + return True |
2524 | + else: |
2525 | + return False |
2526 | + |
2527 | + def wait(self): |
2528 | + |
2529 | + """This waits until the child exits. This is a blocking call. This will |
2530 | + not read any data from the child, so this will block forever if the |
2531 | + child has unread output and has terminated. In other words, the child |
2532 | + may have printed output then called exit(); but, technically, the child |
2533 | + is still alive until its output is read. """ |
2534 | + |
2535 | + if self.isalive(): |
2536 | + pid, status = os.waitpid(self.pid, 0) #@UnusedVariable |
2537 | + else: |
2538 | + raise ExceptionPexpect ('Cannot wait for dead child process.') |
2539 | + self.exitstatus = os.WEXITSTATUS(status) |
2540 | + if os.WIFEXITED (status): |
2541 | + self.status = status |
2542 | + self.exitstatus = os.WEXITSTATUS(status) |
2543 | + self.signalstatus = None |
2544 | + self.terminated = True |
2545 | + elif os.WIFSIGNALED (status): |
2546 | + self.status = status |
2547 | + self.exitstatus = None |
2548 | + self.signalstatus = os.WTERMSIG(status) |
2549 | + self.terminated = True |
2550 | + elif os.WIFSTOPPED (status): |
2551 | + raise ExceptionPexpect ('Wait was called for a child process that is stopped. This is not supported. Is some other process attempting job control with our child pid?') |
2552 | + return self.exitstatus |
2553 | + |
2554 | + def isalive(self): |
2555 | + |
2556 | + """This tests if the child process is running or not. This is |
2557 | + non-blocking. If the child was terminated then this will read the |
2558 | + exitstatus or signalstatus of the child. This returns True if the child |
2559 | + process appears to be running or False if not. It can take literally |
2560 | + SECONDS for Solaris to return the right status. """ |
2561 | + |
2562 | + if self.terminated: |
2563 | + return False |
2564 | + |
2565 | + if self.flag_eof: |
2566 | + # This is for Linux, which requires the blocking form of waitpid to get |
2567 | + # status of a defunct process. This is super-lame. The flag_eof would have |
2568 | + # been set in read_nonblocking(), so this should be safe. |
2569 | + waitpid_options = 0 |
2570 | + else: |
2571 | + waitpid_options = os.WNOHANG |
2572 | + |
2573 | + try: |
2574 | + pid, status = os.waitpid(self.pid, waitpid_options) |
2575 | + except OSError, e: # No child processes |
2576 | + if e[0] == errno.ECHILD: |
2577 | + raise ExceptionPexpect ('isalive() encountered condition where "terminated" is 0, but there was no child process. Did someone else call waitpid() on our process?') |
2578 | + else: |
2579 | + raise e |
2580 | + |
2581 | + # I have to do this twice for Solaris. I can't even believe that I figured this out... |
2582 | + # If waitpid() returns 0 it means that no child process wishes to |
2583 | + # report, and the value of status is undefined. |
2584 | + if pid == 0: |
2585 | + try: |
2586 | + pid, status = os.waitpid(self.pid, waitpid_options) ### os.WNOHANG) # Solaris! |
2587 | + except OSError, e: # This should never happen... |
2588 | + if e[0] == errno.ECHILD: |
2589 | + raise ExceptionPexpect ('isalive() encountered condition that should never happen. There was no child process. Did someone else call waitpid() on our process?') |
2590 | + else: |
2591 | + raise e |
2592 | + |
2593 | + # If pid is still 0 after two calls to waitpid() then |
2594 | + # the process really is alive. This seems to work on all platforms, except |
2595 | + # for Irix which seems to require a blocking call on waitpid or select, so I let read_nonblocking |
2596 | + # take care of this situation (unfortunately, this requires waiting through the timeout). |
2597 | + if pid == 0: |
2598 | + return True |
2599 | + |
2600 | + if pid == 0: |
2601 | + return True |
2602 | + |
2603 | + if os.WIFEXITED (status): |
2604 | + self.status = status |
2605 | + self.exitstatus = os.WEXITSTATUS(status) |
2606 | + self.signalstatus = None |
2607 | + self.terminated = True |
2608 | + elif os.WIFSIGNALED (status): |
2609 | + self.status = status |
2610 | + self.exitstatus = None |
2611 | + self.signalstatus = os.WTERMSIG(status) |
2612 | + self.terminated = True |
2613 | + elif os.WIFSTOPPED (status): |
2614 | + raise ExceptionPexpect ('isalive() encountered condition where child process is stopped. This is not supported. Is some other process attempting job control with our child pid?') |
2615 | + return False |
2616 | + |
2617 | + def kill(self, sig): |
2618 | + |
2619 | + """This sends the given signal to the child application. In keeping |
2620 | + with UNIX tradition it has a misleading name. It does not necessarily |
2621 | + kill the child unless you send the right signal. """ |
2622 | + |
2623 | + # Same as os.kill, but the pid is given for you. |
2624 | + if self.isalive(): |
2625 | + os.kill(self.pid, sig) |
2626 | + |
2627 | + def compile_pattern_list(self, patterns): |
2628 | + |
2629 | + """This compiles a pattern-string or a list of pattern-strings. |
2630 | + Patterns must be a StringType, EOF, TIMEOUT, SRE_Pattern, or a list of |
2631 | + those. Patterns may also be None which results in an empty list (you |
2632 | + might do this if waiting for an EOF or TIMEOUT condition without |
2633 | + expecting any pattern). |
2634 | + |
2635 | + This is used by expect() when calling expect_list(). Thus expect() is |
2636 | + nothing more than:: |
2637 | + |
2638 | + cpl = self.compile_pattern_list(pl) |
2639 | + return self.expect_list(cpl, timeout) |
2640 | + |
2641 | + If you are using expect() within a loop it may be more |
2642 | + efficient to compile the patterns first and then call expect_list(). |
2643 | + This avoid calls in a loop to compile_pattern_list():: |
2644 | + |
2645 | + cpl = self.compile_pattern_list(my_pattern) |
2646 | + while some_condition: |
2647 | + ... |
2648 | + i = self.expect_list(clp, timeout) |
2649 | + ... |
2650 | + """ |
2651 | + |
2652 | + if patterns is None: |
2653 | + return [] |
2654 | + if type(patterns) is not types.ListType: |
2655 | + patterns = [patterns] |
2656 | + |
2657 | + compile_flags = re.DOTALL # Allow dot to match \n |
2658 | + if self.ignorecase: |
2659 | + compile_flags = compile_flags | re.IGNORECASE |
2660 | + compiled_pattern_list = [] |
2661 | + for p in patterns: |
2662 | + if type(p) in types.StringTypes: |
2663 | + compiled_pattern_list.append(re.compile(p, compile_flags)) |
2664 | + elif p is EOF: |
2665 | + compiled_pattern_list.append(EOF) |
2666 | + elif p is TIMEOUT: |
2667 | + compiled_pattern_list.append(TIMEOUT) |
2668 | + elif type(p) is type(re.compile('')): |
2669 | + compiled_pattern_list.append(p) |
2670 | + else: |
2671 | + raise TypeError ('Argument must be one of StringTypes, EOF, TIMEOUT, SRE_Pattern, or a list of those type. %s' % str(type(p))) |
2672 | + |
2673 | + return compiled_pattern_list |
2674 | + |
2675 | + def expect(self, pattern, timeout = -1, searchwindowsize=None): |
2676 | + |
2677 | + """This seeks through the stream until a pattern is matched. The |
2678 | + pattern is overloaded and may take several types. The pattern can be a |
2679 | + StringType, EOF, a compiled re, or a list of any of those types. |
2680 | + Strings will be compiled to re types. This returns the index into the |
2681 | + pattern list. If the pattern was not a list this returns index 0 on a |
2682 | + successful match. This may raise exceptions for EOF or TIMEOUT. To |
2683 | + avoid the EOF or TIMEOUT exceptions add EOF or TIMEOUT to the pattern |
2684 | + list. That will cause expect to match an EOF or TIMEOUT condition |
2685 | + instead of raising an exception. |
2686 | + |
2687 | + If you pass a list of patterns and more than one matches, the first match |
2688 | + in the stream is chosen. If more than one pattern matches at that point, |
2689 | + the leftmost in the pattern list is chosen. For example:: |
2690 | + |
2691 | + # the input is 'foobar' |
2692 | + index = p.expect (['bar', 'foo', 'foobar']) |
2693 | + # returns 1 ('foo') even though 'foobar' is a "better" match |
2694 | + |
2695 | + Please note, however, that buffering can affect this behavior, since |
2696 | + input arrives in unpredictable chunks. For example:: |
2697 | + |
2698 | + # the input is 'foobar' |
2699 | + index = p.expect (['foobar', 'foo']) |
2700 | + # returns 0 ('foobar') if all input is available at once, |
2701 | + # but returs 1 ('foo') if parts of the final 'bar' arrive late |
2702 | + |
2703 | + After a match is found the instance attributes 'before', 'after' and |
2704 | + 'match' will be set. You can see all the data read before the match in |
2705 | + 'before'. You can see the data that was matched in 'after'. The |
2706 | + re.MatchObject used in the re match will be in 'match'. If an error |
2707 | + occurred then 'before' will be set to all the data read so far and |
2708 | + 'after' and 'match' will be None. |
2709 | + |
2710 | + If timeout is -1 then timeout will be set to the self.timeout value. |
2711 | + |
2712 | + A list entry may be EOF or TIMEOUT instead of a string. This will |
2713 | + catch these exceptions and return the index of the list entry instead |
2714 | + of raising the exception. The attribute 'after' will be set to the |
2715 | + exception type. The attribute 'match' will be None. This allows you to |
2716 | + write code like this:: |
2717 | + |
2718 | + index = p.expect (['good', 'bad', pexpect.EOF, pexpect.TIMEOUT]) |
2719 | + if index == 0: |
2720 | + do_something() |
2721 | + elif index == 1: |
2722 | + do_something_else() |
2723 | + elif index == 2: |
2724 | + do_some_other_thing() |
2725 | + elif index == 3: |
2726 | + do_something_completely_different() |
2727 | + |
2728 | + instead of code like this:: |
2729 | + |
2730 | + try: |
2731 | + index = p.expect (['good', 'bad']) |
2732 | + if index == 0: |
2733 | + do_something() |
2734 | + elif index == 1: |
2735 | + do_something_else() |
2736 | + except EOF: |
2737 | + do_some_other_thing() |
2738 | + except TIMEOUT: |
2739 | + do_something_completely_different() |
2740 | + |
2741 | + These two forms are equivalent. It all depends on what you want. You |
2742 | + can also just expect the EOF if you are waiting for all output of a |
2743 | + child to finish. For example:: |
2744 | + |
2745 | + p = pexpect.spawn('/bin/ls') |
2746 | + p.expect (pexpect.EOF) |
2747 | + print p.before |
2748 | + |
2749 | + If you are trying to optimize for speed then see expect_list(). |
2750 | + """ |
2751 | + |
2752 | + compiled_pattern_list = self.compile_pattern_list(pattern) |
2753 | + return self.expect_list(compiled_pattern_list, timeout, searchwindowsize) |
2754 | + |
2755 | + def expect_list(self, pattern_list, timeout = -1, searchwindowsize = -1): |
2756 | + |
2757 | + """This takes a list of compiled regular expressions and returns the |
2758 | + index into the pattern_list that matched the child output. The list may |
2759 | + also contain EOF or TIMEOUT (which are not compiled regular |
2760 | + expressions). This method is similar to the expect() method except that |
2761 | + expect_list() does not recompile the pattern list on every call. This |
2762 | + may help if you are trying to optimize for speed, otherwise just use |
2763 | + the expect() method. This is called by expect(). If timeout==-1 then |
2764 | + the self.timeout value is used. If searchwindowsize==-1 then the |
2765 | + self.searchwindowsize value is used. """ |
2766 | + |
2767 | + return self.expect_loop(searcher_re(pattern_list), timeout, searchwindowsize) |
2768 | + |
2769 | + def expect_exact(self, pattern_list, timeout = -1, searchwindowsize = -1): |
2770 | + |
2771 | + """This is similar to expect(), but uses plain string matching instead |
2772 | + of compiled regular expressions in 'pattern_list'. The 'pattern_list' |
2773 | + may be a string; a list or other sequence of strings; or TIMEOUT and |
2774 | + EOF. |
2775 | + |
2776 | + This call might be faster than expect() for two reasons: string |
2777 | + searching is faster than RE matching and it is possible to limit the |
2778 | + search to just the end of the input buffer. |
2779 | + |
2780 | + This method is also useful when you don't want to have to worry about |
2781 | + escaping regular expression characters that you want to match.""" |
2782 | + |
2783 | + if type(pattern_list) in types.StringTypes or pattern_list in (TIMEOUT, EOF): |
2784 | + pattern_list = [pattern_list] |
2785 | + return self.expect_loop(searcher_string(pattern_list), timeout, searchwindowsize) |
2786 | + |
2787 | + def expect_loop(self, searcher, timeout = -1, searchwindowsize = -1): |
2788 | + |
2789 | + """This is the common loop used inside expect. The 'searcher' should be |
2790 | + an instance of searcher_re or searcher_string, which describes how and what |
2791 | + to search for in the input. |
2792 | + |
2793 | + See expect() for other arguments, return value and exceptions. """ |
2794 | + |
2795 | + self.searcher = searcher |
2796 | + |
2797 | + if timeout == -1: |
2798 | + timeout = self.timeout |
2799 | + if timeout is not None: |
2800 | + end_time = time.time() + timeout |
2801 | + if searchwindowsize == -1: |
2802 | + searchwindowsize = self.searchwindowsize |
2803 | + |
2804 | + try: |
2805 | + incoming = self.buffer |
2806 | + freshlen = len(incoming) |
2807 | + while True: # Keep reading until exception or return. |
2808 | + index = searcher.search(incoming, freshlen, searchwindowsize) |
2809 | + if index >= 0: |
2810 | + self.buffer = incoming[searcher.end : ] |
2811 | + self.before = incoming[ : searcher.start] |
2812 | + self.after = incoming[searcher.start : searcher.end] |
2813 | + self.match = searcher.match |
2814 | + self.match_index = index |
2815 | + return self.match_index |
2816 | + # No match at this point |
2817 | + if timeout < 0 and timeout is not None: |
2818 | + raise TIMEOUT ('Timeout exceeded in expect_any().') |
2819 | + # Still have time left, so read more data |
2820 | + c = self.read_nonblocking (self.maxread, timeout) |
2821 | + freshlen = len(c) |
2822 | + time.sleep (0.0001) |
2823 | + incoming = incoming + c |
2824 | + if timeout is not None: |
2825 | + timeout = end_time - time.time() |
2826 | + except EOF, e: |
2827 | + self.buffer = '' |
2828 | + self.before = incoming |
2829 | + self.after = EOF |
2830 | + index = searcher.eof_index |
2831 | + if index >= 0: |
2832 | + self.match = EOF |
2833 | + self.match_index = index |
2834 | + return self.match_index |
2835 | + else: |
2836 | + self.match = None |
2837 | + self.match_index = None |
2838 | + raise EOF (str(e) + '\n' + str(self)) |
2839 | + except TIMEOUT, e: |
2840 | + self.buffer = incoming |
2841 | + self.before = incoming |
2842 | + self.after = TIMEOUT |
2843 | + index = searcher.timeout_index |
2844 | + if index >= 0: |
2845 | + self.match = TIMEOUT |
2846 | + self.match_index = index |
2847 | + return self.match_index |
2848 | + else: |
2849 | + self.match = None |
2850 | + self.match_index = None |
2851 | + raise TIMEOUT (str(e) + '\n' + str(self)) |
2852 | + except Exception: |
2853 | + self.before = incoming |
2854 | + self.after = None |
2855 | + self.match = None |
2856 | + self.match_index = None |
2857 | + raise |
2858 | + |
2859 | + def getwinsize(self): |
2860 | + |
2861 | + """This returns the terminal window size of the child tty. The return |
2862 | + value is a tuple of (rows, cols). """ |
2863 | + |
2864 | + TIOCGWINSZ = getattr(termios, 'TIOCGWINSZ', 1074295912L) |
2865 | + s = struct.pack('HHHH', 0, 0, 0, 0) |
2866 | + x = fcntl.ioctl(self.fileno(), TIOCGWINSZ, s) |
2867 | + return struct.unpack('HHHH', x)[0:2] |
2868 | + |
2869 | + def setwinsize(self, r, c): |
2870 | + |
2871 | + """This sets the terminal window size of the child tty. This will cause |
2872 | + a SIGWINCH signal to be sent to the child. This does not change the |
2873 | + physical window size. It changes the size reported to TTY-aware |
2874 | + applications like vi or curses -- applications that respond to the |
2875 | + SIGWINCH signal. """ |
2876 | + |
2877 | + # Check for buggy platforms. Some Python versions on some platforms |
2878 | + # (notably OSF1 Alpha and RedHat 7.1) truncate the value for |
2879 | + # termios.TIOCSWINSZ. It is not clear why this happens. |
2880 | + # These platforms don't seem to handle the signed int very well; |
2881 | + # yet other platforms like OpenBSD have a large negative value for |
2882 | + # TIOCSWINSZ and they don't have a truncate problem. |
2883 | + # Newer versions of Linux have totally different values for TIOCSWINSZ. |
2884 | + # Note that this fix is a hack. |
2885 | + TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561) |
2886 | + if TIOCSWINSZ == 2148037735L: # L is not required in Python >= 2.2. |
2887 | + TIOCSWINSZ = -2146929561 # Same bits, but with sign. |
2888 | + # Note, assume ws_xpixel and ws_ypixel are zero. |
2889 | + s = struct.pack('HHHH', r, c, 0, 0) |
2890 | + fcntl.ioctl(self.fileno(), TIOCSWINSZ, s) |
2891 | + |
2892 | + def interact(self, escape_character = chr(29), input_filter = None, output_filter = None): |
2893 | + |
2894 | + """This gives control of the child process to the interactive user (the |
2895 | + human at the keyboard). Keystrokes are sent to the child process, and |
2896 | + the stdout and stderr output of the child process is printed. This |
2897 | + simply echos the child stdout and child stderr to the real stdout and |
2898 | + it echos the real stdin to the child stdin. When the user types the |
2899 | + escape_character this method will stop. The default for |
2900 | + escape_character is ^]. This should not be confused with ASCII 27 -- |
2901 | + the ESC character. ASCII 29 was chosen for historical merit because |
2902 | + this is the character used by 'telnet' as the escape character. The |
2903 | + escape_character will not be sent to the child process. |
2904 | + |
2905 | + You may pass in optional input and output filter functions. These |
2906 | + functions should take a string and return a string. The output_filter |
2907 | + will be passed all the output from the child process. The input_filter |
2908 | + will be passed all the keyboard input from the user. The input_filter |
2909 | + is run BEFORE the check for the escape_character. |
2910 | + |
2911 | + Note that if you change the window size of the parent the SIGWINCH |
2912 | + signal will not be passed through to the child. If you want the child |
2913 | + window size to change when the parent's window size changes then do |
2914 | + something like the following example:: |
2915 | + |
2916 | + import pexpect, struct, fcntl, termios, signal, sys |
2917 | + def sigwinch_passthrough (sig, data): |
2918 | + s = struct.pack("HHHH", 0, 0, 0, 0) |
2919 | + a = struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ , s)) |
2920 | + global p |
2921 | + p.setwinsize(a[0],a[1]) |
2922 | + p = pexpect.spawn('/bin/bash') # Note this is global and used in sigwinch_passthrough. |
2923 | + signal.signal(signal.SIGWINCH, sigwinch_passthrough) |
2924 | + p.interact() |
2925 | + """ |
2926 | + |
2927 | + # Flush the buffer. |
2928 | + self.stdout.write (self.buffer) |
2929 | + self.stdout.flush() |
2930 | + self.buffer = '' |
2931 | + mode = tty.tcgetattr(self.STDIN_FILENO) |
2932 | + tty.setraw(self.STDIN_FILENO) |
2933 | + try: |
2934 | + self.__interact_copy(escape_character, input_filter, output_filter) |
2935 | + finally: |
2936 | + tty.tcsetattr(self.STDIN_FILENO, tty.TCSAFLUSH, mode) |
2937 | + |
2938 | + def __interact_writen(self, fd, data): |
2939 | + |
2940 | + """This is used by the interact() method. |
2941 | + """ |
2942 | + |
2943 | + while data != '' and self.isalive(): |
2944 | + n = os.write(fd, data) |
2945 | + data = data[n:] |
2946 | + |
2947 | + def __interact_read(self, fd): |
2948 | + |
2949 | + """This is used by the interact() method. |
2950 | + """ |
2951 | + |
2952 | + return os.read(fd, 1000) |
2953 | + |
2954 | + def __interact_copy(self, escape_character = None, input_filter = None, output_filter = None): |
2955 | + |
2956 | + """This is used by the interact() method. |
2957 | + """ |
2958 | + |
2959 | + while self.isalive(): |
2960 | + r,w,e = self.__select([self.child_fd, self.STDIN_FILENO], [], []) #@UnusedVariable |
2961 | + if self.child_fd in r: |
2962 | + data = self.__interact_read(self.child_fd) |
2963 | + if output_filter: data = output_filter(data) |
2964 | + if self.logfile is not None: |
2965 | + self.logfile.write (data) |
2966 | + self.logfile.flush() |
2967 | + os.write(self.STDOUT_FILENO, data) |
2968 | + if self.STDIN_FILENO in r: |
2969 | + data = self.__interact_read(self.STDIN_FILENO) |
2970 | + if input_filter: data = input_filter(data) |
2971 | + i = data.rfind(escape_character) |
2972 | + if i != -1: |
2973 | + data = data[:i] |
2974 | + self.__interact_writen(self.child_fd, data) |
2975 | + break |
2976 | + self.__interact_writen(self.child_fd, data) |
2977 | + |
2978 | + def __select (self, iwtd, owtd, ewtd, timeout=None): |
2979 | + |
2980 | + """This is a wrapper around select.select() that ignores signals. If |
2981 | + select.select raises a select.error exception and errno is an EINTR |
2982 | + error then it is ignored. Mainly this is used to ignore sigwinch |
2983 | + (terminal resize). """ |
2984 | + |
2985 | + # if select() is interrupted by a signal (errno==EINTR) then |
2986 | + # we loop back and enter the select() again. |
2987 | + if timeout is not None: |
2988 | + end_time = time.time() + timeout |
2989 | + while True: |
2990 | + try: |
2991 | + return select.select (iwtd, owtd, ewtd, timeout) |
2992 | + except select.error, e: |
2993 | + if e[0] == errno.EINTR: |
2994 | + # if we loop back we have to subtract the amount of time we already waited. |
2995 | + if timeout is not None: |
2996 | + timeout = end_time - time.time() |
2997 | + if timeout < 0: |
2998 | + return ([],[],[]) |
2999 | + else: # something else caused the select.error, so this really is an exception |
3000 | + raise |
3001 | + |
3002 | +############################################################################## |
3003 | +# The following methods are no longer supported or allowed. |
3004 | + |
3005 | + def setmaxread (self, maxread): |
3006 | + |
3007 | + """This method is no longer supported or allowed. I don't like getters |
3008 | + and setters without a good reason. """ |
3009 | + |
3010 | + raise ExceptionPexpect ('This method is no longer supported or allowed. Just assign a value to the maxread member variable.') |
3011 | + |
3012 | + def setlog (self, fileobject): |
3013 | + |
3014 | + """This method is no longer supported or allowed. |
3015 | + """ |
3016 | + |
3017 | + raise ExceptionPexpect ('This method is no longer supported or allowed. Just assign a value to the logfile member variable.') |
3018 | + |
3019 | +############################################################################## |
3020 | +# End of spawn class |
3021 | +############################################################################## |
3022 | + |
3023 | +class searcher_string (object): |
3024 | + |
3025 | + """This is a plain string search helper for the spawn.expect_any() method. |
3026 | + |
3027 | + Attributes: |
3028 | + |
3029 | + eof_index - index of EOF, or -1 |
3030 | + timeout_index - index of TIMEOUT, or -1 |
3031 | + |
3032 | + After a successful match by the search() method the following attributes |
3033 | + are available: |
3034 | + |
3035 | + start - index into the buffer, first byte of match |
3036 | + end - index into the buffer, first byte after match |
3037 | + match - the matching string itself |
3038 | + """ |
3039 | + |
3040 | + def __init__(self, strings): |
3041 | + |
3042 | + """This creates an instance of searcher_string. This argument 'strings' |
3043 | + may be a list; a sequence of strings; or the EOF or TIMEOUT types. """ |
3044 | + |
3045 | + self.eof_index = -1 |
3046 | + self.timeout_index = -1 |
3047 | + self._strings = [] |
3048 | + for n, s in zip(range(len(strings)), strings): |
3049 | + if s is EOF: |
3050 | + self.eof_index = n |
3051 | + continue |
3052 | + if s is TIMEOUT: |
3053 | + self.timeout_index = n |
3054 | + continue |
3055 | + self._strings.append((n, s)) |
3056 | + |
3057 | + def __str__(self): |
3058 | + |
3059 | + """This returns a human-readable string that represents the state of |
3060 | + the object.""" |
3061 | + |
3062 | + ss = [ (ns[0],' %d: "%s"' % ns) for ns in self._strings ] |
3063 | + ss.append((-1,'searcher_string:')) |
3064 | + if self.eof_index >= 0: |
3065 | + ss.append ((self.eof_index,' %d: EOF' % self.eof_index)) |
3066 | + if self.timeout_index >= 0: |
3067 | + ss.append ((self.timeout_index,' %d: TIMEOUT' % self.timeout_index)) |
3068 | + ss.sort() |
3069 | + ss = zip(*ss)[1] |
3070 | + return '\n'.join(ss) |
3071 | + |
3072 | + def search(self, buffer, freshlen, searchwindowsize=None): |
3073 | + |
3074 | + """This searches 'buffer' for the first occurence of one of the search |
3075 | + strings. 'freshlen' must indicate the number of bytes at the end of |
3076 | + 'buffer' which have not been searched before. It helps to avoid |
3077 | + searching the same, possibly big, buffer over and over again. |
3078 | + |
3079 | + See class spawn for the 'searchwindowsize' argument. |
3080 | + |
3081 | + If there is a match this returns the index of that string, and sets |
3082 | + 'start', 'end' and 'match'. Otherwise, this returns -1. """ |
3083 | + |
3084 | + absurd_match = len(buffer) |
3085 | + first_match = absurd_match |
3086 | + |
3087 | + # 'freshlen' helps a lot here. Further optimizations could |
3088 | + # possibly include: |
3089 | + # |
3090 | + # using something like the Boyer-Moore Fast String Searching |
3091 | + # Algorithm; pre-compiling the search through a list of |
3092 | + # strings into something that can scan the input once to |
3093 | + # search for all N strings; realize that if we search for |
3094 | + # ['bar', 'baz'] and the input is '...foo' we need not bother |
3095 | + # rescanning until we've read three more bytes. |
3096 | + # |
3097 | + # Sadly, I don't know enough about this interesting topic. /grahn |
3098 | + |
3099 | + for index, s in self._strings: |
3100 | + if searchwindowsize is None: |
3101 | + # the match, if any, can only be in the fresh data, |
3102 | + # or at the very end of the old data |
3103 | + offset = -(freshlen+len(s)) |
3104 | + else: |
3105 | + # better obey searchwindowsize |
3106 | + offset = -searchwindowsize |
3107 | + n = buffer.find(s, offset) |
3108 | + if n >= 0 and n < first_match: |
3109 | + first_match = n |
3110 | + best_index, best_match = index, s |
3111 | + if first_match == absurd_match: |
3112 | + return -1 |
3113 | + self.match = best_match |
3114 | + self.start = first_match |
3115 | + self.end = self.start + len(self.match) |
3116 | + return best_index |
3117 | + |
3118 | +class searcher_re (object): |
3119 | + |
3120 | + """This is regular expression string search helper for the |
3121 | + spawn.expect_any() method. |
3122 | + |
3123 | + Attributes: |
3124 | + |
3125 | + eof_index - index of EOF, or -1 |
3126 | + timeout_index - index of TIMEOUT, or -1 |
3127 | + |
3128 | + After a successful match by the search() method the following attributes |
3129 | + are available: |
3130 | + |
3131 | + start - index into the buffer, first byte of match |
3132 | + end - index into the buffer, first byte after match |
3133 | + match - the re.match object returned by a succesful re.search |
3134 | + |
3135 | + """ |
3136 | + |
3137 | + def __init__(self, patterns): |
3138 | + |
3139 | + """This creates an instance that searches for 'patterns' Where |
3140 | + 'patterns' may be a list or other sequence of compiled regular |
3141 | + expressions, or the EOF or TIMEOUT types.""" |
3142 | + |
3143 | + self.eof_index = -1 |
3144 | + self.timeout_index = -1 |
3145 | + self._searches = [] |
3146 | + for n, s in zip(range(len(patterns)), patterns): |
3147 | + if s is EOF: |
3148 | + self.eof_index = n |
3149 | + continue |
3150 | + if s is TIMEOUT: |
3151 | + self.timeout_index = n |
3152 | + continue |
3153 | + self._searches.append((n, s)) |
3154 | + |
3155 | + def __str__(self): |
3156 | + |
3157 | + """This returns a human-readable string that represents the state of |
3158 | + the object.""" |
3159 | + |
3160 | + ss = [ (n,' %d: re.compile("%s")' % (n,str(s.pattern))) for n,s in self._searches] |
3161 | + ss.append((-1,'searcher_re:')) |
3162 | + if self.eof_index >= 0: |
3163 | + ss.append ((self.eof_index,' %d: EOF' % self.eof_index)) |
3164 | + if self.timeout_index >= 0: |
3165 | + ss.append ((self.timeout_index,' %d: TIMEOUT' % self.timeout_index)) |
3166 | + ss.sort() |
3167 | + ss = zip(*ss)[1] |
3168 | + return '\n'.join(ss) |
3169 | + |
3170 | + def search(self, buffer, freshlen, searchwindowsize=None): |
3171 | + |
3172 | + """This searches 'buffer' for the first occurence of one of the regular |
3173 | + expressions. 'freshlen' must indicate the number of bytes at the end of |
3174 | + 'buffer' which have not been searched before. |
3175 | + |
3176 | + See class spawn for the 'searchwindowsize' argument. |
3177 | + |
3178 | + If there is a match this returns the index of that string, and sets |
3179 | + 'start', 'end' and 'match'. Otherwise, returns -1.""" |
3180 | + |
3181 | + absurd_match = len(buffer) |
3182 | + first_match = absurd_match |
3183 | + # 'freshlen' doesn't help here -- we cannot predict the |
3184 | + # length of a match, and the re module provides no help. |
3185 | + if searchwindowsize is None: |
3186 | + searchstart = 0 |
3187 | + else: |
3188 | + searchstart = max(0, len(buffer)-searchwindowsize) |
3189 | + for index, s in self._searches: |
3190 | + match = s.search(buffer, searchstart) |
3191 | + if match is None: |
3192 | + continue |
3193 | + n = match.start() |
3194 | + if n < first_match: |
3195 | + first_match = n |
3196 | + the_match = match |
3197 | + best_index = index |
3198 | + if first_match == absurd_match: |
3199 | + return -1 |
3200 | + self.start = first_match |
3201 | + self.match = the_match |
3202 | + self.end = self.match.end() |
3203 | + return best_index |
3204 | + |
3205 | +def which (filename): |
3206 | + |
3207 | + """This takes a given filename; tries to find it in the environment path; |
3208 | + then checks if it is executable. This returns the full path to the filename |
3209 | + if found and executable. Otherwise this returns None.""" |
3210 | + |
3211 | + # Special case where filename already contains a path. |
3212 | + if os.path.dirname(filename) != '': |
3213 | + if os.access (filename, os.X_OK): |
3214 | + return filename |
3215 | + |
3216 | + if not os.environ.has_key('PATH') or os.environ['PATH'] == '': |
3217 | + p = os.defpath |
3218 | + else: |
3219 | + p = os.environ['PATH'] |
3220 | + |
3221 | + # Oddly enough this was the one line that made Pexpect |
3222 | + # incompatible with Python 1.5.2. |
3223 | + #pathlist = p.split (os.pathsep) |
3224 | + pathlist = string.split (p, os.pathsep) |
3225 | + |
3226 | + for path in pathlist: |
3227 | + f = os.path.join(path, filename) |
3228 | + if os.access(f, os.X_OK): |
3229 | + return f |
3230 | + return None |
3231 | + |
3232 | +def split_command_line(command_line): |
3233 | + |
3234 | + """This splits a command line into a list of arguments. It splits arguments |
3235 | + on spaces, but handles embedded quotes, doublequotes, and escaped |
3236 | + characters. It's impossible to do this with a regular expression, so I |
3237 | + wrote a little state machine to parse the command line. """ |
3238 | + |
3239 | + arg_list = [] |
3240 | + arg = '' |
3241 | + |
3242 | + # Constants to name the states we can be in. |
3243 | + state_basic = 0 |
3244 | + state_esc = 1 |
3245 | + state_singlequote = 2 |
3246 | + state_doublequote = 3 |
3247 | + state_whitespace = 4 # The state of consuming whitespace between commands. |
3248 | + state = state_basic |
3249 | + |
3250 | + for c in command_line: |
3251 | + if state == state_basic or state == state_whitespace: |
3252 | + if c == '\\': # Escape the next character |
3253 | + state = state_esc |
3254 | + elif c == r"'": # Handle single quote |
3255 | + state = state_singlequote |
3256 | + elif c == r'"': # Handle double quote |
3257 | + state = state_doublequote |
3258 | + elif c.isspace(): |
3259 | + # Add arg to arg_list if we aren't in the middle of whitespace. |
3260 | + if state == state_whitespace: |
3261 | + None # Do nothing. |
3262 | + else: |
3263 | + arg_list.append(arg) |
3264 | + arg = '' |
3265 | + state = state_whitespace |
3266 | + else: |
3267 | + arg = arg + c |
3268 | + state = state_basic |
3269 | + elif state == state_esc: |
3270 | + arg = arg + c |
3271 | + state = state_basic |
3272 | + elif state == state_singlequote: |
3273 | + if c == r"'": |
3274 | + state = state_basic |
3275 | + else: |
3276 | + arg = arg + c |
3277 | + elif state == state_doublequote: |
3278 | + if c == r'"': |
3279 | + state = state_basic |
3280 | + else: |
3281 | + arg = arg + c |
3282 | + |
3283 | + if arg != '': |
3284 | + arg_list.append(arg) |
3285 | + return arg_list |
3286 | + |
3287 | +# vi:ts=4:sw=4:expandtab:ft=python: |
Thanks for the help! All tests passed for both.
I made a couple of cosmetic changes to continue the file naming convention, otherwise, just left it alone. Good job.