Commit 9bf5e2e1 authored by Jeremy Hylton's avatar Jeremy Hylton

Fix some serious bugs in fsrecover.

When a corrupted record is found, it guesses a location for the next
good transaction header.  If the guessed location happened to have a
'c' character at just the right spot, it would think it was at the end
of the file.

Fix in off-by-one bug in scan() that lead to failure.

Do more sanity checks for invalid transaction headers following a
scan.

Use restore() instead of store() to copy data.  This change should
maximize the chance that the recovered storage is identical except for
the damaged records.

Add tests from the trunk and remove test in testFileStorage.
parent 81c70027
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
# FOR A PARTICULAR PURPOSE # FOR A PARTICULAR PURPOSE
# #
############################################################################## ##############################################################################
"""Simple script for repairing damaged FileStorage files. """Simple script for repairing damaged FileStorage files.
Usage: %s [-f] input output Usage: %s [-f] input output
...@@ -94,64 +92,92 @@ class ErrorFound(Exception): pass ...@@ -94,64 +92,92 @@ class ErrorFound(Exception): pass
def error(mess, *args): def error(mess, *args):
raise ErrorFound(mess % args) raise ErrorFound(mess % args)
def read_transaction_header(file, pos, file_size): def read_txn_header(f, pos, file_size, outp, ltid):
# Read the transaction record # Read the transaction record
file.seek(pos) f.seek(pos)
h = file.read(23) h = f.read(23)
if len(h) < 23: if len(h) < 23:
raise EOFError raise EOFError
tid, stl, status, ul, dl, el = unpack(">8s8scHHH",h) tid, stl, status, ul, dl, el = unpack(">8s8scHHH",h)
if el < 0: el=t32-el if el < 0: el=t32-el
tl=u64(stl) tl = u64(stl)
if status=='c': raise EOFError
if pos+(tl+8) > file_size: if pos + (tl + 8) > file_size:
error("bad transaction length at %s", pos) error("bad transaction length at %s", pos)
if status not in ' up': if tl < (23 + ul + dl + el):
error('invalid status, %s, at %s', status, pos) error("invalid transaction length, %s, at %s", tl, pos)
if ltid and tid < ltid:
error("time-stamp reducation %s < %s, at %s", u64(tid), u64(ltid), pos)
if status == "c":
truncate(f, pos, file_size, output)
raise EOFError
if tl < (23+ul+dl+el): if status not in " up":
error('invalid transaction length, %s, at %s', tl, pos) error("invalid status, %r, at %s", status, pos)
tpos=pos tpos = pos
tend=tpos+tl tend = tpos + tl
if status=='u': if status == "u":
# Undone transaction, skip it # Undone transaction, skip it
file.seek(tend) f.seek(tend)
h = file.read(8) h = f.read(8)
if h != stl: if h != stl:
error('inconsistent transaction length at %s', pos) error("inconsistent transaction length at %s", pos)
pos = tend + 8 pos = tend + 8
return pos, None return pos, None, tid
pos = tpos+(23+ul+dl+el) pos = tpos+(23+ul+dl+el)
user = file.read(ul) user = f.read(ul)
description = file.read(dl) description = f.read(dl)
if el: if el:
try: e=loads(file.read(el)) try: e=loads(f.read(el))
except: e={} except: e={}
else: e={} else: e={}
result = RecordIterator(tid, status, user, description, e, pos, tend, result = RecordIterator(tid, status, user, description, e, pos, tend,
file, tpos) f, tpos)
pos=tend pos = tend
# Read the (intentionally redundant) transaction length # Read the (intentionally redundant) transaction length
file.seek(pos) f.seek(pos)
h = file.read(8) h = f.read(8)
if h != stl: if h != stl:
error("redundant transaction length check failed at %s", pos) error("redundant transaction length check failed at %s", pos)
pos += 8 pos += 8
return pos, result return pos, result, tid
def scan(file, pos): def truncate(f, pos, file_size, outp):
"""Return a potential transaction location following pos in file. """Copy data from pos to end of f to a .trNNN file."""
i = 0
while 1:
trname = outp + ".tr%d" % i
if os.path.exists(trname):
i += 1
tr = open(trname, "wb")
copy(f, tr, file_size - pos)
f.seek(pos)
tr.close()
def copy(src, dst, n):
while n:
buf = src.read(8096)
if not buf:
break
if len(buf) > n:
buf = buf[:n]
dst.write(buf)
n -= len(buf)
def scan(f, pos):
"""Return a potential transaction location following pos in f.
This routine scans forward from pos looking for the last data This routine scans forward from pos looking for the last data
record in a transaction. A period '.' always occurs at the end of record in a transaction. A period '.' always occurs at the end of
...@@ -163,30 +189,30 @@ def scan(file, pos): ...@@ -163,30 +189,30 @@ def scan(file, pos):
actually a transaction header. actually a transaction header.
""" """
while 1: while 1:
file.seek(pos) f.seek(pos)
data = file.read(8096) data = f.read(8096)
if not data: if not data:
return 0 return 0
s = 0 s = 0
while 1: while 1:
l = data.find('.', s) l = data.find(".", s)
if l < 0: if l < 0:
pos += len(data) pos += len(data)
break break
# If we are less than 8 bytes from the end of the # If we are less than 8 bytes from the end of the
# string, we need to read more data. # string, we need to read more data.
if l > len(data) - 8: s = l + 1
if s > len(data) - 8:
pos += l pos += l
break break
s = l + 1
tl = u64(data[s:s+8]) tl = u64(data[s:s+8])
if tl < pos: if tl < pos:
return pos + s + 8 return pos + s + 8
def iprogress(i): def iprogress(i):
if i % 2: if i % 2:
print '.', print ".",
else: else:
print (i/2) % 10, print (i/2) % 10,
sys.stdout.flush() sys.stdout.flush()
...@@ -197,7 +223,7 @@ def progress(p): ...@@ -197,7 +223,7 @@ def progress(p):
def main(): def main():
try: try:
opts, (inp, outp) = getopt.getopt(sys.argv[1:], 'fv:pP:') opts, (inp, outp) = getopt.getopt(sys.argv[1:], "fv:pP:")
except getopt.error: except getopt.error:
die() die()
print __doc__ % argv[0] print __doc__ % argv[0]
...@@ -205,58 +231,63 @@ def main(): ...@@ -205,58 +231,63 @@ def main():
force = partial = verbose = 0 force = partial = verbose = 0
pack = None pack = None
for opt, v in opts: for opt, v in opts:
if opt == '-v': if opt == "-v":
verbose = int(v) verbose = int(v)
elif opt == '-p': elif opt == "-p":
partial = 1 partial = 1
elif opt == '-f': elif opt == "-f":
force = 1 force = 1
elif opt == '-P': elif opt == "-P":
pack = time.time() - float(v) pack = time.time() - float(v)
recover(inp, outp, verbose, partial, force, pack) recover(inp, outp, verbose, partial, force, pack)
def recover(inp, outp, verbose=0, partial=0, force=0, pack=0): def recover(inp, outp, verbose=0, partial=0, force=0, pack=0):
print 'Recovering', inp, 'into', outp print "Recovering", inp, "into", outp
if os.path.exists(outp) and not force: if os.path.exists(outp) and not force:
die("%s exists" % outp) die("%s exists" % outp)
file = open(inp, "rb") f = open(inp, "rb")
if file.read(4) != ZODB.FileStorage.packed_version: if f.read(4) != ZODB.FileStorage.packed_version:
die("input is not a file storage") die("input is not a file storage")
file.seek(0,2) f.seek(0,2)
file_size = file.tell() file_size = f.tell()
ofs = ZODB.FileStorage.FileStorage(outp, create=1) ofs = ZODB.FileStorage.FileStorage(outp, create=1)
_ts = None _ts = None
ok = 1 ok = 1
prog1 = 0 prog1 = 0
preindex = {}
undone = 0 undone = 0
pos = 4 pos = 4
ltid = None
while pos: while pos:
try: try:
npos, transaction = read_transaction_header(file, pos, file_size) npos, txn, tid = read_txn_header(f, pos, file_size, outp, ltid)
except EOFError: except EOFError:
break break
except: except (KeyboardInterrupt, SystemExit):
print "\n%s: %s\n" % sys.exc_info()[:2] raise
except Exception, err:
print "error reading txn header:", err
if not verbose: if not verbose:
progress(prog1) progress(prog1)
pos = scan(file, pos) pos = scan(f, pos)
if verbose > 1:
print "looking for valid txn header at", pos
continue continue
ltid = tid
if transaction is None: if txn is None:
undone = undone + npos - pos undone = undone + npos - pos
pos = npos pos = npos
continue continue
else: else:
pos = npos pos = npos
tid = transaction.tid tid = txn.tid
if _ts is None: if _ts is None:
_ts = TimeStamp(tid) _ts = TimeStamp(tid)
...@@ -264,58 +295,59 @@ def recover(inp, outp, verbose=0, partial=0, force=0, pack=0): ...@@ -264,58 +295,59 @@ def recover(inp, outp, verbose=0, partial=0, force=0, pack=0):
t = TimeStamp(tid) t = TimeStamp(tid)
if t <= _ts: if t <= _ts:
if ok: if ok:
print ('Time stamps out of order %s, %s' % (_ts, t)) print ("Time stamps out of order %s, %s" % (_ts, t))
ok = 0 ok = 0
_ts = t.laterThan(_ts) _ts = t.laterThan(_ts)
tid = `_ts` tid = `_ts`
else: else:
_ts = t _ts = t
if not ok: if not ok:
print ('Time stamps back in order %s' % (t)) print ("Time stamps back in order %s" % (t))
ok = 1 ok = 1
if verbose: ofs.tpc_begin(txn, tid, txn.status)
print 'begin',
if verbose > 1:
print
sys.stdout.flush()
ofs.tpc_begin(transaction, tid, transaction.status)
if verbose: if verbose:
print 'begin', pos, _ts, print "begin", pos, _ts,
if verbose > 1: if verbose > 1:
print print
sys.stdout.flush() sys.stdout.flush()
nrec = 0 nrec = 0
try: try:
for r in transaction: for r in txn:
oid = r.oid
if verbose > 1: if verbose > 1:
print u64(oid), r.version, len(r.data) if r.data is None:
pre = preindex.get(oid) l = "bp"
s = ofs.store(oid, pre, r.data, r.version, transaction) else:
preindex[oid] = s l = len(r.data)
print "%7d %s %s" % (u64(r.oid), l, r.version)
s = ofs.restore(r.oid, r.serial, r.data, r.version,
r.data_txn, txn)
nrec += 1 nrec += 1
except: except (KeyboardInterrupt, SystemExit):
raise
except Exception, err:
if partial and nrec: if partial and nrec:
ofs._status = 'p' ofs._status = "p"
ofs.tpc_vote(transaction) ofs.tpc_vote(txn)
ofs.tpc_finish(transaction) ofs.tpc_finish(txn)
if verbose: if verbose:
print 'partial' print "partial"
else: else:
ofs.tpc_abort(transaction) ofs.tpc_abort(txn)
print "\n%s: %s\n" % sys.exc_info()[:2] print "error copying transaction:", err
if not verbose: if not verbose:
progress(prog1) progress(prog1)
pos = scan(file, pos) pos = scan(f, pos)
if verbose > 1:
print "looking for valid txn header at", pos
else: else:
ofs.tpc_vote(transaction) ofs.tpc_vote(txn)
ofs.tpc_finish(transaction) ofs.tpc_finish(txn)
if verbose: if verbose:
print 'finish' print "finish"
sys.stdout.flush() sys.stdout.flush()
if not verbose: if not verbose:
...@@ -338,7 +370,6 @@ def recover(inp, outp, verbose=0, partial=0, force=0, pack=0): ...@@ -338,7 +370,6 @@ def recover(inp, outp, verbose=0, partial=0, force=0, pack=0):
ofs.close() ofs.close()
if __name__ == "__main__":
if __name__ == '__main__':
main() main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment