code: purgatorio

ref: ec35f468e0eba87c9f09cbbe5fa8af2591e6f914
dir: /appl/cmd/install/updatelog.b/

View raw version
implement Updatelog;

include "sys.m";
	sys: Sys;

include "draw.m";

include "bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;

include "daytime.m";
	daytime: Daytime;

include "string.m";
	str: String;

include "keyring.m";
	kr: Keyring;

include "logs.m";
	logs: Logs;
	Db, Entry, Byname, Byseq: import logs;
	S, mkpath: import logs;
	Log: type Entry;

include "fsproto.m";
	fsproto: FSproto;
	Direntry: import fsproto;

include "arg.m";

Updatelog: module
{
	init:	fn(nil: ref Draw->Context, nil: list of string);
};

now: int;
gen := 0;
changesonly := 0;
uid: string;
gid: string;
debug := 0;
state: ref Db;
rootdir := ".";
scanonly: list of string;
exclude: list of string;
sums := 0;
stderr: ref Sys->FD;
Seen: con 1<<31;
bout: ref Iobuf;

init(nil: ref Draw->Context, args: list of string)
{
	sys = load Sys Sys->PATH;
	bufio = load Bufio Bufio->PATH;
	ensure(bufio, Bufio->PATH);
	fsproto = load FSproto FSproto->PATH;
	ensure(fsproto, FSproto->PATH);
	daytime = load Daytime Daytime->PATH;
	ensure(daytime, Daytime->PATH);
	str = load String String->PATH;
	ensure(str, String->PATH);
	logs = load Logs Logs->PATH;
	ensure(logs, Logs->PATH);
	kr = load Keyring Keyring->PATH;
	ensure(kr, Keyring->PATH);

	arg := load Arg Arg->PATH;
	if(arg == nil)
		error(sys->sprint("can't load %s: %r", Arg->PATH));

	protofile := "/lib/proto/all";
	arg->init(args);
	arg->setusage("updatelog [-p proto] [-r root] [-t now gen] [-c] [-x path] x.log [path ...]");
	while((o := arg->opt()) != 0)
		case o {
		'D' =>
			debug = 1;
		'p' =>
			protofile = arg->earg();
		'r' =>
			rootdir = arg->earg();
		'c' =>
			changesonly = 1;
		'u' =>
			uid = arg->earg();
		'g' =>
			gid = arg->earg();
		's' =>
			sums = 1;
		't' =>
			now = int arg->earg();
			gen = int arg->earg();
		'x' =>
			s := arg->earg();
			exclude = trimpath(s) :: exclude;
		* =>
			arg->usage();
		}
	args = arg->argv();
	if(args == nil)
		arg->usage();
	arg = nil;

	stderr = sys->fildes(2);
	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);

	fsproto->init();
	logs->init(bufio);

	logfile := hd args;
	while((args = tl args) != nil)
		scanonly = trimpath(hd args) :: scanonly;
	checkroot(rootdir, "replica root");

	state = Db.new("server state");

	#
	# replay log to rebuild server state
	#
	logfd := sys->open(logfile, Sys->OREAD);
	if(logfd == nil)
		error(sys->sprint("can't open %s: %r", logfile));
	f := bufio->fopen(logfd, Sys->OREAD);
	if(f == nil)
		error(sys->sprint("can't open %s: %r", logfile));
	while((log := readlog(f)) != nil)
		replaylog(state, log);

	#
	# walk the set of names produced by the proto file, comparing against the server state
	#
	now = daytime->now();
	doproto(rootdir, protofile);

	if(changesonly){
		bout.flush();
		exit;
	}

	#
	# names in the original state that we didn't see in the walk must have been removed:
	# print 'd' log entries for them, in reverse lexicographic order (children before parents)
	#
	state.sort(Logs->Byname);
	for(i := state.nstate; --i >= 0;){
		e := state.state[i];
		if((e.x & Seen) == 0 && considered(e.path)){
			change('d', e, e.seq, e.d, e.path, e.serverpath, e.contents);	# TO DO: content
			if(debug)
				sys->fprint(sys->fildes(2), "remove %q\n", e.path);
		}
	}
	bout.flush();
}

ensure[T](m: T, path: string)
{
	if(m == nil)
		error(sys->sprint("can't load %s: %r", path));
}

checkroot(dir: string, what: string)
{
	(ok, d) := sys->stat(dir);
	if(ok < 0)
		error(sys->sprint("can't stat %s %q: %r", what, dir));
	if((d.mode & Sys->DMDIR) == 0)
		error(sys->sprint("%s %q: not a directory", what, dir));
}

considered(s: string): int
{
	if(scanonly != nil && !islisted(s, scanonly))
		return 0;
	return exclude == nil || !islisted(s, exclude);
}

readlog(in: ref Iobuf): ref Log
{
	(e, err) := Entry.read(in);
	if(err != nil)
		error(err);
	return e;
}

#
# replay a log to reach the state wrt files previously taken from the server
#
replaylog(db: ref Db, log: ref Log)
{
	e := db.look(log.path);
	indb := e != nil && !e.removed();
	case log.action {
	'a' =>	# add new file
		if(indb){
			note(sys->sprint("%q duplicate create", log.path));
			return;
		}
	'c' =>	# contents
		if(!indb){
			note(sys->sprint("%q contents but no entry", log.path));
			return;
		}
	'd' =>	# delete
		if(!indb){
			note(sys->sprint("%q deleted but no entry", log.path));
			return;
		}
		if(e.d.mtime > log.d.mtime){
			note(sys->sprint("%q deleted but it's newer", log.path));
			return;
		}
	'm' =>	# metadata
		if(!indb){
			note(sys->sprint("%q metadata but no entry", log.path));
			return;
		}
	* =>
		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
	}
	update(db, e, log);
}

#
# update file state e to reflect the effect of the log,
# creating a new entry if necessary
#
update(db: ref Db, e: ref Entry, log: ref Entry)
{
	if(e == nil)
		e = db.entry(log.seq, log.path, log.d);
	e.update(log);
}

doproto(tree: string, protofile: string)
{
	entries := chan of Direntry;
	warnings := chan of (string, string);
	err := fsproto->readprotofile(protofile, tree, entries, warnings);
	if(err != nil)
		error(sys->sprint("can't read %s: %s", protofile, err));
	for(;;)alt{
	(old, new, d) := <-entries =>
		if(d == nil)
			return;
		if(debug)
			sys->fprint(stderr, "old=%q new=%q length=%bd\n", old, new, d.length);
		while(new != nil && new[0] == '/')
			new = new[1:];
		if(!considered(new))
			continue;
		if(sums && (d.mode & Sys->DMDIR) == 0)
			digests := md5sum(old) :: nil;
		if(uid != nil)
			d.uid = uid;
		if(gid != nil)
			d.gid = gid;
		old = relative(old, rootdir);
		db := state.look(new);
		if(db == nil){
			if(!changesonly){
				db = state.entry(nextseq(), new, *d);
				change('a', db, db.seq, db.d, db.path, old, digests);
			}
		}else{
			if(!samestat(db.d, *d))
				change('c', db, nextseq(), *d, new, old, digests);
			if(!samemeta(db.d, *d))
				change('m', db, nextseq(), *d, new, old, nil);	# need digest?
		}
		if(db != nil)
			db.x |= Seen;
	(old, msg) := <-warnings =>
		#if(contains(msg, "entry not found") || contains(msg, "not exist"))
		#	break;
		sys->fprint(sys->fildes(2), "updatelog: warning[old=%s]: %s\n", old, msg);
	}
}

change(action: int, e: ref Entry, seq: big, d: Sys->Dir, path: string, serverpath: string, digests: list of string)
{
	log := ref Entry;
	log.seq = seq;
	log.action = action;
	log.d = d;
	log.path = path;
	log.serverpath = serverpath;
	log.contents = digests;
	e.update(log);
	bout.puts(log.logtext()+"\n");
}

samestat(a: Sys->Dir, b: Sys->Dir): int
{
	# doesn't check permission/ownership, does check QTDIR/QTFILE
	if(a.mode & Sys->DMDIR)
		return (b.mode & Sys->DMDIR) != 0;
	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
}

samemeta(a: Sys->Dir, b: Sys->Dir): int
{
	return a.mode == b.mode && (uid == nil || a.uid == b.uid) && (gid == nil || a.gid == b.gid) && samestat(a, b);
}

nextseq(): big
{
	return (big now << 32) | big gen++;
}

error(s: string)
{
	sys->fprint(sys->fildes(2), "updatelog: %s\n", s);
	raise "fail:error";
}

note(s: string)
{
	sys->fprint(sys->fildes(2), "updatelog: note: %s\n", s);
}

contains(s: string, sub: string): int
{
	return str->splitstrl(s, sub).t1 != nil;
}

isprefix(a, b: string): int
{
	la := len a;
	lb := len b;
	if(la > lb)
		return 0;
	if(la == lb)
		return a == b;
	return a == b[0:la] && b[la] == '/';
}

trimpath(s: string): string
{
	while(len s > 1 && s[len s-1] == '/')
		s = s[0:len s-1];
	while(s != nil && s[0] == '/')
		s = s[1:];
	return s;
}

relative(name: string, root: string): string
{
	if(root == nil || name == nil)
		return name;
	if(isprefix(root, name)){
		name = name[len root:];
		while(name != nil && name[0] == '/')
			name = name[1:];
	}
	return name;
}

islisted(s: string, l: list of string): int
{
	for(; l != nil; l = tl l)
		if(isprefix(hd l, s))
			return 1;
	return 0;
}

md5sum(file: string): string
{
	fd := sys->open(file, Sys->OREAD);
	if(fd == nil)
		error(sys->sprint("can't open %s: %r", file));
	ds: ref Keyring->DigestState;
	buf := array[Sys->ATOMICIO] of byte;
	while((n := sys->read(fd, buf, len buf)) > 0)
		ds = kr->md5(buf, n, nil, ds);
	if(n < 0)
		error(sys->sprint("error reading %s: %r", file));
	digest := array[Keyring->MD5dlen] of byte;
	kr->md5(nil, 0, digest, ds);
	s: string;
	for(i := 0; i < len digest; i++)
		s += sys->sprint("%.2ux", int digest[i]);
	return s;
}