]> arthur.barton.de Git - bup.git/blob - cmd/fsck-cmd.py
fsck: report par2 parallel detection status
[bup.git] / cmd / fsck-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
5 """
6 # end of bup preamble
7
8 from __future__ import absolute_import, print_function
9 import sys, os, glob, subprocess
10 from shutil import rmtree
11 from subprocess import PIPE, Popen
12 from tempfile import mkdtemp
13
14 from bup import options, git
15 from bup.helpers import Sha1, chunkyreader, istty2, log, progress
16
17 par2_ok = 0
18 nullf = open('/dev/null')
19
20 def debug(s):
21     if opt.verbose > 1:
22         log(s)
23
24 def run(argv):
25     # at least in python 2.5, using "stdout=2" or "stdout=sys.stderr" below
26     # doesn't actually work, because subprocess closes fd #2 right before
27     # execing for some reason.  So we work around it by duplicating the fd
28     # first.
29     fd = os.dup(2)  # copy stderr
30     try:
31         p = subprocess.Popen(argv, stdout=fd, close_fds=False)
32         return p.wait()
33     finally:
34         os.close(fd)
35
36 def par2_setup():
37     global par2_ok
38     rv = 1
39     try:
40         p = subprocess.Popen(['par2', '--help'],
41                              stdout=nullf, stderr=nullf, stdin=nullf)
42         rv = p.wait()
43     except OSError:
44         log('fsck: warning: par2 not found; disabling recovery features.\n')
45     else:
46         par2_ok = 1
47
48 def is_par2_parallel():
49     # A true result means it definitely allows -t1; a false result is
50     # technically inconclusive, but likely means no.
51     tmpdir = mkdtemp(prefix="bup-fsck")
52     try:
53         canary = tmpdir + '/canary'
54         with open(canary, 'w') as f:
55             print('canary', file=f)
56         p = subprocess.Popen(('par2', 'create', '-qq', '-t1', canary),
57                              stderr=PIPE, stdin=nullf)
58         _, err = p.communicate()
59         parallel = p.returncode == 0
60         if opt.verbose:
61             if err != b'Invalid option specified: -t1\n':
62                 log('Unexpected par2 error output\n')
63                 log(err)
64             if parallel:
65                 log('Assuming par2 supports parallel processing\n')
66             else:
67                 log('Assuming par2 does not support parallel processing\n')
68         return parallel
69     finally:
70         rmtree(tmpdir)
71
72 _par2_parallel = None
73
74 def par2(action, args, verb_floor=0):
75     global _par2_parallel
76     if _par2_parallel is None:
77         _par2_parallel = is_par2_parallel()
78     cmd = ['par2', action]
79     if opt.verbose >= verb_floor and not istty2:
80         cmd.append('-q')
81     else:
82         cmd.append('-qq')
83     if _par2_parallel:
84         cmd.append('-t1')
85     cmd.extend(args)
86     return run(cmd)
87
88 def par2_generate(base):
89     return par2('create',
90                 ['-n1', '-c200', '--', base, base + '.pack', base + '.idx'],
91                 verb_floor=2)
92
93 def par2_verify(base):
94     return par2('verify', ['--', base], verb_floor=3)
95
96 def par2_repair(base):
97     return par2('repair', ['--', base], verb_floor=2)
98
99 def quick_verify(base):
100     f = open(base + '.pack', 'rb')
101     f.seek(-20, 2)
102     wantsum = f.read(20)
103     assert(len(wantsum) == 20)
104     f.seek(0)
105     sum = Sha1()
106     for b in chunkyreader(f, os.fstat(f.fileno()).st_size - 20):
107         sum.update(b)
108     if sum.digest() != wantsum:
109         raise ValueError('expected %r, got %r' % (wantsum.encode('hex'),
110                                                   sum.hexdigest()))
111         
112
113 def git_verify(base):
114     if opt.quick:
115         try:
116             quick_verify(base)
117         except Exception as e:
118             log('error: %s\n' % e)
119             return 1
120         return 0
121     else:
122         return run(['git', 'verify-pack', '--', base])
123     
124     
125 def do_pack(base, last, par2_exists):
126     code = 0
127     if par2_ok and par2_exists and (opt.repair or not opt.generate):
128         vresult = par2_verify(base)
129         if vresult != 0:
130             if opt.repair:
131                 rresult = par2_repair(base)
132                 if rresult != 0:
133                     action_result = 'failed'
134                     log('%s par2 repair: failed (%d)\n' % (last, rresult))
135                     code = rresult
136                 else:
137                     action_result = 'repaired'
138                     log('%s par2 repair: succeeded (0)\n' % last)
139                     code = 100
140             else:
141                 action_result = 'failed'
142                 log('%s par2 verify: failed (%d)\n' % (last, vresult))
143                 code = vresult
144         else:
145             action_result = 'ok'
146     elif not opt.generate or (par2_ok and not par2_exists):
147         gresult = git_verify(base)
148         if gresult != 0:
149             action_result = 'failed'
150             log('%s git verify: failed (%d)\n' % (last, gresult))
151             code = gresult
152         else:
153             if par2_ok and opt.generate:
154                 presult = par2_generate(base)
155                 if presult != 0:
156                     action_result = 'failed'
157                     log('%s par2 create: failed (%d)\n' % (last, presult))
158                     code = presult
159                 else:
160                     action_result = 'generated'
161             else:
162                 action_result = 'ok'
163     else:
164         assert(opt.generate and (not par2_ok or par2_exists))
165         action_result = 'exists' if par2_exists else 'skipped'
166     if opt.verbose:
167         print(last, action_result)
168     return code
169
170
171 optspec = """
172 bup fsck [options...] [filenames...]
173 --
174 r,repair    attempt to repair errors using par2 (dangerous!)
175 g,generate  generate auto-repair information using par2
176 v,verbose   increase verbosity (can be used more than once)
177 quick       just check pack sha1sum, don't use git verify-pack
178 j,jobs=     run 'n' jobs in parallel
179 par2-ok     immediately return 0 if par2 is ok, 1 if not
180 disable-par2  ignore par2 even if it is available
181 """
182 o = options.Options(optspec)
183 (opt, flags, extra) = o.parse(sys.argv[1:])
184
185 par2_setup()
186 if opt.par2_ok:
187     if par2_ok:
188         sys.exit(0)  # 'true' in sh
189     else:
190         sys.exit(1)
191 if opt.disable_par2:
192     par2_ok = 0
193
194 git.check_repo_or_die()
195
196 if not extra:
197     debug('fsck: No filenames given: checking all packs.\n')
198     extra = glob.glob(git.repo('objects/pack/*.pack'))
199
200 code = 0
201 count = 0
202 outstanding = {}
203 for name in extra:
204     if name.endswith('.pack'):
205         base = name[:-5]
206     elif name.endswith('.idx'):
207         base = name[:-4]
208     elif name.endswith('.par2'):
209         base = name[:-5]
210     elif os.path.exists(name + '.pack'):
211         base = name
212     else:
213         raise Exception('%s is not a pack file!' % name)
214     (dir,last) = os.path.split(base)
215     par2_exists = os.path.exists(base + '.par2')
216     if par2_exists and os.stat(base + '.par2').st_size == 0:
217         par2_exists = 0
218     sys.stdout.flush()
219     debug('fsck: checking %s (%s)\n' 
220           % (last, par2_ok and par2_exists and 'par2' or 'git'))
221     if not opt.verbose:
222         progress('fsck (%d/%d)\r' % (count, len(extra)))
223     
224     if not opt.jobs:
225         nc = do_pack(base, last, par2_exists)
226         code = code or nc
227         count += 1
228     else:
229         while len(outstanding) >= opt.jobs:
230             (pid,nc) = os.wait()
231             nc >>= 8
232             if pid in outstanding:
233                 del outstanding[pid]
234                 code = code or nc
235                 count += 1
236         pid = os.fork()
237         if pid:  # parent
238             outstanding[pid] = 1
239         else: # child
240             try:
241                 sys.exit(do_pack(base, last, par2_exists))
242             except Exception as e:
243                 log('exception: %r\n' % e)
244                 sys.exit(99)
245                 
246 while len(outstanding):
247     (pid,nc) = os.wait()
248     nc >>= 8
249     if pid in outstanding:
250         del outstanding[pid]
251         code = code or nc
252         count += 1
253     if not opt.verbose:
254         progress('fsck (%d/%d)\r' % (count, len(extra)))
255
256 if istty2:
257     debug('fsck done.           \n')
258 sys.exit(code)