2010-12-07 02:17:22 -08:00
|
|
|
import sys, os, errno, glob, stat, sqlite3
|
2010-11-19 03:03:05 -08:00
|
|
|
import vars
|
2010-11-25 06:35:22 -08:00
|
|
|
from helpers import unlink, err, debug2, debug3, mkdirp, close_on_exec
|
2010-11-21 02:08:05 -08:00
|
|
|
|
2010-12-09 01:56:17 -08:00
|
|
|
SCHEMA_VER=1
|
2010-12-08 21:40:42 -08:00
|
|
|
TIMEOUT=60
|
2010-12-07 02:17:22 -08:00
|
|
|
|
|
|
|
|
_db = None
|
|
|
|
|
def db():
|
|
|
|
|
global _db
|
|
|
|
|
if _db:
|
|
|
|
|
return _db
|
|
|
|
|
dbdir = '%s/.redo' % vars.BASE
|
|
|
|
|
dbfile = '%s/db.sqlite3' % dbdir
|
|
|
|
|
mkdirp(dbdir)
|
|
|
|
|
must_create = not os.path.exists(dbfile)
|
|
|
|
|
if not must_create:
|
2010-12-08 21:40:42 -08:00
|
|
|
_db = sqlite3.connect(dbfile, timeout=TIMEOUT)
|
2010-12-07 02:17:22 -08:00
|
|
|
try:
|
|
|
|
|
row = _db.cursor().execute("select version from Schema").fetchone()
|
|
|
|
|
except sqlite3.OperationalError:
|
|
|
|
|
row = None
|
|
|
|
|
ver = row and row[0] or None
|
|
|
|
|
if ver != SCHEMA_VER:
|
|
|
|
|
err("state database: discarding v%s (wanted v%s)\n"
|
|
|
|
|
% (ver, SCHEMA_VER))
|
|
|
|
|
must_create = True
|
|
|
|
|
_db = None
|
|
|
|
|
if must_create:
|
|
|
|
|
unlink(dbfile)
|
2010-12-08 21:40:42 -08:00
|
|
|
_db = sqlite3.connect(dbfile, timeout=TIMEOUT)
|
2010-12-09 01:56:17 -08:00
|
|
|
_db.execute("create table Schema "
|
|
|
|
|
" (version int)")
|
2010-12-07 02:17:22 -08:00
|
|
|
_db.execute("create table Runid "
|
|
|
|
|
" (id integer primary key autoincrement)")
|
2010-12-09 01:56:17 -08:00
|
|
|
_db.execute("create table Files "
|
|
|
|
|
" (name not null primary key, "
|
|
|
|
|
" is_generated int, "
|
|
|
|
|
" checked_runid int, "
|
|
|
|
|
" changed_runid int, "
|
|
|
|
|
" stamp, "
|
|
|
|
|
" csum)")
|
2010-12-07 02:17:22 -08:00
|
|
|
_db.execute("create table Deps "
|
2010-12-09 01:56:17 -08:00
|
|
|
" (target int, "
|
|
|
|
|
" source int, "
|
|
|
|
|
" mode not null, "
|
|
|
|
|
" primary key (target,source))")
|
2010-12-07 02:17:22 -08:00
|
|
|
_db.execute("insert into Schema (version) values (?)", [SCHEMA_VER])
|
2010-12-09 01:56:17 -08:00
|
|
|
_db.execute("insert into Runid default values") # eat the '0' runid
|
2010-12-07 02:17:22 -08:00
|
|
|
_db.commit()
|
|
|
|
|
|
|
|
|
|
if not vars.RUNID:
|
|
|
|
|
_db.execute("insert into Runid default values")
|
|
|
|
|
_db.commit()
|
|
|
|
|
vars.RUNID = _db.execute("select last_insert_rowid()").fetchone()[0]
|
|
|
|
|
os.environ['REDO_RUNID'] = str(vars.RUNID)
|
|
|
|
|
|
|
|
|
|
_db.execute("pragma journal_mode = PERSIST")
|
|
|
|
|
_db.execute("pragma synchronous = off")
|
|
|
|
|
return _db
|
|
|
|
|
|
2010-11-21 02:08:05 -08:00
|
|
|
|
|
|
|
|
def init():
|
|
|
|
|
# FIXME: just wiping out all the locks is kind of cheating. But we
|
|
|
|
|
# only do this from the toplevel redo process, so unless the user
|
|
|
|
|
# deliberately starts more than one redo on the same repository, it's
|
|
|
|
|
# sort of ok.
|
2010-12-07 02:17:22 -08:00
|
|
|
db()
|
2010-11-21 02:08:05 -08:00
|
|
|
for f in glob.glob('%s/.redo/lock*' % vars.BASE):
|
|
|
|
|
os.unlink(f)
|
2010-11-19 03:03:05 -08:00
|
|
|
|
|
|
|
|
|
2010-11-22 03:34:37 -08:00
|
|
|
_insane = None
|
|
|
|
|
def is_sane():
|
|
|
|
|
global _insane
|
|
|
|
|
if not _insane:
|
|
|
|
|
_insane = not os.path.exists('%s/.redo' % vars.BASE)
|
|
|
|
|
if _insane:
|
|
|
|
|
err('.redo directory disappeared; cannot continue.\n')
|
|
|
|
|
return not _insane
|
|
|
|
|
|
|
|
|
|
|
2010-11-24 03:45:38 -08:00
|
|
|
_cwd = None
|
2010-11-21 04:57:04 -08:00
|
|
|
def relpath(t, base):
|
2010-11-24 03:45:38 -08:00
|
|
|
global _cwd
|
|
|
|
|
if not _cwd:
|
|
|
|
|
_cwd = os.getcwd()
|
|
|
|
|
t = os.path.normpath(os.path.join(_cwd, t))
|
2010-11-21 04:57:04 -08:00
|
|
|
tparts = t.split('/')
|
|
|
|
|
bparts = base.split('/')
|
|
|
|
|
for tp,bp in zip(tparts,bparts):
|
|
|
|
|
if tp != bp:
|
|
|
|
|
break
|
|
|
|
|
tparts.pop(0)
|
|
|
|
|
bparts.pop(0)
|
|
|
|
|
while bparts:
|
|
|
|
|
tparts.insert(0, '..')
|
|
|
|
|
bparts.pop(0)
|
|
|
|
|
return '/'.join(tparts)
|
|
|
|
|
|
|
|
|
|
|
2010-12-07 02:17:22 -08:00
|
|
|
def xx_sname(typ, t):
|
2010-11-19 03:03:05 -08:00
|
|
|
# FIXME: t.replace(...) is non-reversible and non-unique here!
|
|
|
|
|
tnew = relpath(t, vars.BASE)
|
|
|
|
|
v = vars.BASE + ('/.redo/%s^%s' % (typ, tnew.replace('/', '^')))
|
2010-11-25 06:35:22 -08:00
|
|
|
if vars.DEBUG >= 3:
|
|
|
|
|
debug3('sname: (%r) %r -> %r\n' % (os.getcwd(), t, tnew))
|
2010-11-19 03:03:05 -08:00
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
2010-12-07 02:17:22 -08:00
|
|
|
class File(object):
|
|
|
|
|
__slots__ = ['id', 'name', 'is_generated',
|
|
|
|
|
'checked_runid', 'changed_runid',
|
|
|
|
|
'stamp', 'csum']
|
2010-11-19 03:03:05 -08:00
|
|
|
|
2010-12-07 02:17:22 -08:00
|
|
|
def __init__(self, id=None, name=None):
|
|
|
|
|
q = ('select rowid, name, is_generated, checked_runid, changed_runid, '
|
|
|
|
|
' stamp, csum '
|
|
|
|
|
' from Files ')
|
|
|
|
|
if id != None:
|
|
|
|
|
q += 'where rowid=?'
|
|
|
|
|
l = [id]
|
|
|
|
|
elif name != None:
|
|
|
|
|
name = relpath(name, vars.BASE)
|
|
|
|
|
q += 'where name=?'
|
|
|
|
|
l = [name]
|
2010-11-21 04:14:52 -08:00
|
|
|
else:
|
2010-12-07 02:17:22 -08:00
|
|
|
raise Exception('name or id must be set')
|
|
|
|
|
d = db()
|
|
|
|
|
row = d.execute(q, l).fetchone()
|
|
|
|
|
if not row:
|
|
|
|
|
if not name:
|
|
|
|
|
raise Exception('File with id=%r not found and '
|
|
|
|
|
'name not given' % id)
|
|
|
|
|
d.execute('insert into Files (name) values (?)', [name])
|
|
|
|
|
d.commit()
|
|
|
|
|
row = d.execute(q, l).fetchone()
|
|
|
|
|
assert(row)
|
|
|
|
|
(self.id, self.name, self.is_generated,
|
|
|
|
|
self.checked_runid, self.changed_runid,
|
|
|
|
|
self.stamp, self.csum) = row
|
|
|
|
|
|
|
|
|
|
def save(self):
|
|
|
|
|
if not os.path.exists('%s/.redo' % vars.BASE):
|
|
|
|
|
# this might happen if 'make clean' removes the .redo dir
|
|
|
|
|
return
|
|
|
|
|
d = db()
|
|
|
|
|
d.execute('update Files set '
|
|
|
|
|
' is_generated=?, checked_runid=?, changed_runid=?, '
|
|
|
|
|
' stamp=?, csum=? '
|
|
|
|
|
' where rowid=?',
|
|
|
|
|
[self.is_generated, self.checked_runid, self.changed_runid,
|
|
|
|
|
self.stamp, self.csum,
|
|
|
|
|
self.id])
|
|
|
|
|
d.commit()
|
|
|
|
|
|
|
|
|
|
def set_checked(self):
|
|
|
|
|
self.checked_runid = vars.RUNID
|
2010-11-24 04:52:17 -08:00
|
|
|
|
2010-12-07 02:17:22 -08:00
|
|
|
def set_changed(self):
|
|
|
|
|
debug2('BUILT: %r (%r)\n' % (self.name, self.stamp))
|
|
|
|
|
self.changed_runid = vars.RUNID
|
|
|
|
|
|
|
|
|
|
def set_static(self):
|
|
|
|
|
self.update_stamp()
|
|
|
|
|
|
|
|
|
|
def update_stamp(self):
|
|
|
|
|
newstamp = self.read_stamp()
|
|
|
|
|
if newstamp != self.stamp:
|
|
|
|
|
debug2("STAMP: %s: %r -> %r\n" % (self.name, self.stamp, newstamp))
|
|
|
|
|
self.stamp = newstamp
|
|
|
|
|
self.set_changed()
|
|
|
|
|
|
|
|
|
|
def is_changed(self):
|
|
|
|
|
return self.changed_runid and self.changed_runid >= vars.RUNID
|
|
|
|
|
|
|
|
|
|
def is_checked(self):
|
|
|
|
|
return (self.checked_runid and self.checked_runid >= vars.RUNID
|
|
|
|
|
and not (self.changed_runid
|
|
|
|
|
and self.changed_runid >= self.checked_runid))
|
|
|
|
|
|
|
|
|
|
def deps(self):
|
|
|
|
|
q = "select mode, source from Deps where target=?"
|
2010-12-09 00:45:13 -08:00
|
|
|
for mode,source_id in db().execute(q, [self.id]).fetchall():
|
2010-12-07 02:17:22 -08:00
|
|
|
assert(mode in ('c', 'm'))
|
|
|
|
|
name = File(id=source_id).name
|
|
|
|
|
yield mode,name
|
|
|
|
|
|
|
|
|
|
def zap_deps(self):
|
|
|
|
|
debug2('zap-deps: %r\n' % self.name)
|
|
|
|
|
db().execute('delete from Deps where target=?', [self.id])
|
|
|
|
|
db().commit()
|
|
|
|
|
|
|
|
|
|
def add_dep(self, mode, dep):
|
|
|
|
|
src = File(name=dep)
|
|
|
|
|
reldep = relpath(dep, vars.BASE)
|
|
|
|
|
debug2('add-dep: %r < %s %r\n' % (self.name, mode, reldep))
|
|
|
|
|
assert(src.name == reldep)
|
|
|
|
|
d = db()
|
|
|
|
|
d.execute("delete from Deps where target=? and source=?",
|
|
|
|
|
[self.id, src.id])
|
|
|
|
|
d.execute("insert into Deps "
|
|
|
|
|
" (target, mode, source) values (?,?,?)",
|
|
|
|
|
[self.id, mode, src.id])
|
|
|
|
|
d.commit()
|
|
|
|
|
|
|
|
|
|
def read_stamp(self):
|
|
|
|
|
try:
|
|
|
|
|
st = os.stat(os.path.join(vars.BASE, self.name))
|
|
|
|
|
except OSError:
|
|
|
|
|
return '0' # does not exist
|
|
|
|
|
if stat.S_ISDIR(st.st_mode):
|
|
|
|
|
return 'dir' # the timestamp of a directory is meaningless
|
2010-11-21 00:54:35 -08:00
|
|
|
else:
|
2010-12-07 02:17:22 -08:00
|
|
|
# a "unique identifier" stamp for a regular file
|
|
|
|
|
return str((st.st_ctime, st.st_mtime, st.st_size, st.st_ino))
|
|
|
|
|
|
2010-11-19 03:03:05 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Lock:
|
|
|
|
|
def __init__(self, t):
|
2010-11-21 03:57:52 -08:00
|
|
|
self.owned = False
|
2010-11-22 03:21:17 -08:00
|
|
|
self.rfd = self.wfd = None
|
2010-12-07 02:17:22 -08:00
|
|
|
self.lockname = xx_sname('lock', t)
|
2010-11-19 03:03:05 -08:00
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
|
if self.owned:
|
|
|
|
|
self.unlock()
|
|
|
|
|
|
2010-11-19 06:04:45 -08:00
|
|
|
def trylock(self):
|
2010-11-19 03:03:05 -08:00
|
|
|
try:
|
|
|
|
|
os.mkfifo(self.lockname, 0600)
|
|
|
|
|
self.owned = True
|
2010-11-22 03:21:17 -08:00
|
|
|
self.rfd = os.open(self.lockname, os.O_RDONLY|os.O_NONBLOCK)
|
|
|
|
|
self.wfd = os.open(self.lockname, os.O_WRONLY)
|
|
|
|
|
close_on_exec(self.rfd, True)
|
|
|
|
|
close_on_exec(self.wfd, True)
|
2010-11-19 03:03:05 -08:00
|
|
|
except OSError, e:
|
|
|
|
|
if e.errno == errno.EEXIST:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
raise
|
|
|
|
|
|
2010-11-21 22:46:20 -08:00
|
|
|
def waitlock(self):
|
|
|
|
|
while not self.owned:
|
|
|
|
|
self.wait()
|
|
|
|
|
self.trylock()
|
|
|
|
|
assert(self.owned)
|
|
|
|
|
|
2010-11-19 03:03:05 -08:00
|
|
|
def unlock(self):
|
|
|
|
|
if not self.owned:
|
|
|
|
|
raise Exception("can't unlock %r - we don't own it"
|
|
|
|
|
% self.lockname)
|
2010-11-22 03:21:17 -08:00
|
|
|
unlink(self.lockname)
|
|
|
|
|
# ping any connected readers
|
|
|
|
|
os.close(self.rfd)
|
|
|
|
|
os.close(self.wfd)
|
|
|
|
|
self.rfd = self.wfd = None
|
2010-11-19 03:03:05 -08:00
|
|
|
self.owned = False
|
|
|
|
|
|
|
|
|
|
def wait(self):
|
|
|
|
|
if self.owned:
|
|
|
|
|
raise Exception("can't wait on %r - we own it" % self.lockname)
|
|
|
|
|
try:
|
|
|
|
|
# open() will finish only when a writer exists and does close()
|
2010-11-22 03:21:17 -08:00
|
|
|
fd = os.open(self.lockname, os.O_RDONLY)
|
|
|
|
|
try:
|
|
|
|
|
os.read(fd, 1)
|
|
|
|
|
finally:
|
|
|
|
|
os.close(fd)
|
2010-11-19 03:03:05 -08:00
|
|
|
except OSError, e:
|
|
|
|
|
if e.errno == errno.ENOENT:
|
|
|
|
|
pass # it's not even unlocked or was unlocked earlier
|
|
|
|
|
else:
|
|
|
|
|
raise
|