1 """Helper functions and classes for bup."""
2 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re
3 from bup import _version
5 # This function should really be in helpers, not in bup.options. But we
6 # want options.py to be standalone so people can include it in other projects.
7 from bup.options import _tty_width
12 """Convert the string 's' to an integer. Return 0 if s is not a number."""
19 buglvl = atoi(os.environ.get('BUP_DEBUG', 0))
22 # Write (blockingly) to sockets that may or may not be in blocking mode.
23 # We need this because our stderr is sometimes eaten by subprocesses
24 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
25 # leading to race conditions. Ick. We'll do it the hard way.
26 def _hard_write(fd, buf):
28 (r,w,x) = select.select([], [fd], [], None)
30 raise IOError('select(fd) returned without being writable')
32 sz = os.write(fd, buf)
34 if e.errno != errno.EAGAIN:
40 """Print a log message to stderr."""
42 _hard_write(sys.stderr.fileno(), s)
55 def mkdirp(d, mode=None):
56 """Recursively create directories on path 'd'.
58 Unlike os.makedirs(), it doesn't raise an exception if the last element of
59 the path already exists.
67 if e.errno == errno.EEXIST:
74 """Get the next item from an iterator, None if we reached the end."""
82 """Delete a file at path 'f' if it currently exists.
84 Unlike os.unlink(), does not throw an exception if the file didn't already
90 if e.errno == errno.ENOENT:
91 pass # it doesn't exist, that's what you asked for
95 """Run a subprocess and return its output."""
96 p = subprocess.Popen(argv, stdout=subprocess.PIPE)
103 """Get the absolute path of a file.
105 Behaves like os.path.realpath, but doesn't follow a symlink for the last
106 element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
107 will follow symlinks in p's directory)
113 if st and stat.S_ISLNK(st.st_mode):
114 (dir, name) = os.path.split(p)
115 dir = os.path.realpath(dir)
116 out = os.path.join(dir, name)
118 out = os.path.realpath(p)
119 #log('realpathing:%r,%r\n' % (p, out))
125 """Get the user's login name."""
130 _username = pwd.getpwuid(uid)[0]
132 _username = 'user%d' % uid
138 """Get the user's full name."""
140 if not _userfullname:
143 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
145 _userfullname = 'user%d' % uid
151 """Get the FQDN of this machine."""
154 _hostname = socket.getfqdn()
158 _resource_path = None
159 def resource_path(subdir=''):
160 global _resource_path
161 if not _resource_path:
162 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
163 return os.path.join(_resource_path, subdir)
165 class NotOk(Exception):
169 """A helper class for bup's client-server protocol."""
170 def __init__(self, inp, outp):
174 def read(self, size):
175 """Read 'size' bytes from input stream."""
177 return self.inp.read(size)
180 """Read from input stream until a newline is found."""
182 return self.inp.readline()
184 def write(self, data):
185 """Write 'data' to output stream."""
186 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
187 self.outp.write(data)
190 """Return true if input stream is readable."""
191 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
193 assert(rl[0] == self.inp.fileno())
199 """Indicate end of output from last sent command."""
203 """Indicate server error to the client."""
204 s = re.sub(r'\s+', ' ', str(s))
205 self.write('\nerror %s\n' % s)
207 def _check_ok(self, onempty):
210 for rl in linereader(self.inp):
211 #log('%d got line: %r\n' % (os.getpid(), rl))
212 if not rl: # empty line
216 elif rl.startswith('error '):
217 #log('client: error: %s\n' % rl[6:])
221 raise Exception('server exited unexpectedly; see errors above')
223 def drain_and_check_ok(self):
224 """Remove all data for the current command from input stream."""
227 return self._check_ok(onempty)
230 """Verify that server action completed successfully."""
232 raise Exception('expected "ok", got %r' % rl)
233 return self._check_ok(onempty)
237 """Generate a list of input lines from 'f' without terminating newlines."""
245 def chunkyreader(f, count = None):
246 """Generate a list of chunks of data read from 'f'.
248 If count is None, read until EOF is reached.
250 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
251 reached while reading, raise IOError.
255 b = f.read(min(count, 65536))
257 raise IOError('EOF with %d bytes remaining' % count)
268 """Append "/" to 's' if it doesn't aleady end in "/"."""
269 if s and not s.endswith('/'):
275 def _mmap_do(f, sz, flags, prot):
277 st = os.fstat(f.fileno())
279 map = mmap.mmap(f.fileno(), sz, flags, prot)
280 f.close() # map will persist beyond file close
284 def mmap_read(f, sz = 0):
285 """Create a read-only memory mapped region on file 'f'.
287 If sz is 0, the region will cover the entire file.
289 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
292 def mmap_readwrite(f, sz = 0):
293 """Create a read-write memory mapped region on file 'f'.
295 If sz is 0, the region will cover the entire file.
297 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
301 """Parse data size information into a float number.
303 Here are some examples of conversions:
304 199.2k means 203981 bytes
305 1GB means 1073741824 bytes
306 2.1 tb means 2199023255552 bytes
308 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
310 raise ValueError("can't parse %r as a number" % s)
311 (val, unit) = g.groups()
314 if unit in ['t', 'tb']:
315 mult = 1024*1024*1024*1024
316 elif unit in ['g', 'gb']:
317 mult = 1024*1024*1024
318 elif unit in ['m', 'mb']:
320 elif unit in ['k', 'kb']:
322 elif unit in ['', 'b']:
325 raise ValueError("invalid unit %r in number %r" % (unit, s))
330 """Count the number of elements in an iterator. (consumes the iterator)"""
331 return reduce(lambda x,y: x+1, l)
336 """Append an error message to the list of saved errors.
338 Once processing is able to stop and output the errors, the saved errors are
339 accessible in the module variable helpers.saved_errors.
341 saved_errors.append(e)
344 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
346 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
352 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
354 The new exception handler will make sure that bup will exit without an ugly
355 stacktrace when Ctrl-C is hit.
357 oldhook = sys.excepthook
358 def newhook(exctype, value, traceback):
359 if exctype == KeyboardInterrupt:
360 log('Interrupted.\n')
362 return oldhook(exctype, value, traceback)
363 sys.excepthook = newhook
366 def columnate(l, prefix):
367 """Format elements of 'l' in columns with 'prefix' leading each line.
369 The number of columns is determined automatically based on the string
375 clen = max(len(s) for s in l)
376 ncols = (tty_width() - len(prefix)) / (clen + 2)
381 while len(l) % ncols:
384 for s in range(0, len(l), rows):
385 cols.append(l[s:s+rows])
387 for row in zip(*cols):
388 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
392 # hashlib is only available in python 2.5 or higher, but the 'sha' module
393 # produces a DeprecationWarning in python 2.6 or higher. We want to support
394 # python 2.4 and above without any stupid warnings, so let's try using hashlib
395 # first, and downgrade if it fails.
406 """Format bup's version date string for output."""
407 return _version.DATE.split(' ')[0]
409 def version_commit():
410 """Get the commit hash of bup's current version."""
411 return _version.COMMIT
414 """Format bup's version tag (the official version number).
416 When generated from a commit other than one pointed to with a tag, the
417 returned string will be "unknown-" followed by the first seven positions of
420 names = _version.NAMES.strip()
421 assert(names[0] == '(')
422 assert(names[-1] == ')')
424 l = [n.strip() for n in names.split(',')]
426 if n.startswith('tag: bup-'):
428 return 'unknown-%s' % _version.COMMIT[:7]