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)
29 def mkdirp(d, mode=None):
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.
41 if e.errno == errno.EEXIST:
48 """Get the next item from an iterator, None if we reached the end."""
56 """Delete a file at path 'f' if it currently exists.
58 Unlike os.unlink(), does not throw an exception if the file didn't already
64 if e.errno == errno.ENOENT:
65 pass # it doesn't exist, that's what you asked for
69 """Run a subprocess and return its output."""
70 p = subprocess.Popen(argv, stdout=subprocess.PIPE)
77 """Get the absolute path of a file.
79 Behaves like os.path.realpath, but doesn't follow a symlink for the last
80 element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
81 will follow symlinks in p's directory)
87 if st and stat.S_ISLNK(st.st_mode):
88 (dir, name) = os.path.split(p)
89 dir = os.path.realpath(dir)
90 out = os.path.join(dir, name)
92 out = os.path.realpath(p)
93 #log('realpathing:%r,%r\n' % (p, out))
99 """Get the user's login name."""
104 _username = pwd.getpwuid(uid)[0]
106 _username = 'user%d' % uid
112 """Get the user's full name."""
114 if not _userfullname:
117 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
119 _userfullname = 'user%d' % uid
125 """Get the FQDN of this machine."""
128 _hostname = socket.getfqdn()
132 _resource_path = None
133 def resource_path(subdir=''):
134 global _resource_path
135 if not _resource_path:
136 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
137 return os.path.join(_resource_path, subdir)
139 class NotOk(Exception):
143 """A helper class for bup's client-server protocol."""
144 def __init__(self, inp, outp):
148 def read(self, size):
149 """Read 'size' bytes from input stream."""
151 return self.inp.read(size)
154 """Read from input stream until a newline is found."""
156 return self.inp.readline()
158 def write(self, data):
159 """Write 'data' to output stream."""
160 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
161 self.outp.write(data)
164 """Return true if input stream is readable."""
165 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
167 assert(rl[0] == self.inp.fileno())
173 """Indicate end of output from last sent command."""
177 """Indicate server error to the client."""
178 s = re.sub(r'\s+', ' ', str(s))
179 self.write('\nerror %s\n' % s)
181 def _check_ok(self, onempty):
184 for rl in linereader(self.inp):
185 #log('%d got line: %r\n' % (os.getpid(), rl))
186 if not rl: # empty line
190 elif rl.startswith('error '):
191 #log('client: error: %s\n' % rl[6:])
195 raise Exception('server exited unexpectedly; see errors above')
197 def drain_and_check_ok(self):
198 """Remove all data for the current command from input stream."""
201 return self._check_ok(onempty)
204 """Verify that server action completed successfully."""
206 raise Exception('expected "ok", got %r' % rl)
207 return self._check_ok(onempty)
211 """Generate a list of input lines from 'f' without terminating newlines."""
219 def chunkyreader(f, count = None):
220 """Generate a list of chunks of data read from 'f'.
222 If count is None, read until EOF is reached.
224 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
225 reached while reading, raise IOError.
229 b = f.read(min(count, 65536))
231 raise IOError('EOF with %d bytes remaining' % count)
242 """Append "/" to 's' if it doesn't aleady end in "/"."""
243 if s and not s.endswith('/'):
249 def _mmap_do(f, sz, flags, prot):
251 st = os.fstat(f.fileno())
253 map = mmap.mmap(f.fileno(), sz, flags, prot)
254 f.close() # map will persist beyond file close
258 def mmap_read(f, sz = 0):
259 """Create a read-only memory mapped region on file 'f'.
261 If sz is 0, the region will cover the entire file.
263 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
266 def mmap_readwrite(f, sz = 0):
267 """Create a read-write memory mapped region on file 'f'.
269 If sz is 0, the region will cover the entire file.
271 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
275 """Parse data size information into a float number.
277 Here are some examples of conversions:
278 199.2k means 203981 bytes
279 1GB means 1073741824 bytes
280 2.1 tb means 2199023255552 bytes
282 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
284 raise ValueError("can't parse %r as a number" % s)
285 (val, unit) = g.groups()
288 if unit in ['t', 'tb']:
289 mult = 1024*1024*1024*1024
290 elif unit in ['g', 'gb']:
291 mult = 1024*1024*1024
292 elif unit in ['m', 'mb']:
294 elif unit in ['k', 'kb']:
296 elif unit in ['', 'b']:
299 raise ValueError("invalid unit %r in number %r" % (unit, s))
304 """Count the number of elements in an iterator. (consumes the iterator)"""
305 return reduce(lambda x,y: x+1, l)
309 """Convert the string 's' to an integer. Return 0 if s is not a number."""
318 """Append an error message to the list of saved errors.
320 Once processing is able to stop and output the errors, the saved errors are
321 accessible in the module variable helpers.saved_errors.
323 saved_errors.append(e)
326 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
328 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
334 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
336 The new exception handler will make sure that bup will exit without an ugly
337 stacktrace when Ctrl-C is hit.
339 oldhook = sys.excepthook
340 def newhook(exctype, value, traceback):
341 if exctype == KeyboardInterrupt:
342 log('Interrupted.\n')
344 return oldhook(exctype, value, traceback)
345 sys.excepthook = newhook
348 def columnate(l, prefix):
349 """Format elements of 'l' in columns with 'prefix' leading each line.
351 The number of columns is determined automatically based on the string
357 clen = max(len(s) for s in l)
358 ncols = (78 - len(prefix)) / (clen + 2)
363 while len(l) % ncols:
366 for s in range(0, len(l), rows):
367 cols.append(l[s:s+rows])
369 for row in zip(*cols):
370 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
374 # hashlib is only available in python 2.5 or higher, but the 'sha' module
375 # produces a DeprecationWarning in python 2.6 or higher. We want to support
376 # python 2.4 and above without any stupid warnings, so let's try using hashlib
377 # first, and downgrade if it fails.
388 """Format bup's version date string for output."""
389 return _version.DATE.split(' ')[0]
391 def version_commit():
392 """Get the commit hash of bup's current version."""
393 return _version.COMMIT
396 """Format bup's version tag (the official version number).
398 When generated from a commit other than one pointed to with a tag, the
399 returned string will be "unknown-" followed by the first seven positions of
402 names = _version.NAMES.strip()
403 assert(names[0] == '(')
404 assert(names[-1] == ')')
406 l = [n.strip() for n in names.split(',')]
408 if n.startswith('tag: bup-'):
410 return 'unknown-%s' % _version.COMMIT[:7]