1 """Helper functions and classes for bup."""
2 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re
3 from bup import _version
7 """Convert the string 's' to an integer. Return 0 if s is not a number."""
14 buglvl = atoi(os.environ.get('BUP_DEBUG', 0))
17 # Write (blockingly) to sockets that may or may not be in blocking mode.
18 # We need this because our stderr is sometimes eaten by subprocesses
19 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
20 # leading to race conditions. Ick. We'll do it the hard way.
21 def _hard_write(fd, buf):
23 (r,w,x) = select.select([], [fd], [], None)
25 raise IOError('select(fd) returned without being writable')
27 sz = os.write(fd, buf)
29 if e.errno != errno.EAGAIN:
35 """Print a log message to stderr."""
37 _hard_write(sys.stderr.fileno(), s)
50 def mkdirp(d, mode=None):
51 """Recursively create directories on path 'd'.
53 Unlike os.makedirs(), it doesn't raise an exception if the last element of
54 the path already exists.
62 if e.errno == errno.EEXIST:
69 """Get the next item from an iterator, None if we reached the end."""
77 """Delete a file at path 'f' if it currently exists.
79 Unlike os.unlink(), does not throw an exception if the file didn't already
85 if e.errno == errno.ENOENT:
86 pass # it doesn't exist, that's what you asked for
90 """Run a subprocess and return its output."""
91 p = subprocess.Popen(argv, stdout=subprocess.PIPE)
98 """Get the absolute path of a file.
100 Behaves like os.path.realpath, but doesn't follow a symlink for the last
101 element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
102 will follow symlinks in p's directory)
108 if st and stat.S_ISLNK(st.st_mode):
109 (dir, name) = os.path.split(p)
110 dir = os.path.realpath(dir)
111 out = os.path.join(dir, name)
113 out = os.path.realpath(p)
114 #log('realpathing:%r,%r\n' % (p, out))
120 """Get the user's login name."""
125 _username = pwd.getpwuid(uid)[0]
127 _username = 'user%d' % uid
133 """Get the user's full name."""
135 if not _userfullname:
138 _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
140 _userfullname = 'user%d' % uid
146 """Get the FQDN of this machine."""
149 _hostname = socket.getfqdn()
153 _resource_path = None
154 def resource_path(subdir=''):
155 global _resource_path
156 if not _resource_path:
157 _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
158 return os.path.join(_resource_path, subdir)
160 class NotOk(Exception):
164 """A helper class for bup's client-server protocol."""
165 def __init__(self, inp, outp):
169 def read(self, size):
170 """Read 'size' bytes from input stream."""
172 return self.inp.read(size)
175 """Read from input stream until a newline is found."""
177 return self.inp.readline()
179 def write(self, data):
180 """Write 'data' to output stream."""
181 #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
182 self.outp.write(data)
185 """Return true if input stream is readable."""
186 [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
188 assert(rl[0] == self.inp.fileno())
194 """Indicate end of output from last sent command."""
198 """Indicate server error to the client."""
199 s = re.sub(r'\s+', ' ', str(s))
200 self.write('\nerror %s\n' % s)
202 def _check_ok(self, onempty):
205 for rl in linereader(self.inp):
206 #log('%d got line: %r\n' % (os.getpid(), rl))
207 if not rl: # empty line
211 elif rl.startswith('error '):
212 #log('client: error: %s\n' % rl[6:])
216 raise Exception('server exited unexpectedly; see errors above')
218 def drain_and_check_ok(self):
219 """Remove all data for the current command from input stream."""
222 return self._check_ok(onempty)
225 """Verify that server action completed successfully."""
227 raise Exception('expected "ok", got %r' % rl)
228 return self._check_ok(onempty)
232 """Generate a list of input lines from 'f' without terminating newlines."""
240 def chunkyreader(f, count = None):
241 """Generate a list of chunks of data read from 'f'.
243 If count is None, read until EOF is reached.
245 If count is a positive integer, read 'count' bytes from 'f'. If EOF is
246 reached while reading, raise IOError.
250 b = f.read(min(count, 65536))
252 raise IOError('EOF with %d bytes remaining' % count)
263 """Append "/" to 's' if it doesn't aleady end in "/"."""
264 if s and not s.endswith('/'):
270 def _mmap_do(f, sz, flags, prot):
272 st = os.fstat(f.fileno())
274 map = mmap.mmap(f.fileno(), sz, flags, prot)
275 f.close() # map will persist beyond file close
279 def mmap_read(f, sz = 0):
280 """Create a read-only memory mapped region on file 'f'.
282 If sz is 0, the region will cover the entire file.
284 return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
287 def mmap_readwrite(f, sz = 0):
288 """Create a read-write memory mapped region on file 'f'.
290 If sz is 0, the region will cover the entire file.
292 return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
296 """Parse data size information into a float number.
298 Here are some examples of conversions:
299 199.2k means 203981 bytes
300 1GB means 1073741824 bytes
301 2.1 tb means 2199023255552 bytes
303 g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
305 raise ValueError("can't parse %r as a number" % s)
306 (val, unit) = g.groups()
309 if unit in ['t', 'tb']:
310 mult = 1024*1024*1024*1024
311 elif unit in ['g', 'gb']:
312 mult = 1024*1024*1024
313 elif unit in ['m', 'mb']:
315 elif unit in ['k', 'kb']:
317 elif unit in ['', 'b']:
320 raise ValueError("invalid unit %r in number %r" % (unit, s))
325 """Count the number of elements in an iterator. (consumes the iterator)"""
326 return reduce(lambda x,y: x+1, l)
331 """Append an error message to the list of saved errors.
333 Once processing is able to stop and output the errors, the saved errors are
334 accessible in the module variable helpers.saved_errors.
336 saved_errors.append(e)
339 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
341 """Calls log(s) if stderr is a TTY. Does nothing otherwise."""
347 """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
349 The new exception handler will make sure that bup will exit without an ugly
350 stacktrace when Ctrl-C is hit.
352 oldhook = sys.excepthook
353 def newhook(exctype, value, traceback):
354 if exctype == KeyboardInterrupt:
355 log('Interrupted.\n')
357 return oldhook(exctype, value, traceback)
358 sys.excepthook = newhook
361 def columnate(l, prefix):
362 """Format elements of 'l' in columns with 'prefix' leading each line.
364 The number of columns is determined automatically based on the string
370 clen = max(len(s) for s in l)
371 ncols = (78 - len(prefix)) / (clen + 2)
376 while len(l) % ncols:
379 for s in range(0, len(l), rows):
380 cols.append(l[s:s+rows])
382 for row in zip(*cols):
383 out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
387 # hashlib is only available in python 2.5 or higher, but the 'sha' module
388 # produces a DeprecationWarning in python 2.6 or higher. We want to support
389 # python 2.4 and above without any stupid warnings, so let's try using hashlib
390 # first, and downgrade if it fails.
401 """Format bup's version date string for output."""
402 return _version.DATE.split(' ')[0]
404 def version_commit():
405 """Get the commit hash of bup's current version."""
406 return _version.COMMIT
409 """Format bup's version tag (the official version number).
411 When generated from a commit other than one pointed to with a tag, the
412 returned string will be "unknown-" followed by the first seven positions of
415 names = _version.NAMES.strip()
416 assert(names[0] == '(')
417 assert(names[-1] == ')')
419 l = [n.strip() for n in names.split(',')]
421 if n.startswith('tag: bup-'):
423 return 'unknown-%s' % _version.COMMIT[:7]