]> arthur.barton.de Git - bup.git/blob - lib/bup/helpers.py
5906dc8062ed8b0bb1e8103ef920ce0a829d06c1
[bup.git] / lib / bup / helpers.py
1 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re
2 from bup import _version
3
4
5 # Write (blockingly) to sockets that may or may not be in blocking mode.
6 # We need this because our stderr is sometimes eaten by subprocesses
7 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
8 # leading to race conditions.  Ick.  We'll do it the hard way.
9 def _hard_write(fd, buf):
10     while buf:
11         (r,w,x) = select.select([], [fd], [], None)
12         if not w:
13             raise IOError('select(fd) returned without being writable')
14         try:
15             sz = os.write(fd, buf)
16         except OSError, e:
17             if e.errno != errno.EAGAIN:
18                 raise
19         assert(sz >= 0)
20         buf = buf[sz:]
21
22 def log(s):
23     sys.stdout.flush()
24     _hard_write(sys.stderr.fileno(), s)
25
26
27 def mkdirp(d):
28     try:
29         os.makedirs(d)
30     except OSError, e:
31         if e.errno == errno.EEXIST:
32             pass
33         else:
34             raise
35
36
37 def next(it):
38     try:
39         return it.next()
40     except StopIteration:
41         return None
42     
43     
44 def unlink(f):
45     try:
46         os.unlink(f)
47     except OSError, e:
48         if e.errno == errno.ENOENT:
49             pass  # it doesn't exist, that's what you asked for
50
51
52 def readpipe(argv):
53     p = subprocess.Popen(argv, stdout=subprocess.PIPE)
54     r = p.stdout.read()
55     p.wait()
56     return r
57
58
59 # FIXME: this function isn't very generic, because it splits the filename
60 # in an odd way and depends on a terminating '/' to indicate directories.
61 # But it's used in a couple of places, so let's put it here.
62 def pathsplit(p):
63     l = p.split('/')
64     l = [i+'/' for i in l[:-1]] + l[-1:]
65     if l[-1] == '':
66         l.pop()  # extra blank caused by terminating '/'
67     return l
68
69
70 # like os.path.realpath, but doesn't follow a symlink for the last element.
71 # (ie. if 'p' itself is itself a symlink, this one won't follow it)
72 def realpath(p):
73     try:
74         st = os.lstat(p)
75     except OSError:
76         st = None
77     if st and stat.S_ISLNK(st.st_mode):
78         (dir, name) = os.path.split(p)
79         dir = os.path.realpath(dir)
80         out = os.path.join(dir, name)
81     else:
82         out = os.path.realpath(p)
83     #log('realpathing:%r,%r\n' % (p, out))
84     return out
85
86
87 _username = None
88 def username():
89     global _username
90     if not _username:
91         uid = os.getuid()
92         try:
93             _username = pwd.getpwuid(uid)[0]
94         except KeyError:
95             _username = 'user%d' % uid
96     return _username
97
98
99 _userfullname = None
100 def userfullname():
101     global _userfullname
102     if not _userfullname:
103         uid = os.getuid()
104         try:
105             _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
106         except KeyError:
107             _userfullname = 'user%d' % uid
108     return _userfullname
109
110
111 _hostname = None
112 def hostname():
113     global _hostname
114     if not _hostname:
115         _hostname = socket.getfqdn()
116     return _hostname
117
118
119 class NotOk(Exception):
120     pass
121
122 class Conn:
123     def __init__(self, inp, outp):
124         self.inp = inp
125         self.outp = outp
126
127     def read(self, size):
128         self.outp.flush()
129         return self.inp.read(size)
130
131     def readline(self):
132         self.outp.flush()
133         return self.inp.readline()
134
135     def write(self, data):
136         #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
137         self.outp.write(data)
138
139     def has_input(self):
140         [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
141         if rl:
142             assert(rl[0] == self.inp.fileno())
143             return True
144         else:
145             return None
146
147     def ok(self):
148         self.write('\nok\n')
149
150     def error(self, s):
151         s = re.sub(r'\s+', ' ', str(s))
152         self.write('\nerror %s\n' % s)
153
154     def _check_ok(self, onempty):
155         self.outp.flush()
156         rl = ''
157         for rl in linereader(self.inp):
158             #log('%d got line: %r\n' % (os.getpid(), rl))
159             if not rl:  # empty line
160                 continue
161             elif rl == 'ok':
162                 return None
163             elif rl.startswith('error '):
164                 #log('client: error: %s\n' % rl[6:])
165                 return NotOk(rl[6:])
166             else:
167                 onempty(rl)
168         raise Exception('server exited unexpectedly; see errors above')
169
170     def drain_and_check_ok(self):
171         def onempty(rl):
172             pass
173         return self._check_ok(onempty)
174
175     def check_ok(self):
176         def onempty(rl):
177             raise Exception('expected "ok", got %r' % rl)
178         return self._check_ok(onempty)
179
180
181 def linereader(f):
182     while 1:
183         line = f.readline()
184         if not line:
185             break
186         yield line[:-1]
187
188
189 def chunkyreader(f, count = None):
190     if count != None:
191         while count > 0:
192             b = f.read(min(count, 65536))
193             if not b:
194                 raise IOError('EOF with %d bytes remaining' % count)
195             yield b
196             count -= len(b)
197     else:
198         while 1:
199             b = f.read(65536)
200             if not b: break
201             yield b
202
203
204 def slashappend(s):
205     if s and not s.endswith('/'):
206         return s + '/'
207     else:
208         return s
209
210
211 def _mmap_do(f, sz, flags, prot):
212     if not sz:
213         st = os.fstat(f.fileno())
214         sz = st.st_size
215     map = mmap.mmap(f.fileno(), sz, flags, prot)
216     f.close()  # map will persist beyond file close
217     return map
218
219
220 def mmap_read(f, sz = 0):
221     return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ)
222
223
224 def mmap_readwrite(f, sz = 0):
225     return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)
226
227
228 def parse_num(s):
229     g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
230     if not g:
231         raise ValueError("can't parse %r as a number" % s)
232     (val, unit) = g.groups()
233     num = float(val)
234     unit = unit.lower()
235     if unit in ['t', 'tb']:
236         mult = 1024*1024*1024*1024
237     elif unit in ['g', 'gb']:
238         mult = 1024*1024*1024
239     elif unit in ['m', 'mb']:
240         mult = 1024*1024
241     elif unit in ['k', 'kb']:
242         mult = 1024
243     elif unit in ['', 'b']:
244         mult = 1
245     else:
246         raise ValueError("invalid unit %r in number %r" % (unit, s))
247     return int(num*mult)
248
249     
250 # count the number of elements in an iterator (consumes the iterator)
251 def count(l):
252     return reduce(lambda x,y: x+1, l)
253
254
255 def atoi(s):
256     try:
257         return int(s or '0')
258     except ValueError:
259         return 0
260
261
262 saved_errors = []
263 def add_error(e):
264     saved_errors.append(e)
265     log('%-70s\n' % e)
266
267 istty = os.isatty(2) or atoi(os.environ.get('BUP_FORCE_TTY'))
268 def progress(s):
269     if istty:
270         log(s)
271
272
273 def handle_ctrl_c():
274     oldhook = sys.excepthook
275     def newhook(exctype, value, traceback):
276         if exctype == KeyboardInterrupt:
277             log('Interrupted.\n')
278         else:
279             return oldhook(exctype, value, traceback)
280     sys.excepthook = newhook
281
282
283 def columnate(l, prefix):
284     l = l[:]
285     clen = max(len(s) for s in l)
286     ncols = (78 - len(prefix)) / (clen + 2)
287     if ncols <= 1:
288         ncols = 1
289         clen = 0
290     cols = []
291     while len(l) % ncols:
292         l.append('')
293     rows = len(l)/ncols
294     for s in range(0, len(l), rows):
295         cols.append(l[s:s+rows])
296     out = ''
297     for row in zip(*cols):
298         out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
299     return out
300
301
302 # hashlib is only available in python 2.5 or higher, but the 'sha' module
303 # produces a DeprecationWarning in python 2.6 or higher.  We want to support
304 # python 2.4 and above without any stupid warnings, so let's try using hashlib
305 # first, and downgrade if it fails.
306 try:
307     import hashlib
308 except ImportError:
309     import sha
310     Sha1 = sha.sha
311 else:
312     Sha1 = hashlib.sha1
313
314
315 def version_date():
316     return _version.DATE.split(' ')[0]
317
318 def version_commit():
319     return _version.COMMIT
320
321 def version_tag():
322     names = _version.NAMES.strip()
323     assert(names[0] == '(')
324     assert(names[-1] == ')')
325     names = names[1:-1]
326     l = [n.strip() for n in names.split(',')]
327     for n in l:
328         if n.startswith('tag: bup-'):
329             return n[9:]
330     return 'unknown-%s' % _version.COMMIT[:7]