pec2 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/pec2/cli.rb +1 -9
- data/lib/pec2/pssh.rb +58 -28
- data/lib/pec2/version.rb +1 -1
- data/pec2.gemspec +4 -1
- data/spec/cli_spec.rb +0 -1
- data/spec/pssh_spec.rb +1 -54
- metadata +46 -24
- data/exe/bin/pnuke +0 -96
- data/exe/bin/prsync +0 -126
- data/exe/bin/pscp +0 -108
- data/exe/bin/pslurp +0 -129
- data/exe/bin/pssh +0 -118
- data/exe/bin/pssh-askpass +0 -11
- data/exe/man/man1/pnuke.1 +0 -268
- data/exe/man/man1/prsync.1 +0 -299
- data/exe/man/man1/pscp.1 +0 -271
- data/exe/man/man1/pslurp.1 +0 -280
- data/exe/man/man1/pssh.1 +0 -368
- data/exe/psshlib/__init__.py +0 -0
- data/exe/psshlib/askpass_client.py +0 -102
- data/exe/psshlib/askpass_server.py +0 -101
- data/exe/psshlib/cli.py +0 -110
- data/exe/psshlib/color.py +0 -39
- data/exe/psshlib/manager.py +0 -345
- data/exe/psshlib/psshutil.py +0 -108
- data/exe/psshlib/task.py +0 -288
- data/exe/psshlib/version.py +0 -1
@@ -1,101 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
# -*- Mode: python -*-
|
3
|
-
|
4
|
-
# Copyright (c) 2009-2012, Andrew McNabb
|
5
|
-
|
6
|
-
"""Sends the password over a socket to askpass.
|
7
|
-
"""
|
8
|
-
|
9
|
-
import errno
|
10
|
-
import getpass
|
11
|
-
import os
|
12
|
-
import socket
|
13
|
-
import sys
|
14
|
-
import tempfile
|
15
|
-
import textwrap
|
16
|
-
|
17
|
-
from psshlib import psshutil
|
18
|
-
|
19
|
-
|
20
|
-
class PasswordServer(object):
|
21
|
-
"""Listens on a UNIX domain socket for password requests."""
|
22
|
-
def __init__(self):
|
23
|
-
self.sock = None
|
24
|
-
self.tempdir = None
|
25
|
-
self.address = None
|
26
|
-
self.socketmap = {}
|
27
|
-
self.buffermap = {}
|
28
|
-
|
29
|
-
def start(self, iomap, backlog):
|
30
|
-
"""Prompts for the password, creates a socket, and starts listening.
|
31
|
-
|
32
|
-
The specified backlog should be the max number of clients connecting
|
33
|
-
at once.
|
34
|
-
"""
|
35
|
-
message = ('Warning: do not enter your password if anyone else has'
|
36
|
-
' superuser privileges or access to your account.')
|
37
|
-
print(textwrap.fill(message))
|
38
|
-
|
39
|
-
self.password = getpass.getpass()
|
40
|
-
|
41
|
-
# Note that according to the docs for mkdtemp, "The directory is
|
42
|
-
# readable, writable, and searchable only by the creating user."
|
43
|
-
self.tempdir = tempfile.mkdtemp(prefix='pssh.')
|
44
|
-
self.address = os.path.join(self.tempdir, 'pssh_askpass_socket')
|
45
|
-
self.sock = socket.socket(socket.AF_UNIX)
|
46
|
-
psshutil.set_cloexec(self.sock)
|
47
|
-
self.sock.bind(self.address)
|
48
|
-
self.sock.listen(backlog)
|
49
|
-
iomap.register_read(self.sock.fileno(), self.handle_listen)
|
50
|
-
|
51
|
-
def handle_listen(self, fd, iomap):
|
52
|
-
try:
|
53
|
-
conn = self.sock.accept()[0]
|
54
|
-
except socket.error:
|
55
|
-
_, e, _ = sys.exc_info()
|
56
|
-
number = e.args[0]
|
57
|
-
if number == errno.EINTR:
|
58
|
-
return
|
59
|
-
else:
|
60
|
-
# TODO: print an error message here?
|
61
|
-
self.sock.close()
|
62
|
-
self.sock = None
|
63
|
-
fd = conn.fileno()
|
64
|
-
iomap.register_write(fd, self.handle_write)
|
65
|
-
self.socketmap[fd] = conn
|
66
|
-
self.buffermap[fd] = self.password
|
67
|
-
|
68
|
-
def handle_write(self, fd, iomap):
|
69
|
-
buffer = self.buffermap[fd]
|
70
|
-
conn = self.socketmap[fd]
|
71
|
-
try:
|
72
|
-
bytes_written = conn.send(buffer)
|
73
|
-
except socket.error:
|
74
|
-
_, e, _ = sys.exc_info()
|
75
|
-
number = e.args[0]
|
76
|
-
if number == errno.EINTR:
|
77
|
-
return
|
78
|
-
else:
|
79
|
-
self.close_socket(fd, iomap)
|
80
|
-
|
81
|
-
buffer = buffer[bytes_written:]
|
82
|
-
if buffer:
|
83
|
-
self.buffermap[fd] = buffer
|
84
|
-
else:
|
85
|
-
self.close_socket(fd, iomap)
|
86
|
-
|
87
|
-
def close_socket(self, fd, iomap):
|
88
|
-
iomap.unregister(fd)
|
89
|
-
self.socketmap[fd].close()
|
90
|
-
del self.socketmap[fd]
|
91
|
-
del self.buffermap[fd]
|
92
|
-
|
93
|
-
def __del__(self):
|
94
|
-
if self.sock:
|
95
|
-
self.sock.close()
|
96
|
-
self.sock = None
|
97
|
-
if self.address:
|
98
|
-
os.remove(self.address)
|
99
|
-
if self.tempdir:
|
100
|
-
os.rmdir(self.tempdir)
|
101
|
-
|
data/exe/psshlib/cli.py
DELETED
@@ -1,110 +0,0 @@
|
|
1
|
-
# Copyright (c) 2009-2012, Andrew McNabb
|
2
|
-
# Copyright (c) 2003-2008, Brent N. Chun
|
3
|
-
|
4
|
-
import optparse
|
5
|
-
import os
|
6
|
-
import shlex
|
7
|
-
import sys
|
8
|
-
import textwrap
|
9
|
-
import version
|
10
|
-
|
11
|
-
_DEFAULT_PARALLELISM = 32
|
12
|
-
_DEFAULT_TIMEOUT = 0 # "infinity" by default
|
13
|
-
|
14
|
-
|
15
|
-
def common_parser():
|
16
|
-
"""
|
17
|
-
Create a basic OptionParser with arguments common to all pssh programs.
|
18
|
-
"""
|
19
|
-
# The "resolve" conflict handler avoids errors from the hosts option
|
20
|
-
# conflicting with the help option.
|
21
|
-
parser = optparse.OptionParser(conflict_handler='resolve',
|
22
|
-
version=version.VERSION)
|
23
|
-
# Ensure that options appearing after the command are sent to ssh.
|
24
|
-
parser.disable_interspersed_args()
|
25
|
-
parser.epilog = "Example: pssh -h nodes.txt -l irb2 -o /tmp/foo uptime"
|
26
|
-
|
27
|
-
parser.add_option('-h', '--hosts', dest='host_files', action='append',
|
28
|
-
metavar='HOST_FILE',
|
29
|
-
help='hosts file (each line "[user@]host[:port]")')
|
30
|
-
parser.add_option('-H', '--host', dest='host_strings', action='append',
|
31
|
-
metavar='HOST_STRING',
|
32
|
-
help='additional host entries ("[user@]host[:port]")')
|
33
|
-
parser.add_option('-l', '--user', dest='user',
|
34
|
-
help='username (OPTIONAL)')
|
35
|
-
parser.add_option('-p', '--par', dest='par', type='int',
|
36
|
-
help='max number of parallel threads (OPTIONAL)')
|
37
|
-
parser.add_option('-o', '--outdir', dest='outdir',
|
38
|
-
help='output directory for stdout files (OPTIONAL)')
|
39
|
-
parser.add_option('-e', '--errdir', dest='errdir',
|
40
|
-
help='output directory for stderr files (OPTIONAL)')
|
41
|
-
parser.add_option('-t', '--timeout', dest='timeout', type='int',
|
42
|
-
help='timeout (secs) (0 = no timeout) per host (OPTIONAL)')
|
43
|
-
parser.add_option('-O', '--option', dest='options', action='append',
|
44
|
-
metavar='OPTION', help='SSH option (OPTIONAL)')
|
45
|
-
parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
|
46
|
-
help='turn on warning and diagnostic messages (OPTIONAL)')
|
47
|
-
parser.add_option('-A', '--askpass', dest='askpass', action='store_true',
|
48
|
-
help='Ask for a password (OPTIONAL)')
|
49
|
-
parser.add_option('-x', '--extra-args', action='callback', type='string',
|
50
|
-
metavar='ARGS', callback=shlex_append, dest='extra',
|
51
|
-
help='Extra command-line arguments, with processing for '
|
52
|
-
'spaces, quotes, and backslashes')
|
53
|
-
parser.add_option('-X', '--extra-arg', dest='extra', action='append',
|
54
|
-
metavar='ARG', help='Extra command-line argument')
|
55
|
-
|
56
|
-
return parser
|
57
|
-
|
58
|
-
|
59
|
-
def common_defaults(**kwargs):
|
60
|
-
defaults = dict(par=_DEFAULT_PARALLELISM, timeout=_DEFAULT_TIMEOUT)
|
61
|
-
defaults.update(**kwargs)
|
62
|
-
envvars = [('user', 'PSSH_USER'),
|
63
|
-
('par', 'PSSH_PAR'),
|
64
|
-
('outdir', 'PSSH_OUTDIR'),
|
65
|
-
('errdir', 'PSSH_ERRDIR'),
|
66
|
-
('timeout', 'PSSH_TIMEOUT'),
|
67
|
-
('verbose', 'PSSH_VERBOSE'),
|
68
|
-
('print_out', 'PSSH_PRINT'),
|
69
|
-
('askpass', 'PSSH_ASKPASS'),
|
70
|
-
('inline', 'PSSH_INLINE'),
|
71
|
-
('recursive', 'PSSH_RECURSIVE'),
|
72
|
-
('archive', 'PSSH_ARCHIVE'),
|
73
|
-
('compress', 'PSSH_COMPRESS'),
|
74
|
-
('localdir', 'PSSH_LOCALDIR'),
|
75
|
-
]
|
76
|
-
for option, var, in envvars:
|
77
|
-
value = os.getenv(var)
|
78
|
-
if value:
|
79
|
-
defaults[option] = value
|
80
|
-
|
81
|
-
value = os.getenv('PSSH_OPTIONS')
|
82
|
-
if value:
|
83
|
-
defaults['options'] = [value]
|
84
|
-
|
85
|
-
value = os.getenv('PSSH_HOSTS')
|
86
|
-
if value:
|
87
|
-
message1 = ('Warning: the PSSH_HOSTS environment variable is '
|
88
|
-
'deprecated. Please use the "-h" option instead, and consider '
|
89
|
-
'creating aliases for convenience. For example:')
|
90
|
-
message2 = " alias pssh_abc='pssh -h /path/to/hosts_abc'"
|
91
|
-
sys.stderr.write(textwrap.fill(message1))
|
92
|
-
sys.stderr.write('\n')
|
93
|
-
sys.stderr.write(message2)
|
94
|
-
sys.stderr.write('\n')
|
95
|
-
defaults['host_files'] = [value]
|
96
|
-
|
97
|
-
return defaults
|
98
|
-
|
99
|
-
|
100
|
-
def shlex_append(option, opt_str, value, parser):
|
101
|
-
"""An optparse callback similar to the append action.
|
102
|
-
|
103
|
-
The given value is processed with shlex, and the resulting list is
|
104
|
-
concatenated to the option's dest list.
|
105
|
-
"""
|
106
|
-
lst = getattr(parser.values, option.dest)
|
107
|
-
if lst is None:
|
108
|
-
lst = []
|
109
|
-
setattr(parser.values, option.dest, lst)
|
110
|
-
lst.extend(shlex.split(value))
|
data/exe/psshlib/color.py
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# Copyright (c) 2009-2012, Andrew McNabb
|
2
|
-
# Copyright (c) 2003-2008, Brent N. Chun
|
3
|
-
|
4
|
-
def with_color(string, fg, bg=49):
|
5
|
-
'''Given foreground/background ANSI color codes, return a string that,
|
6
|
-
when printed, will format the supplied string using the supplied colors.
|
7
|
-
'''
|
8
|
-
return "\x1b[%dm\x1b[%dm%s\x1b[39m\x1b[49m" % (fg, bg, string)
|
9
|
-
|
10
|
-
def B(string):
|
11
|
-
'''Returns a string that, when printed, will display the supplied string
|
12
|
-
in ANSI bold.
|
13
|
-
'''
|
14
|
-
return "\x1b[1m%s\x1b[22m" % string
|
15
|
-
|
16
|
-
def r(string): return with_color(string, 31) # Red
|
17
|
-
def g(string): return with_color(string, 32) # Green
|
18
|
-
def y(string): return with_color(string, 33) # Yellow
|
19
|
-
def b(string): return with_color(string, 34) # Blue
|
20
|
-
def m(string): return with_color(string, 35) # Magenta
|
21
|
-
def c(string): return with_color(string, 36) # Cyan
|
22
|
-
def w(string): return with_color(string, 37) # White
|
23
|
-
|
24
|
-
#following from Python cookbook, #475186
|
25
|
-
def has_colors(stream):
|
26
|
-
'''Returns boolean indicating whether or not the supplied stream supports
|
27
|
-
ANSI color.
|
28
|
-
'''
|
29
|
-
if not hasattr(stream, "isatty"):
|
30
|
-
return False
|
31
|
-
if not stream.isatty():
|
32
|
-
return False # auto color only on TTYs
|
33
|
-
try:
|
34
|
-
import curses
|
35
|
-
curses.setupterm()
|
36
|
-
return curses.tigetnum("colors") > 2
|
37
|
-
except:
|
38
|
-
# guess false in case of error
|
39
|
-
return False
|
data/exe/psshlib/manager.py
DELETED
@@ -1,345 +0,0 @@
|
|
1
|
-
# Copyright (c) 2009-2012, Andrew McNabb
|
2
|
-
|
3
|
-
from errno import EINTR
|
4
|
-
import os
|
5
|
-
import select
|
6
|
-
import signal
|
7
|
-
import sys
|
8
|
-
import threading
|
9
|
-
|
10
|
-
try:
|
11
|
-
import queue
|
12
|
-
except ImportError:
|
13
|
-
import Queue as queue
|
14
|
-
|
15
|
-
from psshlib.askpass_server import PasswordServer
|
16
|
-
from psshlib import psshutil
|
17
|
-
|
18
|
-
READ_SIZE = 1 << 16
|
19
|
-
|
20
|
-
|
21
|
-
class FatalError(RuntimeError):
|
22
|
-
"""A fatal error in the PSSH Manager."""
|
23
|
-
pass
|
24
|
-
|
25
|
-
|
26
|
-
class Manager(object):
|
27
|
-
"""Executes tasks concurrently.
|
28
|
-
|
29
|
-
Tasks are added with add_task() and executed in parallel with run().
|
30
|
-
Returns a list of the exit statuses of the processes.
|
31
|
-
|
32
|
-
Arguments:
|
33
|
-
limit: Maximum number of commands running at once.
|
34
|
-
timeout: Maximum allowed execution time in seconds.
|
35
|
-
"""
|
36
|
-
def __init__(self, opts):
|
37
|
-
self.limit = opts.par
|
38
|
-
self.timeout = opts.timeout
|
39
|
-
self.askpass = opts.askpass
|
40
|
-
self.outdir = opts.outdir
|
41
|
-
self.errdir = opts.errdir
|
42
|
-
self.iomap = IOMap()
|
43
|
-
|
44
|
-
self.taskcount = 0
|
45
|
-
self.tasks = []
|
46
|
-
self.running = []
|
47
|
-
self.done = []
|
48
|
-
|
49
|
-
self.askpass_socket = None
|
50
|
-
|
51
|
-
def run(self):
|
52
|
-
"""Processes tasks previously added with add_task."""
|
53
|
-
try:
|
54
|
-
if self.outdir or self.errdir:
|
55
|
-
writer = Writer(self.outdir, self.errdir)
|
56
|
-
writer.start()
|
57
|
-
else:
|
58
|
-
writer = None
|
59
|
-
|
60
|
-
if self.askpass:
|
61
|
-
pass_server = PasswordServer()
|
62
|
-
pass_server.start(self.iomap, self.limit)
|
63
|
-
self.askpass_socket = pass_server.address
|
64
|
-
|
65
|
-
self.set_sigchld_handler()
|
66
|
-
|
67
|
-
try:
|
68
|
-
self.update_tasks(writer)
|
69
|
-
wait = None
|
70
|
-
while self.running or self.tasks:
|
71
|
-
# Opt for efficiency over subsecond timeout accuracy.
|
72
|
-
if wait is None or wait < 1:
|
73
|
-
wait = 1
|
74
|
-
self.iomap.poll(wait)
|
75
|
-
self.update_tasks(writer)
|
76
|
-
wait = self.check_timeout()
|
77
|
-
except KeyboardInterrupt:
|
78
|
-
# This exception handler tries to clean things up and prints
|
79
|
-
# out a nice status message for each interrupted host.
|
80
|
-
self.interrupted()
|
81
|
-
|
82
|
-
except KeyboardInterrupt:
|
83
|
-
# This exception handler doesn't print out any fancy status
|
84
|
-
# information--it just stops.
|
85
|
-
pass
|
86
|
-
|
87
|
-
if writer:
|
88
|
-
writer.signal_quit()
|
89
|
-
writer.join()
|
90
|
-
|
91
|
-
statuses = [task.exitstatus for task in self.done]
|
92
|
-
return statuses
|
93
|
-
|
94
|
-
def clear_sigchld_handler(self):
|
95
|
-
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
96
|
-
|
97
|
-
def set_sigchld_handler(self):
|
98
|
-
# TODO: find out whether set_wakeup_fd still works if the default
|
99
|
-
# signal handler is used (I'm pretty sure it doesn't work if the
|
100
|
-
# signal is ignored).
|
101
|
-
signal.signal(signal.SIGCHLD, self.handle_sigchld)
|
102
|
-
# This should keep reads and writes from getting EINTR.
|
103
|
-
if hasattr(signal, 'siginterrupt'):
|
104
|
-
signal.siginterrupt(signal.SIGCHLD, False)
|
105
|
-
|
106
|
-
def handle_sigchld(self, number, frame):
|
107
|
-
"""Apparently we need a sigchld handler to make set_wakeup_fd work."""
|
108
|
-
# Write to the signal pipe (only for Python <2.5, where the
|
109
|
-
# set_wakeup_fd method doesn't exist).
|
110
|
-
if self.iomap.wakeup_writefd:
|
111
|
-
os.write(self.iomap.wakeup_writefd, '\0')
|
112
|
-
for task in self.running:
|
113
|
-
if task.proc:
|
114
|
-
task.proc.poll()
|
115
|
-
# Apparently some UNIX systems automatically resent the SIGCHLD
|
116
|
-
# handler to SIG_DFL. Reset it just in case.
|
117
|
-
self.set_sigchld_handler()
|
118
|
-
|
119
|
-
def add_task(self, task):
|
120
|
-
"""Adds a Task to be processed with run()."""
|
121
|
-
self.tasks.append(task)
|
122
|
-
|
123
|
-
def update_tasks(self, writer):
|
124
|
-
"""Reaps tasks and starts as many new ones as allowed."""
|
125
|
-
# Mask signals to work around a Python bug:
|
126
|
-
# http://bugs.python.org/issue1068268
|
127
|
-
# Since sigprocmask isn't in the stdlib, clear the SIGCHLD handler.
|
128
|
-
# Since signals are masked, reap_tasks needs to be called once for
|
129
|
-
# each loop.
|
130
|
-
keep_running = True
|
131
|
-
while keep_running:
|
132
|
-
self.clear_sigchld_handler()
|
133
|
-
self._start_tasks_once(writer)
|
134
|
-
self.set_sigchld_handler()
|
135
|
-
keep_running = self.reap_tasks()
|
136
|
-
|
137
|
-
def _start_tasks_once(self, writer):
|
138
|
-
"""Starts tasks once.
|
139
|
-
|
140
|
-
Due to http://bugs.python.org/issue1068268, signals must be masked
|
141
|
-
when this method is called.
|
142
|
-
"""
|
143
|
-
while 0 < len(self.tasks) and len(self.running) < self.limit:
|
144
|
-
task = self.tasks.pop(0)
|
145
|
-
self.running.append(task)
|
146
|
-
task.start(self.taskcount, self.iomap, writer, self.askpass_socket)
|
147
|
-
self.taskcount += 1
|
148
|
-
|
149
|
-
def reap_tasks(self):
|
150
|
-
"""Checks to see if any tasks have terminated.
|
151
|
-
|
152
|
-
After cleaning up, returns the number of tasks that finished.
|
153
|
-
"""
|
154
|
-
still_running = []
|
155
|
-
finished_count = 0
|
156
|
-
for task in self.running:
|
157
|
-
if task.running():
|
158
|
-
still_running.append(task)
|
159
|
-
else:
|
160
|
-
self.finished(task)
|
161
|
-
finished_count += 1
|
162
|
-
self.running = still_running
|
163
|
-
return finished_count
|
164
|
-
|
165
|
-
def check_timeout(self):
|
166
|
-
"""Kills timed-out processes and returns the lowest time left."""
|
167
|
-
if self.timeout <= 0:
|
168
|
-
return None
|
169
|
-
|
170
|
-
min_timeleft = None
|
171
|
-
for task in self.running:
|
172
|
-
timeleft = self.timeout - task.elapsed()
|
173
|
-
if timeleft <= 0:
|
174
|
-
task.timedout()
|
175
|
-
continue
|
176
|
-
if min_timeleft is None or timeleft < min_timeleft:
|
177
|
-
min_timeleft = timeleft
|
178
|
-
|
179
|
-
if min_timeleft is None:
|
180
|
-
return 0
|
181
|
-
else:
|
182
|
-
return max(0, min_timeleft)
|
183
|
-
|
184
|
-
def interrupted(self):
|
185
|
-
"""Cleans up after a keyboard interrupt."""
|
186
|
-
for task in self.running:
|
187
|
-
task.interrupted()
|
188
|
-
self.finished(task)
|
189
|
-
|
190
|
-
for task in self.tasks:
|
191
|
-
task.cancel()
|
192
|
-
self.finished(task)
|
193
|
-
|
194
|
-
def finished(self, task):
|
195
|
-
"""Marks a task as complete and reports its status to stdout."""
|
196
|
-
self.done.append(task)
|
197
|
-
n = len(self.done)
|
198
|
-
task.report(n)
|
199
|
-
|
200
|
-
|
201
|
-
class IOMap(object):
|
202
|
-
"""A manager for file descriptors and their associated handlers.
|
203
|
-
|
204
|
-
The poll method dispatches events to the appropriate handlers.
|
205
|
-
"""
|
206
|
-
def __init__(self):
|
207
|
-
self.readmap = {}
|
208
|
-
self.writemap = {}
|
209
|
-
|
210
|
-
# Setup the wakeup file descriptor to avoid hanging on lost signals.
|
211
|
-
wakeup_readfd, wakeup_writefd = os.pipe()
|
212
|
-
self.register_read(wakeup_readfd, self.wakeup_handler)
|
213
|
-
# TODO: remove test when we stop supporting Python <2.5
|
214
|
-
if hasattr(signal, 'set_wakeup_fd'):
|
215
|
-
signal.set_wakeup_fd(wakeup_writefd)
|
216
|
-
self.wakeup_writefd = None
|
217
|
-
else:
|
218
|
-
self.wakeup_writefd = wakeup_writefd
|
219
|
-
|
220
|
-
def register_read(self, fd, handler):
|
221
|
-
"""Registers an IO handler for a file descriptor for reading."""
|
222
|
-
self.readmap[fd] = handler
|
223
|
-
|
224
|
-
def register_write(self, fd, handler):
|
225
|
-
"""Registers an IO handler for a file descriptor for writing."""
|
226
|
-
self.writemap[fd] = handler
|
227
|
-
|
228
|
-
def unregister(self, fd):
|
229
|
-
"""Unregisters the given file descriptor."""
|
230
|
-
if fd in self.readmap:
|
231
|
-
del self.readmap[fd]
|
232
|
-
if fd in self.writemap:
|
233
|
-
del self.writemap[fd]
|
234
|
-
|
235
|
-
def poll(self, timeout=None):
|
236
|
-
"""Performs a poll and dispatches the resulting events."""
|
237
|
-
if not self.readmap and not self.writemap:
|
238
|
-
return
|
239
|
-
rlist = list(self.readmap)
|
240
|
-
wlist = list(self.writemap)
|
241
|
-
try:
|
242
|
-
rlist, wlist, _ = select.select(rlist, wlist, [], timeout)
|
243
|
-
except select.error:
|
244
|
-
_, e, _ = sys.exc_info()
|
245
|
-
errno = e.args[0]
|
246
|
-
if errno == EINTR:
|
247
|
-
return
|
248
|
-
else:
|
249
|
-
raise
|
250
|
-
for fd in rlist:
|
251
|
-
handler = self.readmap[fd]
|
252
|
-
handler(fd, self)
|
253
|
-
for fd in wlist:
|
254
|
-
handler = self.writemap[fd]
|
255
|
-
handler(fd, self)
|
256
|
-
|
257
|
-
def wakeup_handler(self, fd, iomap):
|
258
|
-
"""Handles read events on the signal wakeup pipe.
|
259
|
-
|
260
|
-
This ensures that SIGCHLD signals aren't lost.
|
261
|
-
"""
|
262
|
-
try:
|
263
|
-
os.read(fd, READ_SIZE)
|
264
|
-
except (OSError, IOError):
|
265
|
-
_, e, _ = sys.exc_info()
|
266
|
-
errno, message = e.args
|
267
|
-
if errno != EINTR:
|
268
|
-
sys.stderr.write('Fatal error reading from wakeup pipe: %s\n'
|
269
|
-
% message)
|
270
|
-
raise FatalError
|
271
|
-
|
272
|
-
|
273
|
-
class Writer(threading.Thread):
|
274
|
-
"""Thread that writes to files by processing requests from a Queue.
|
275
|
-
|
276
|
-
Until AIO becomes widely available, it is impossible to make a nonblocking
|
277
|
-
write to an ordinary file. The Writer thread processes all writing to
|
278
|
-
ordinary files so that the main thread can work without blocking.
|
279
|
-
"""
|
280
|
-
OPEN = object()
|
281
|
-
EOF = object()
|
282
|
-
ABORT = object()
|
283
|
-
|
284
|
-
def __init__(self, outdir, errdir):
|
285
|
-
threading.Thread.__init__(self)
|
286
|
-
# A daemon thread automatically dies if the program is terminated.
|
287
|
-
self.setDaemon(True)
|
288
|
-
self.queue = queue.Queue()
|
289
|
-
self.outdir = outdir
|
290
|
-
self.errdir = errdir
|
291
|
-
|
292
|
-
self.host_counts = {}
|
293
|
-
self.files = {}
|
294
|
-
|
295
|
-
def run(self):
|
296
|
-
while True:
|
297
|
-
filename, data = self.queue.get()
|
298
|
-
if filename == self.ABORT:
|
299
|
-
return
|
300
|
-
|
301
|
-
if data == self.OPEN:
|
302
|
-
self.files[filename] = open(filename, 'wb', buffering=1)
|
303
|
-
psshutil.set_cloexec(self.files[filename])
|
304
|
-
else:
|
305
|
-
dest = self.files[filename]
|
306
|
-
if data == self.EOF:
|
307
|
-
dest.close()
|
308
|
-
else:
|
309
|
-
dest.write(data)
|
310
|
-
|
311
|
-
def open_files(self, host):
|
312
|
-
"""Called from another thread to create files for stdout and stderr.
|
313
|
-
|
314
|
-
Returns a pair of filenames (outfile, errfile). These filenames are
|
315
|
-
used as handles for future operations. Either or both may be None if
|
316
|
-
outdir or errdir or not set.
|
317
|
-
"""
|
318
|
-
outfile = errfile = None
|
319
|
-
if self.outdir or self.errdir:
|
320
|
-
count = self.host_counts.get(host, 0)
|
321
|
-
self.host_counts[host] = count + 1
|
322
|
-
if count:
|
323
|
-
filename = "%s.%s" % (host, count)
|
324
|
-
else:
|
325
|
-
filename = host
|
326
|
-
if self.outdir:
|
327
|
-
outfile = os.path.join(self.outdir, filename)
|
328
|
-
self.queue.put((outfile, self.OPEN))
|
329
|
-
if self.errdir:
|
330
|
-
errfile = os.path.join(self.errdir, filename)
|
331
|
-
self.queue.put((errfile, self.OPEN))
|
332
|
-
return outfile, errfile
|
333
|
-
|
334
|
-
def write(self, filename, data):
|
335
|
-
"""Called from another thread to enqueue a write."""
|
336
|
-
self.queue.put((filename, data))
|
337
|
-
|
338
|
-
def close(self, filename):
|
339
|
-
"""Called from another thread to close the given file."""
|
340
|
-
self.queue.put((filename, self.EOF))
|
341
|
-
|
342
|
-
def signal_quit(self):
|
343
|
-
"""Called from another thread to request the Writer to quit."""
|
344
|
-
self.queue.put((self.ABORT, None))
|
345
|
-
|