xref: /inferno-os/appl/cmd/install/updatelog.b (revision 37da2899f40661e3e9631e497da8dc59b971cbd0)
1implement Updatelog;
2
3include "sys.m";
4	sys: Sys;
5
6include "draw.m";
7
8include "bufio.m";
9	bufio: Bufio;
10	Iobuf: import bufio;
11
12include "daytime.m";
13	daytime: Daytime;
14
15include "string.m";
16	str: String;
17
18include "keyring.m";
19	kr: Keyring;
20
21include "logs.m";
22	logs: Logs;
23	Db, Entry, Byname, Byseq: import logs;
24	S, mkpath: import logs;
25	Log: type Entry;
26
27include "fsproto.m";
28	fsproto: FSproto;
29	Direntry: import fsproto;
30
31include "arg.m";
32
33Updatelog: module
34{
35	init:	fn(nil: ref Draw->Context, nil: list of string);
36};
37
38now: int;
39gen := 0;
40changesonly := 0;
41uid: string;
42gid: string;
43debug := 0;
44state: ref Db;
45rootdir := ".";
46scanonly: list of string;
47exclude: list of string;
48sums := 0;
49stderr: ref Sys->FD;
50Seen: con 1<<31;
51bout: ref Iobuf;
52
53init(nil: ref Draw->Context, args: list of string)
54{
55	sys = load Sys Sys->PATH;
56	bufio = load Bufio Bufio->PATH;
57	ensure(bufio, Bufio->PATH);
58	fsproto = load FSproto FSproto->PATH;
59	ensure(fsproto, FSproto->PATH);
60	daytime = load Daytime Daytime->PATH;
61	ensure(daytime, Daytime->PATH);
62	str = load String String->PATH;
63	ensure(str, String->PATH);
64	logs = load Logs Logs->PATH;
65	ensure(logs, Logs->PATH);
66	kr = load Keyring Keyring->PATH;
67	ensure(kr, Keyring->PATH);
68
69	arg := load Arg Arg->PATH;
70	if(arg == nil)
71		error(sys->sprint("can't load %s: %r", Arg->PATH));
72
73	protofile := "/lib/proto/all";
74	arg->init(args);
75	arg->setusage("updatelog [-p proto] [-r root] [-t now gen] [-c] [-x path] x.log [path ...]");
76	while((o := arg->opt()) != 0)
77		case o {
78		'D' =>
79			debug = 1;
80		'p' =>
81			protofile = arg->earg();
82		'r' =>
83			rootdir = arg->earg();
84		'c' =>
85			changesonly = 1;
86		'u' =>
87			uid = arg->earg();
88		'g' =>
89			gid = arg->earg();
90		's' =>
91			sums = 1;
92		't' =>
93			now = int arg->earg();
94			gen = int arg->earg();
95		'x' =>
96			s := arg->earg();
97			exclude = trimpath(s) :: exclude;
98		* =>
99			arg->usage();
100		}
101	args = arg->argv();
102	if(args == nil)
103		arg->usage();
104	arg = nil;
105
106	stderr = sys->fildes(2);
107	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
108
109	fsproto->init();
110	logs->init(bufio);
111
112	logfile := hd args;
113	while((args = tl args) != nil)
114		scanonly = trimpath(hd args) :: scanonly;
115	checkroot(rootdir, "replica root");
116
117	state = Db.new("server state");
118
119	#
120	# replay log to rebuild server state
121	#
122	logfd := sys->open(logfile, Sys->OREAD);
123	if(logfd == nil)
124		error(sys->sprint("can't open %s: %r", logfile));
125	f := bufio->fopen(logfd, Sys->OREAD);
126	if(f == nil)
127		error(sys->sprint("can't open %s: %r", logfile));
128	while((log := readlog(f)) != nil)
129		replaylog(state, log);
130
131	#
132	# walk the set of names produced by the proto file, comparing against the server state
133	#
134	now = daytime->now();
135	doproto(rootdir, protofile);
136
137	if(changesonly){
138		bout.flush();
139		exit;
140	}
141
142	#
143	# names in the original state that we didn't see in the walk must have been removed:
144	# print 'd' log entries for them, in reverse lexicographic order (children before parents)
145	#
146	state.sort(Logs->Byname);
147	for(i := state.nstate; --i >= 0;){
148		e := state.state[i];
149		if((e.x & Seen) == 0 && considered(e.path)){
150			change('d', e, e.seq, e.d, e.path, e.serverpath, e.contents);	# TO DO: content
151			if(debug)
152				sys->fprint(sys->fildes(2), "remove %q\n", e.path);
153		}
154	}
155	bout.flush();
156}
157
158ensure[T](m: T, path: string)
159{
160	if(m == nil)
161		error(sys->sprint("can't load %s: %r", path));
162}
163
164checkroot(dir: string, what: string)
165{
166	(ok, d) := sys->stat(dir);
167	if(ok < 0)
168		error(sys->sprint("can't stat %s %q: %r", what, dir));
169	if((d.mode & Sys->DMDIR) == 0)
170		error(sys->sprint("%s %q: not a directory", what, dir));
171}
172
173considered(s: string): int
174{
175	if(scanonly != nil && !islisted(s, scanonly))
176		return 0;
177	return exclude == nil || !islisted(s, exclude);
178}
179
180readlog(in: ref Iobuf): ref Log
181{
182	(e, err) := Entry.read(in);
183	if(err != nil)
184		error(err);
185	return e;
186}
187
188#
189# replay a log to reach the state wrt files previously taken from the server
190#
191replaylog(db: ref Db, log: ref Log)
192{
193	e := db.look(log.path);
194	indb := e != nil && !e.removed();
195	case log.action {
196	'a' =>	# add new file
197		if(indb){
198			note(sys->sprint("%q duplicate create", log.path));
199			return;
200		}
201	'c' =>	# contents
202		if(!indb){
203			note(sys->sprint("%q contents but no entry", log.path));
204			return;
205		}
206	'd' =>	# delete
207		if(!indb){
208			note(sys->sprint("%q deleted but no entry", log.path));
209			return;
210		}
211		if(e.d.mtime > log.d.mtime){
212			note(sys->sprint("%q deleted but it's newer", log.path));
213			return;
214		}
215	'm' =>	# metadata
216		if(!indb){
217			note(sys->sprint("%q metadata but no entry", log.path));
218			return;
219		}
220	* =>
221		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
222	}
223	update(db, e, log);
224}
225
226#
227# update file state e to reflect the effect of the log,
228# creating a new entry if necessary
229#
230update(db: ref Db, e: ref Entry, log: ref Entry)
231{
232	if(e == nil)
233		e = db.entry(log.seq, log.path, log.d);
234	e.update(log);
235}
236
237doproto(tree: string, protofile: string)
238{
239	entries := chan of Direntry;
240	warnings := chan of (string, string);
241	err := fsproto->readprotofile(protofile, tree, entries, warnings);
242	if(err != nil)
243		error(sys->sprint("can't read %s: %s", protofile, err));
244	for(;;)alt{
245	(old, new, d) := <-entries =>
246		if(d == nil)
247			return;
248		if(debug)
249			sys->fprint(stderr, "old=%q new=%q length=%bd\n", old, new, d.length);
250		while(new != nil && new[0] == '/')
251			new = new[1:];
252		if(!considered(new))
253			continue;
254		if(sums && (d.mode & Sys->DMDIR) == 0)
255			digests := md5sum(old) :: nil;
256		if(uid != nil)
257			d.uid = uid;
258		if(gid != nil)
259			d.gid = gid;
260		old = relative(old, rootdir);
261		db := state.look(new);
262		if(db == nil){
263			if(!changesonly){
264				db = state.entry(nextseq(), new, *d);
265				change('a', db, db.seq, db.d, db.path, old, digests);
266			}
267		}else{
268			if(!samestat(db.d, *d))
269				change('c', db, nextseq(), *d, new, old, digests);
270			if(!samemeta(db.d, *d))
271				change('m', db, nextseq(), *d, new, old, nil);	# need digest?
272		}
273		if(db != nil)
274			db.x |= Seen;
275	(old, msg) := <-warnings =>
276		#if(contains(msg, "entry not found") || contains(msg, "not exist"))
277		#	break;
278		sys->fprint(sys->fildes(2), "updatelog: warning[old=%s]: %s\n", old, msg);
279	}
280}
281
282change(action: int, e: ref Entry, seq: big, d: Sys->Dir, path: string, serverpath: string, digests: list of string)
283{
284	log := ref Entry;
285	log.seq = seq;
286	log.action = action;
287	log.d = d;
288	log.path = path;
289	log.serverpath = serverpath;
290	log.contents = digests;
291	e.update(log);
292	bout.puts(log.logtext()+"\n");
293}
294
295samestat(a: Sys->Dir, b: Sys->Dir): int
296{
297	# doesn't check permission/ownership, does check QTDIR/QTFILE
298	if(a.mode & Sys->DMDIR)
299		return (b.mode & Sys->DMDIR) != 0;
300	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
301}
302
303samemeta(a: Sys->Dir, b: Sys->Dir): int
304{
305	return a.mode == b.mode && (uid == nil || a.uid == b.uid) && (gid == nil || a.gid == b.gid) && samestat(a, b);
306}
307
308nextseq(): big
309{
310	return (big now << 32) | big gen++;
311}
312
313error(s: string)
314{
315	sys->fprint(sys->fildes(2), "updatelog: %s\n", s);
316	raise "fail:error";
317}
318
319note(s: string)
320{
321	sys->fprint(sys->fildes(2), "updatelog: note: %s\n", s);
322}
323
324contains(s: string, sub: string): int
325{
326	return str->splitstrl(s, sub).t1 != nil;
327}
328
329isprefix(a, b: string): int
330{
331	la := len a;
332	lb := len b;
333	if(la > lb)
334		return 0;
335	if(la == lb)
336		return a == b;
337	return a == b[0:la] && b[la] == '/';
338}
339
340trimpath(s: string): string
341{
342	while(len s > 1 && s[len s-1] == '/')
343		s = s[0:len s-1];
344	while(s != nil && s[0] == '/')
345		s = s[1:];
346	return s;
347}
348
349relative(name: string, root: string): string
350{
351	if(root == nil || name == nil)
352		return name;
353	if(isprefix(root, name)){
354		name = name[len root:];
355		while(name != nil && name[0] == '/')
356			name = name[1:];
357	}
358	return name;
359}
360
361islisted(s: string, l: list of string): int
362{
363	for(; l != nil; l = tl l)
364		if(isprefix(hd l, s))
365			return 1;
366	return 0;
367}
368
369md5sum(file: string): string
370{
371	fd := sys->open(file, Sys->OREAD);
372	if(fd == nil)
373		error(sys->sprint("can't open %s: %r", file));
374	ds: ref Keyring->DigestState;
375	buf := array[Sys->ATOMICIO] of byte;
376	while((n := sys->read(fd, buf, len buf)) > 0)
377		ds = kr->md5(buf, n, nil, ds);
378	if(n < 0)
379		error(sys->sprint("error reading %s: %r", file));
380	digest := array[Keyring->MD5dlen] of byte;
381	kr->md5(nil, 0, digest, ds);
382	s: string;
383	for(i := 0; i < len digest; i++)
384		s += sys->sprint("%.2ux", int digest[i]);
385	return s;
386}
387