summaryrefslogtreecommitdiff
path: root/appl/cmd/install/applylog.b
diff options
context:
space:
mode:
authorCharles.Forsyth <devnull@localhost>2006-12-22 17:07:39 +0000
committerCharles.Forsyth <devnull@localhost>2006-12-22 17:07:39 +0000
commit37da2899f40661e3e9631e497da8dc59b971cbd0 (patch)
treecbc6d4680e347d906f5fa7fca73214418741df72 /appl/cmd/install/applylog.b
parent54bc8ff236ac10b3eaa928fd6bcfc0cdb2ba46ae (diff)
20060303a
Diffstat (limited to 'appl/cmd/install/applylog.b')
-rw-r--r--appl/cmd/install/applylog.b699
1 files changed, 699 insertions, 0 deletions
diff --git a/appl/cmd/install/applylog.b b/appl/cmd/install/applylog.b
new file mode 100644
index 00000000..6a1b2e63
--- /dev/null
+++ b/appl/cmd/install/applylog.b
@@ -0,0 +1,699 @@
+implement Applylog;
+
+#
+# apply a plan 9-style replica log
+# this version applies everything and doesn't use the database
+#
+
+include "sys.m";
+ sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+ bufio: Bufio;
+ Iobuf: import bufio;
+
+include "string.m";
+ str: String;
+
+include "keyring.m";
+ kr: Keyring;
+
+include "daytime.m";
+ daytime: Daytime;
+
+include "logs.m";
+ logs: Logs;
+ Db, Entry, Byname, Byseq: import logs;
+ S: import logs;
+
+include "arg.m";
+
+Applylog: module
+{
+ init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Apply, Applydb, Install, Asis, Skip: con iota;
+
+client: ref Db; # client current state from client log
+updates: ref Db; # state delta from new section of server log
+
+nerror := 0;
+nconflict := 0;
+debug := 0;
+verbose := 0;
+resolve := 0;
+setuid := 0;
+setgid := 0;
+nflag := 0;
+timefile: string;
+clientroot: string;
+srvroot: string;
+logfd: ref Sys->FD;
+now := 0;
+gen := 0;
+noerr := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+ sys = load Sys Sys->PATH;
+
+ bufio = load Bufio Bufio->PATH;
+ ensure(bufio, Bufio->PATH);
+ str = load String String->PATH;
+ ensure(str, String->PATH);
+ kr = load Keyring Keyring->PATH;
+ ensure(kr, Keyring->PATH);
+ daytime = load Daytime Daytime->PATH;
+ ensure(daytime, Daytime->PATH);
+ logs = load Logs Logs->PATH;
+ ensure(logs, Logs->PATH);
+ logs->init(bufio);
+
+ arg := load Arg Arg->PATH;
+ ensure(arg, Arg->PATH);
+ arg->init(args);
+ arg->setusage("applylog [-vuged] [-sc] [-T timefile] clientlog clientroot serverroot [path ... ] <serverlog");
+ dump := 0;
+ while((o := arg->opt()) != 0)
+ case o {
+ 'T' => timefile = arg->earg();
+ 'd' => dump = 1; debug = 1;
+ 'e' => noerr = 1;
+ 'g' => setgid = 1;
+ 'n' => nflag = 1; verbose = 1;
+ 's' or 'c' => resolve = o;
+ 'u' => setuid = 1;
+ 'v' => verbose = 1;
+ * => arg->usage();
+ }
+ args = arg->argv();
+ if(len args < 3)
+ arg->usage();
+ arg = nil;
+
+ now = daytime->now();
+ client = Db.new("client log");
+ updates = Db.new("update log");
+ clientlog := hd args; args = tl args;
+ clientroot = hd args; args = tl args;
+ srvroot = hd args; args = tl args;
+ if(args != nil)
+ error("restriction by path not yet done");
+
+ checkroot(clientroot, "client root");
+ checkroot(srvroot, "server root");
+
+ # replay the client log to build last installation state of files taken from server
+ if(nflag)
+ logfd = sys->open(clientlog, Sys->OREAD);
+ else
+ logfd = sys->open(clientlog, Sys->ORDWR);
+ if(logfd == nil)
+ error(sys->sprint("can't open %s: %r", clientlog));
+ f := bufio->fopen(logfd, Sys->OREAD);
+ if(f == nil)
+ error(sys->sprint("can't open %s: %r", clientlog));
+ while((log := readlog(f)) != nil)
+ replaylog(client, log);
+ f = nil;
+ sys->seek(logfd, big 0, 2);
+ if(dump)
+ dumpstate();
+ if(debug){
+ sys->print(" CLIENT STATE\n");
+ client.sort(Byname);
+ dumpdb(client, 0);
+ }
+
+ # read server's log and use the new section to build a sequence of update actions
+ minseq := big 0;
+ if(timefile != nil)
+ minseq = readseq(timefile);
+ f = bufio->fopen(sys->fildes(0), Sys->OREAD);
+ while((log = readlog(f)) != nil)
+ if(log.seq > minseq)
+ update(updates, updates.look(log.path), log);
+ updates.sort(Byseq);
+ if(debug){
+ sys->print(" SEQUENCED UPDATES\n");
+ dumpdb(updates, 1);
+ }
+
+ # apply those actions
+ maxseq := minseq;
+ skip := 0;
+ for(i := 0; i < updates.nstate; i++){
+ e := updates.state[i];
+ ce := client.look(e.path);
+ if(ce != nil && ce.seq >= e.seq){ # replay
+ if(debug)
+ sys->print("replay %c %q\n", e.action, e.path);
+ if(!nflag && !skip)
+ maxseq = e.seq;
+ continue;
+ }
+ if(verbose)
+ sys->print("%s\n", e.sumtext());
+ case chooseaction(e) {
+ Install =>
+ if(debug)
+ sys->print("resolve %q to install\n", e.path);
+ c := e;
+ c.action = 'a'; # force (re)creation/installation
+ if(!enact(c)){
+ skip = 1;
+ continue; # don't update db
+ }
+ Apply =>
+ if(!enact(e)){
+ skip = 1;
+ continue; # don't update db
+ }
+ Applydb =>
+ if(debug)
+ sys->print("resolve %q to update db\n", e.path);
+ # carry on to update the log
+ Asis =>
+ if(debug)
+ sys->print("resolve %q to client\n", e.path);
+ #continue; # ?
+ Skip =>
+ if(debug)
+ sys->print("conflict %q\n", e.path);
+ skip = 1;
+ continue;
+ * =>
+ error("internal error: unexpected result from chooseaction");
+ }
+ # action complete: add to client log
+ if(ce == nil)
+ ce = client.entry(e.seq, e.path, e.d);
+ ce.update(e);
+ if(!nflag){
+ if(!skip)
+ maxseq = e.seq;
+ if(logfd != nil){
+ # append action, now accepted, to client's own log
+ if(sys->fprint(logfd, "%s\n", e.logtext()) < 0)
+ error(sys->sprint("error writing to %q: %r", clientlog));
+ }
+ }
+ }
+ sys->fprint(sys->fildes(2), "maxseq: %bud %bud\n", maxseq>>32, maxseq & 16rFFFFFFFF);
+ if(!nflag && !skip && timefile != nil)
+ writeseq(timefile, maxseq);
+ if(nconflict)
+ raise sys->sprint("fail:%d conflicts", nconflict);
+ if(nerror)
+ raise sys->sprint("fail:%d errors", nerror);
+}
+
+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));
+}
+
+readlog(in: ref Iobuf): ref Entry
+{
+ (e, err) := Entry.read(in);
+ if(err != nil)
+ error(err);
+ return e;
+}
+
+readseq(file: string): big
+{
+ fd := sys->open(file, Sys->OREAD);
+ if(fd == nil)
+ error(sys->sprint("can't open %q: %r", file));
+ buf := array[128] of byte;
+ n := sys->read(fd, buf, len buf);
+ if(n <= 0)
+ error(sys->sprint("can't read valid seq from %q", file));
+ (nf, flds) := sys->tokenize(string buf[0:n], " \t\n");
+ if(nf != 2)
+ error(sys->sprint("illegal sequence number in %q", file));
+ n0 := bigof(hd flds, 10);
+ n1 := bigof(hd tl flds, 10);
+ return (n0 << 32) | n1;
+}
+
+writeseq(file: string, n: big)
+{
+ fd := sys->create(file, Sys->OWRITE, 8r666);
+ if(fd == nil)
+ error(sys->sprint("can't create %q: %r", file));
+ if(sys->fprint(fd, "%11bud %11bud", n>>32, n&16rFFFFFFFF) < 0)
+ error(sys->sprint("error writing seq to %q: %r", file));
+}
+
+#
+# replay a log to reach the state wrt files previously taken from the server
+#
+replaylog(db: ref Db, log: ref Entry)
+{
+ 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);
+}
+
+chooseaction(e: ref Entry): int
+{
+ cf := logs->mkpath(clientroot, e.path);
+ sf := logs->mkpath(srvroot, e.serverpath);
+ (ishere, cd) := sys->stat(logs->mkpath(clientroot, e.path));
+ ishere = ishere >= 0; # in local file system
+ db := client.look(e.path);
+ indb := db != nil && !db.removed(); # previously arrived from server
+
+ unchanged := indb && ishere && (samestat(db.d, cd) || samecontents(sf, cf)) || !indb && !ishere;
+ if(unchanged && (e.action != 'm' || samemeta(db.d, cd)))
+ return Apply;
+ if(!ishere && e.action == 'd'){
+ if(indb)
+ return Applydb;
+ return Asis;
+ }
+ case resolve {
+ 'c' =>
+ return Asis;
+ 's' =>
+ if(!ishere || e.action == 'm' && !unchanged)
+ return Install;
+ return Apply;
+ * =>
+ # describe source of conflict
+ if(indb){
+ if(ishere){
+ if(e.action == 'm' && unchanged && !samemeta(db.d, cd))
+ conflict(e.path, "locally modified metadata", action(e.action));
+ else
+ conflict(e.path, "locally modified", action(e.action));
+ }else
+ conflict(e.path, "locally removed", action(e.action));
+ }else{
+ if(db != nil)
+ conflict(e.path, "locally retained or recreated", action(e.action)); # server installed it but later removed it
+ else
+ conflict(e.path, "locally created", action(e.action));
+ }
+ return Skip;
+ }
+}
+
+enact(e: ref Entry): int
+{
+ if(nflag)
+ return 0;
+ srcfile := logs->mkpath(srvroot, e.serverpath);
+ dstfile := logs->mkpath(clientroot, e.path);
+ case e.action {
+ 'a' => # create and copy in
+ if(debug)
+ sys->print("create %q\n", dstfile);
+ if(e.d.mode & Sys->DMDIR)
+ err := mkdir(dstfile, e);
+ else
+ err = copyin(srcfile, dstfile, 1, e);
+ if(err != nil){
+ if(noerr)
+ error(err);
+ warn(err);
+ return 0;
+ }
+ 'c' => # contents
+ err := copyin(srcfile, dstfile, 0, e);
+ if(err != nil){
+ if(noerr)
+ error(err);
+ warn(err);
+ return 0;
+ }
+ 'd' => # delete
+ if(debug)
+ sys->print("remove %q\n", dstfile);
+ if(remove(dstfile) < 0){
+ warn(sys->sprint("can't remove %q: %r", dstfile));
+ return 0;
+ }
+ 'm' => # metadata
+ if(debug)
+ sys->print("wstat %q\n", dstfile);
+ d := sys->nulldir;
+ d.mode = e.d.mode;
+ if(sys->wstat(dstfile, d) < 0)
+ warn(sys->sprint("%q: can't change mode to %uo", dstfile, d.mode));
+ if(setgid){
+ d = sys->nulldir;
+ d.gid = e.d.gid;
+ if(sys->wstat(dstfile, d) < 0)
+ warn(sys->sprint("%q: can't change gid to %q", dstfile, d.gid));
+ }
+ if(setuid){
+ d = sys->nulldir;
+ d.uid = e.d.uid;
+ if(sys->wstat(dstfile, d) < 0)
+ warn(sys->sprint("%q: can't change uid to %q", dstfile, d.uid));
+ }
+ * =>
+ error(sys->sprint("unexpected log operation: %c %q", e.action, e.path));
+ return 0;
+ }
+ return 1;
+}
+
+rev[T](l: list of T): list of T
+{
+ rl: list of T;
+ for(; l != nil; l = tl l)
+ rl = hd l :: rl;
+ return rl;
+}
+
+ensure[T](m: T, path: string)
+{
+ if(m == nil)
+ error(sys->sprint("can't load %s: %r", path));
+}
+
+error(s: string)
+{
+ sys->fprint(sys->fildes(2), "applylog: %s\n", s);
+ raise "fail:error";
+}
+
+note(s: string)
+{
+ sys->fprint(sys->fildes(2), "applylog: note: %s\n", s);
+}
+
+warn(s: string)
+{
+ sys->fprint(sys->fildes(2), "applylog: warning: %s\n", s);
+ nerror++;
+}
+
+conflict(name: string, why: string, wont: string)
+{
+ sys->fprint(sys->fildes(2), "%q: %s; will not %s\n", name, why, wont);
+ nconflict++;
+}
+
+action(a: int): string
+{
+ case a {
+ 'a' => return "create";
+ 'c' => return "update";
+ 'd' => return "delete";
+ 'm' => return "update metadata";
+ * => return sys->sprint("unknown action %c", a);
+ }
+}
+
+samecontents(path1, path2: string): int
+{
+ f1 := sys->open(path1, Sys->OREAD);
+ if(f1 == nil)
+ return 0;
+ f2 := sys->open(path2, Sys->OREAD);
+ if(f2 == nil)
+ return 0;
+ b1 := array[Sys->ATOMICIO] of byte;
+ b2 := array[Sys->ATOMICIO] of byte;
+ n := 256; # start with something small; dis files and big executables should fail more quickly
+ n1, n2: int;
+ do{
+ n1 = sys->read(f1, b1, n);
+ n2 = sys->read(f2, b2, n);
+ if(n1 != n2)
+ return 0;
+ for(i := 0; i < n1; i++)
+ if(b1[i] != b2[i])
+ return 0;
+ n += len b1 - n;
+ }while(n1 > 0);
+ return 1;
+}
+
+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 && (!setuid || a.uid == b.uid) && (!setgid || a.gid == b.gid) && samestat(a, b);
+}
+
+bigof(s: string, base: int): big
+{
+ (b, r) := str->tobig(s, base);
+ if(r != nil)
+ error("cruft in integer field in log entry: "+s);
+ return b;
+}
+
+intof(s: string, base: int): int
+{
+ return int bigof(s, base);
+}
+
+mkdir(dstpath: string, e: ref Entry): string
+{
+ fd := create(dstpath, Sys->OREAD, e.d.mode);
+ if(fd == nil)
+ return sys->sprint("can't mkdir %q: %r", dstpath);
+ fchmod(fd, e.d.mode);
+ if(setgid)
+ fchgrp(fd, e.d.gid);
+ if(setuid)
+ fchown(fd, e.d.uid);
+# e.d.mtime = now;
+ return nil;
+}
+
+fchmod(fd: ref Sys->FD, mode: int)
+{
+ d := sys->nulldir;
+ d.mode = mode;
+ if(sys->fwstat(fd, d) < 0)
+ warn(sys->sprint("%q: can't set mode %o: %r", sys->fd2path(fd), mode));
+}
+
+fchgrp(fd: ref Sys->FD, gid: string)
+{
+ d := sys->nulldir;
+ d.gid = gid;
+ if(sys->fwstat(fd, d) < 0)
+ warn(sys->sprint("%q: can't set group id %s: %r", sys->fd2path(fd), gid));
+}
+
+fchown(fd: ref Sys->FD, uid: string)
+{
+ d := sys->nulldir;
+ d.uid = uid;
+ if(sys->fwstat(fd, d) < 0)
+ warn(sys->sprint("%q: can't set user id %s: %r", sys->fd2path(fd), uid));
+}
+
+copyin(srcpath: string, dstpath: string, dowstat: int, e: ref Entry): string
+{
+ if(debug)
+ sys->print("copyin %q -> %q\n", srcpath, dstpath);
+ f := sys->open(srcpath, Sys->OREAD);
+ if(f == nil)
+ return sys->sprint("can't open %q: %r", srcpath);
+ t: ref Sys->FD;
+ (ok, nil) := sys->stat(dstpath);
+ if(ok < 0){
+ t = create(dstpath, Sys->OWRITE, e.d.mode | 8r222);
+ if(t == nil)
+ return sys->sprint("can't create %q: %r", dstpath);
+ # TO DO: force access to parent directory
+ dowstat = 1;
+ }else{
+ t = sys->open(dstpath, Sys->OWRITE|Sys->OTRUNC);
+ if(t == nil){
+ err := sys->sprint("%r");
+ if(!contains(err, "permission"))
+ return sys->sprint("can't overwrite %q: %s", dstpath, err);
+ }
+ }
+ (nw, err) := copy(f, t);
+ if(err != nil)
+ return err;
+ if(nw != e.d.length)
+ warn(sys->sprint("%q: log said %bud bytes, copied %bud bytes", dstpath, e.d.length, nw));
+ f = nil;
+ if(dowstat){
+ fchmod(t, e.d.mode);
+ if(setgid)
+ fchgrp(t, e.d.gid);
+ if(setuid)
+ fchown(t, e.d.uid);
+ }
+ nd := sys->nulldir;
+ nd.mtime = e.d.mtime;
+ if(sys->fwstat(t, nd) < 0)
+ warn(sys->sprint("%q: can't set mtime: %r", dstpath));
+ return nil;
+}
+
+copy(f: ref Sys->FD, t: ref Sys->FD): (big, string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ nw := big 0;
+ while((n := sys->read(f, buf, len buf)) > 0){
+ if(sys->write(t, buf, n) != n)
+ return (nw, sys->sprint("error writing %q: %r", sys->fd2path(t)));
+ nw += big n;
+ }
+ if(n < 0)
+ return (nw, sys->sprint("error reading %q: %r", sys->fd2path(f)));
+ return (nw, nil);
+}
+
+contents(e: ref Entry): string
+{
+ s := "";
+ for(cl := e.contents; cl != nil; cl = tl cl)
+ s += " " + hd cl;
+ return s;
+}
+
+dumpstate()
+{
+ for(i := 0; i < client.nstate; i++)
+ sys->print("%d\t%s\n", i, client.state[i].text());
+}
+
+dumpdb(db: ref Db, tag: int)
+{
+ for(i := 0; i < db.nstate; i++){
+ if(!tag)
+ s := db.state[i].dbtext();
+ else
+ s = db.state[i].text();
+ if(s != nil)
+ sys->print("%s\n", s);
+ }
+}
+
+#
+# perhaps these should be in a utility module
+#
+parent(name: string): string
+{
+ slash := -1;
+ for(i := 0; i < len name; i++)
+ if(name[i] == '/')
+ slash = i;
+ if(slash > 0)
+ return name[0:slash];
+ return "/";
+}
+
+writableparent(name: string): (int, string)
+{
+ p := parent(name);
+ (ok, d) := sys->stat(p);
+ if(ok < 0)
+ return (-1, nil);
+ nd := sys->nulldir;
+ nd.mode |= 8r222;
+ sys->wstat(p, nd);
+ return (d.mode, p);
+}
+
+create(name: string, rw: int, mode: int): ref Sys->FD
+{
+ fd := sys->create(name, rw, mode);
+ if(fd == nil){
+ err := sys->sprint("%r");
+ if(!contains(err, "permission")){
+ sys->werrstr(err);
+ return nil;
+ }
+ (pm, p) := writableparent(name);
+ if(pm >= 0){
+ fd = sys->create(name, rw, mode);
+ d := sys->nulldir;
+ d.mode = pm;
+ sys->wstat(p, d);
+ }
+ sys->werrstr(err);
+ }
+ return fd;
+}
+
+remove(name: string): int
+{
+ if(sys->remove(name) >= 0)
+ return 0;
+ err := sys->sprint("%r");
+ if(contains(err, "entry not found") || contains(err, "not exist"))
+ return 0;
+ (pm, p) := writableparent(name);
+ rc := sys->remove(name);
+ d := sys->nulldir;
+ if(pm >= 0){
+ d.mode = pm;
+ sys->wstat(p, d);
+ }
+ sys->werrstr(err);
+ return rc;
+}
+
+contains(s: string, sub: string): int
+{
+ return str->splitstrl(s, sub).t1 != nil;
+}