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