1 """Helper functions and classes for bup."""
2 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re
3 from bup import _version
6 # Write (blockingly) to sockets that may or may not be in blocking mode.
7 # We need this because our stderr is sometimes eaten by subprocesses
8 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
9 # leading to race conditions. Ick. We'll do it the hard way.
10 def _hard_write(fd, buf):
12 (r,w,x) = select.select([], [fd], [], None)
14 raise IOError('select(fd) returned without being writable')
16 sz = os.write(fd, buf)
18 if e.errno != errno.EAGAIN:
24 """Print a log message to stderr."""
26 _hard_write(sys.stderr.fileno(), s)
30 """Recursively create directories on path 'd'.
32 Unlike os.makedirs(), it doesn't raise an exception if the last element of
33 the path already exists.
38 if e.errno == errno.EEXIST:
45 """Get the next item from an iterator, None if we reached the end."""
53 """Delete a file at path 'f' if it currently exists.
55 Unlike os.unlink(), does not throw an exception if the file didn't already
61 if e.errno == errno.ENOENT:
62 pass # it doesn't exist, that's what you asked for
66 """Run a subprocess and return its output."""
67 p = subprocess.Popen(argv, stdout=subprocess.PIPE)
74 """Get the absolute path of a file.
76 Behaves like os.path.realpath, but doesn't follow a symlink for the last
77 element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
78 will follow symlinks in p's directory)
84 if st and stat.S_ISLNK(st.st_mode):
85 (dir, name) = os.path.split(p)
86 dir = os.path.realpath(dir)
87 out = os.path.join(dir, name)
89 out = os.path.realpath(p)
90 #log('realpathing:%r,%r\n' % (p, out))
96 """Get the user's login name."""
101 _username = pwd.getpwuid(uid)[0]
103 _username = 'user%d' % uid
109 """Get the user's full name."""
111 if not _userfullname:
114 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
116 _userfullname = 'user%d' % uid
122 """Get the FQDN of this machine."""
125 _hostname = socket.getfqdn()
129 _resource_path = None
130 def resource_path(subdir=''):
131 global _resource_path
132 if not _resource_path:
133 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
134 return os.path.join(_resource_path, subdir)
136 class NotOk(Exception):
140 """A helper class for bup's client-server protocol."""
141 def __init__(self, inp, outp):
145 def read(self, size):
146 """Read 'size' bytes from input stream."""
148 return self.inp.read(size)
151 """Read from input stream until a newline is found."""
153 return self.inp.readline()
155 def write(self, data):
156 """Write 'data' to output stream."""
157 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
158 self.outp.write(data)
161 """Return true if input stream is readable."""
162 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
164 assert(rl[0] == self.inp.fileno())
170 """Indicate end of output from last sent command."""
174 """Indicate server error to the client."""
175 s = re.sub(r'\s+', ' ', str(s))
176 self.write('\nerror %s\n' % s)
178 def _check_ok(self, onempty):
181 for rl in linereader(self.inp):
182 #log('%d got line: %r\n' % (os.getpid(), rl))
183 if not rl: # empty line
187 elif rl.startswith('error '):
188 #log('client: error: %s\n' % rl[6:])
192 raise Exception('server exited unexpectedly; see errors above')
194 def drain_and_check_ok(self):
195 """Remove all data for the current command from input stream."""
198 return self._check_ok(onempty)
201 """Verify that server action completed successfully."""
203 raise Exception('expected "ok", got %r' % rl)
204 return self._check_ok(onempty)
208 """Generate a list of input lines from 'f' without terminating newlines."""
216 def chunkyreader(f, count = None):
217 """Generate a list of chunks of data read from 'f'.
219 If count is None, read until EOF is reached.
221 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
222 reached while reading, raise IOError.
226 b = f.read(min(count, 65536))
228 raise IOError('EOF with %d bytes remaining' % count)
239 """Append "/" to 's' if it doesn't aleady end in "/"."""
240 if s and not s.endswith('/'):
246 def _mmap_do(f, sz, flags, prot):
248 st = os.fstat(f.fileno())
250 map = mmap.mmap(f.fileno(), sz, flags, prot)
251 f.close() # map will persist beyond file close
255 def mmap_read(f, sz = 0):
256 """Create a read-only memory mapped region on file 'f'.
258 If sz is 0, the region will cover the entire file.
260 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
263 def mmap_readwrite(f, sz = 0):
264 """Create a read-write memory mapped region on file 'f'.
266 If sz is 0, the region will cover the entire file.
268 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
272 """Parse data size information into a float number.
274 Here are some examples of conversions:
275 199.2k means 203981 bytes
276 1GB means 1073741824 bytes
277 2.1 tb means 2199023255552 bytes
279 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
281 raise ValueError("can't parse %r as a number" % s)
282 (val, unit) = g.groups()
285 if unit in ['t', 'tb']:
286 mult = 1024*1024*1024*1024
287 elif unit in ['g', 'gb']:
288 mult = 1024*1024*1024
289 elif unit in ['m', 'mb']:
291 elif unit in ['k', 'kb']:
293 elif unit in ['', 'b']:
296 raise ValueError("invalid unit %r in number %r" % (unit, s))
301 """Count the number of elements in an iterator. (consumes the iterator)"""
302 return reduce(lambda x,y: x+1, l)
306 """Convert the string 's' to an integer. Return 0 if s is not a number."""
315 """Append an error message to the list of saved errors.
317 Once processing is able to stop and output the errors, the saved errors are
318 accessible in the module variable helpers.saved_errors.
320 saved_errors.append(e)
323 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
325 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
331 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
333 The new exception handler will make sure that bup will exit without an ugly
334 stacktrace when Ctrl-C is hit.
336 oldhook = sys.excepthook
337 def newhook(exctype, value, traceback):
338 if exctype == KeyboardInterrupt:
339 log('Interrupted.\n')
341 return oldhook(exctype, value, traceback)
342 sys.excepthook = newhook
345 def columnate(l, prefix):
346 """Format elements of 'l' in columns with 'prefix' leading each line.
348 The number of columns is determined automatically based on the string
352 clen = max(len(s) for s in l)
353 ncols = (78 - len(prefix)) / (clen + 2)
358 while len(l) % ncols:
361 for s in range(0, len(l), rows):
362 cols.append(l[s:s+rows])
364 for row in zip(*cols):
365 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
369 # hashlib is only available in python 2.5 or higher, but the 'sha' module
370 # produces a DeprecationWarning in python 2.6 or higher. We want to support
371 # python 2.4 and above without any stupid warnings, so let's try using hashlib
372 # first, and downgrade if it fails.
383 """Format bup's version date string for output."""
384 return _version.DATE.split(' ')[0]
386 def version_commit():
387 """Get the commit hash of bup's current version."""
388 return _version.COMMIT
391 """Format bup's version tag (the official version number).
393 When generated from a commit other than one pointed to with a tag, the
394 returned string will be "unknown-" followed by the first seven positions of
397 names = _version.NAMES.strip()
398 assert(names[0] == '(')
399 assert(names[-1] == ')')
401 l = [n.strip() for n in names.split(',')]
403 if n.startswith('tag: bup-'):
405 return 'unknown-%s' % _version.COMMIT[:7]