source: trunk/athena/etc/synctree/synctree.c @ 10717

Revision 10717, 17.3 KB checked in by ghudson, 27 years ago (diff)
Ditch alloca for malloc.
Line 
1/* Copyright (C) 1988  Tim Shepard   All rights reserved. */
2
3#include <stdio.h>
4#include <sys/types.h>
5#include <sys/param.h>
6#include <unistd.h>
7#include <dirent.h>
8#include <sys/stat.h>
9
10#include <sys/time.h>
11#include <sys/resource.h>
12#include <string.h>
13#ifdef OLD
14#include <ndbm.h>
15#endif
16#include <errno.h>
17
18#ifdef SOLARIS
19#define NO_LINEBUF
20#endif
21#include "synctree.h"
22
23#ifndef DEBUG
24char *srcdir = "/server";
25char *dstdir = "";            /* effectively "/" */
26char *src_rule_file = ".rconf";
27char *dst_rule_file = ".rconf.local";
28char *date_db_name  = ".reconcile_dates";
29#else /* DEBUG */
30char *srcdir = "s";
31char *dstdir = "d";
32char *src_rule_file = ".rconf";
33char *dst_rule_file = ".rconf.local";
34#ifdef DATE
35char *date_db_name  = ".reconcile_dates";
36#endif
37#endif /* DEBUG */
38
39rule rules[MAXNRULES];
40unsigned int lastrule;
41unsigned int rflag = 0;
42
43int dodir();
44char *destination_pathname();
45
46int  verbosef = 1;
47int  nflag = 0;
48int  aflag = 0;
49char *afile;
50
51bool nosrcrules = FALSE;
52bool nodstrules = FALSE;
53uid_t uid, euid;
54#ifndef SOLARIS
55uid_t getuid(), geteuid();
56#endif
57void usage(arg0)
58     char *arg0;
59{
60  fprintf(stderr,"Usage: %s [-v] [-n] [-nosrcrules] [-nodstrules] [-s srcdir] [-d dstdir] [-a rules]\n",arg0);
61  exit(1);
62}
63
64main(argc, argv)
65     int argc;
66     char *argv[];
67{
68  int i;
69#ifndef NO_RLIMIT
70  struct rlimit rl;
71#endif
72
73#ifndef NO_LINEBUF
74  setlinebuf(stdout);
75#else
76  setvbuf(stdout,NULL,_IOLBF,BUFSIZ);
77#endif
78
79  uid = getuid();
80  euid = geteuid();
81
82#ifndef NO_RLIMIT
83  getrlimit(RLIMIT_DATA,&rl);
84  rl.rlim_cur = rl.rlim_max;
85  setrlimit(RLIMIT_DATA,&rl);
86
87  getrlimit(RLIMIT_STACK,&rl);
88  rl.rlim_cur = rl.rlim_max;
89  setrlimit(RLIMIT_STACK,&rl);
90#endif
91
92  i = 0;
93 
94  while (++i < argc)
95    {
96      if (argv[i][0] != '-') usage(argv[0]);
97
98      switch (argv[i][1]) {
99      case 's':
100        if (argv[i][2])
101          { srcdir = (char *) malloc(strlen(&argv[i][2])+1);
102            (void) strcpy(srcdir, &argv[i][2]);
103          }
104        else if (++i >= argc) usage(argv[0]);
105        else
106          { srcdir = (char *) malloc(strlen(&argv[i][0])+1);
107            (void) strcpy(srcdir, argv[i]);
108          }
109        break;
110
111      case 'd':
112        if (argv[i][2])
113          { dstdir = (char *) malloc(strlen(&argv[i][2])+1);
114            (void) strcpy(dstdir, &argv[i][2]);
115          }
116        else if (++i >= argc) usage(argv[0]);
117        else
118          { dstdir = (char *) malloc(strlen(&argv[i][0])+1);
119            (void) strcpy(dstdir, argv[i]);
120          }
121        if (strcmp(dstdir,"/") == 0)
122          dstdir[0] = '\0';
123        break;
124
125      case 'a':
126        if (argv[i][2])
127          { afile = (char *) malloc(strlen(&argv[i][2])+1);
128            (void) strcpy(afile, &argv[i][2]);
129          }
130        else if (++i >= argc) usage(argv[0]);
131        else
132          { afile = (char *) malloc(strlen(&argv[i][0])+1);
133            (void) strcpy(afile, argv[i]);
134          }
135        if (strcmp(afile,"/") == 0)
136          afile[0] = '\0';
137        aflag++;
138        break;
139
140      case 'v':
141        verbosef++;
142        break;
143      case 'q':
144        verbosef = 0;
145        break;
146      case 'n':
147        if (strcmp(&argv[i][1],"nosrcrules") == 0)
148          { nosrcrules = TRUE;
149            break;
150          }         
151        if (strcmp(&argv[i][1],"nodstrules") == 0)
152          { nodstrules = TRUE;
153            break;
154          }         
155        if (strcmp(&argv[i][1],"n") == 0)
156          { nflag = 1;
157            break;
158          }         
159        /* fall through to default */
160      default:
161        usage(argv[0]);
162      }
163    }
164     
165  (void) umask(0);
166
167  if (srcdir[0] != '/')
168    { char *p = srcdir;
169      srcdir = (char *) malloc(MAXPATHLEN);
170      if ((srcdir = getcwd(srcdir,MAXPATHLEN )) == NULL)
171        { perror("getcwd() failed");
172          fprintf(stderr,"exiting\n");
173          exit(1);
174        }
175      if ((strlen(srcdir) + strlen("/") + strlen(p) + 1) > MAXPATHLEN)
176        { fprintf(stderr,"full pathname is too long, exiting");
177          exit(1);
178        }
179      (void) strcat(srcdir,"/");
180      (void) strcat(srcdir,p);
181    }
182
183  if ((dstdir[0] != '/') && (dstdir[0] != '\0'))
184    { char *p = dstdir;
185      dstdir = (char *) malloc(MAXPATHLEN);
186      if ((dstdir = getcwd(dstdir, MAXPATHLEN)) == NULL)
187        { perror("getcwd() failed");
188          fprintf(stderr,"exiting\n");
189          exit(1);
190        }
191      if ((strlen(dstdir) + strlen("/") + strlen(p) + 1) > MAXPATHLEN)
192        { fprintf(stderr,"full pathname is too long, exiting");
193          exit(1);
194        }
195
196      (void) strcat(dstdir,"/");
197      (void) strcat(dstdir,p);
198    }
199
200
201  lastrule = 0;
202
203  /* The hardwired rules (number zero and number one):
204            map * $
205            ignore *
206
207            */
208
209  {
210      static char *mapdests[] = { "$", (char *)0 };
211
212      lstrule.type = R_MAP;
213      lstrule.u.u_map.globexp    = "*";
214      lstrule.u.u_map.file_types = TYPE_ALL;
215      lstrule.u.u_map.dests      = mapdests;
216  }
217  newrule();
218  lstrule.type = R_ACTION;
219  lstrule.u.u_action.type        = ACTION_IGNORE;
220  lstrule.u.u_action.globexp     = "*";
221  lstrule.u.u_action.file_types  = TYPE_ALL;
222  lstrule.u.u_action.options     = 0;
223
224  exit(dodir(srcdir,dstdir,""));
225}
226
227struct src {
228  char *name;
229  char *pathname;
230  struct stat stat;
231  unsigned int type;
232  int map_ruleno;
233  bool chased;
234  struct src *next;
235};
236
237struct targ {
238  char *pathname;
239  struct src *sp;
240  int action_ruleno;
241  bool updated; /* for when */
242  struct targ *next;
243};
244
245int dodir(src,dst,part)
246     char *src;
247     char *dst;
248     char *part;
249{
250    struct src srclist,*sp,*spp,*sppp;
251    struct targ targlist,*tp,*tpp,*tppp;
252    int i,j;
253    void *getmemt;
254    bool sorted;
255    DIR *dirp;
256    struct dirent *dp;
257    char *testpath;
258    struct stat teststat;
259
260    if (verbosef)
261        printf("processing %s\n", dst);
262 
263    srclist.next = 0;
264    targlist.next = 0;
265
266#define getmem(amt) (((getmemt = malloc(amt)) == NULL)? panic("dodir: bad return from malloc"), (void *) NULL : getmemt)
267#define newsrc(ptr)  (ptr = (struct src *)  getmem(sizeof(struct src)),  ptr->next=srclist.next,  srclist.next=ptr)
268#define newtarg(ptr) (ptr = (struct targ *) getmem(sizeof(struct targ)), ptr->next=targlist.next, targlist.next=ptr)
269#define allsrcs(ptr)  for (ptr=srclist.next; ptr!=NULL; ptr=ptr->next)
270#define alltargs(ptr) for (ptr=targlist.next; ptr!=NULL; ptr=ptr->next)
271
272    getrules(src,dst);
273 
274    /* read in source directory */
275    if ((dirp = opendir(src)) == NULL) {
276        fprintf(stderr, "Can't read directory %s: %s\n", src, strerror(errno));
277        return 1;
278    }
279    while ((dp = readdir(dirp)) != NULL) {
280        if (!strcmp(dp->d_name,".") || !strcmp(dp->d_name,".."))
281            continue;
282        newsrc(sp);
283        sp->pathname = (char *)0;
284        sp->name = (char *) getmem(strlen(dp->d_name) + 1);
285        strcpy(sp->name, dp->d_name);
286    }
287    closedir(dirp);
288
289    /* XXX */
290    /* read in target directory */
291    if ((rflag & RFLAG_SCAN_TARGET) && (dirp = opendir(dst))) {
292        testpath = (char *) getmem(strlen(src) + 66);
293        while ((dp = readdir(dirp)) != NULL) {
294            if (!strcmp(dp->d_name,".") || !strcmp(dp->d_name,".."))
295                continue;
296            strcpy(testpath,src);
297            strcat(testpath,"/");
298            strcat(testpath,dp->d_name);
299            if (lstat(testpath, &teststat)) {
300                newsrc(sp);
301                sp->name  = (char *) getmem(strlen(dp->d_name) + 1);
302                strcpy(sp->name, dp->d_name);
303
304                sp->pathname =
305                    (char *) getmem( strlen(src) + strlen(sp->name) + 2 );
306                strcpy(sp->pathname, testpath);
307            }
308        }
309        closedir(dirp);
310    }
311    /* XXX */
312
313    /* process each src */
314    allsrcs(sp) {
315
316        /*
317         * If sp->pathname is already set, it must be a virtual file,
318         * which indicates that the file only exists in the target
319         * directory.  We still wish to get the stats for such files,
320         * but we want to mark it as a virtual file, so we frob with
321         * the sp->type field.
322         */
323        if (!sp->pathname) {
324            sp->pathname =
325                (char *) getmem( strlen(src) + strlen(sp->name) + 2 );
326            (void) strcpy(sp->pathname,src);
327            (void) strcat(sp->pathname,"/");
328            (void) strcat(sp->pathname,sp->name);
329            /* get information about file */
330            if (lstat(sp->pathname,&(sp->stat)) < 0) {
331#define perror(problem, whatnext) printf("%s: %s: %s. %s\n", sp->pathname, problem, strerror(errno), whatnext)
332                perror("lstat() failed", "ignoring");
333                sp->map_ruleno = 0;
334                continue;
335            }
336
337            sp->type = 0;
338
339        } else {
340            char path[MAXPATHLEN];
341
342            (void) strcpy(path, dst);
343            (void) strcat(path, "/");
344            (void) strcat(path, sp->name);
345
346            if (lstat(path, &(sp->stat)) < 0) {
347#define perror(problem, whatnext) printf("%s: %s: %s. %s\n", sp->pathname, problem, strerror(errno), whatnext)
348                perror("lstat() failed", "ignoring");
349                sp->map_ruleno = 0;
350                continue;
351            }
352             
353            sp->type = TYPE_V;
354        }
355   
356        {
357            int mode = sp->stat.st_mode & S_IFMT;
358            switch(mode) {
359#define X(a,b) case a: \
360                if (verbosef > 1) printf("Found %s, mode %7.7o\n",sp->pathname,mode); \
361                    sp->type |= b; break
362                        X(S_IFDIR,TYPE_D);
363                X(S_IFCHR,TYPE_C);
364                X(S_IFBLK,TYPE_B);
365                X(S_IFREG,TYPE_R);
366                X(S_IFLNK,TYPE_L);
367                X(S_IFSOCK,TYPE_S);
368#undef X
369            default:
370                printf("%s: unknown type of src file: %7.7o, ignoring.\n",
371                       sp->pathname, mode);
372                sp->map_ruleno = 0;
373                continue;
374            }
375        }
376   
377        if ((sp->type == TYPE_L) &&
378            (findrule(sp->pathname,R_CHASE,sp->type,sp->pathname) > 0)) {
379            sp->chased = TRUE;
380            if (stat(sp->pathname,&(sp->stat)) < 0) {
381                perror("stat() after chase failed", "ignoring.");
382#undef perror
383                sp->map_ruleno = 0;
384                continue;
385            }
386            {
387                int mode = sp->stat.st_mode & S_IFMT;
388                switch (mode) {
389#define X(a,b) case a: sp->type = b ; break
390                    X(S_IFDIR,TYPE_D);
391                    X(S_IFCHR,TYPE_C);
392                    X(S_IFBLK,TYPE_B);
393                    X(S_IFREG,TYPE_R);
394                    X(S_IFLNK,TYPE_L);
395                    X(S_IFSOCK,TYPE_S);
396#undef X
397                default:
398                    printf("%s: unknown type of chased file: %7.7o, ignoring.\n",
399                           sp->pathname, mode);
400                    sp->map_ruleno = 0;
401                    continue;
402                }
403            }
404        } else {
405            sp->chased = FALSE;
406        }
407 
408        sp->map_ruleno = findrule(sp->pathname,R_MAP,sp->type,sp->pathname);
409
410        for (i = 0; rules[sp->map_ruleno].u.u_map.dests[i] != 0; i++) {
411            char *r;
412            newtarg(tp);
413            r = destination_pathname(dst,
414                                     sp->name,
415                                     rules[sp->map_ruleno].u.u_map.dests[i]);
416            tp->pathname = (char *) getmem(strlen(r)+1);
417            strcpy(tp->pathname,r);
418            tp->sp = sp;
419            if (verbosef > 2) printf("new targ: %s (from %s)\n",
420                                     tp->pathname, sp->pathname);
421
422        }
423    }
424    alltargs(tp) {
425        tp->action_ruleno =
426            findrule(tp->pathname,R_ACTION,tp->sp->type,tp->sp->pathname);
427        tp->updated = FALSE;
428    }
429
430    alltargs(tp) {
431        bool exists = FALSE;
432        bool chased = FALSE;
433        struct stat targstat;
434        unsigned int targtype;
435        time_t dbdate;
436        if (lstat(tp->pathname,&targstat) == 0) {
437            int mode = targstat.st_mode & S_IFMT;
438       
439            exists = TRUE;     
440            switch (mode) {
441#define X(a,b) case a: targtype = b ; break
442                X(S_IFDIR,TYPE_D);
443                X(S_IFCHR,TYPE_C);
444                X(S_IFBLK,TYPE_B);
445                X(S_IFREG,TYPE_R);
446                X(S_IFLNK,TYPE_L);
447                X(S_IFSOCK,TYPE_S);
448#undef X
449            }
450            if ((targtype == TYPE_L) &&
451                option_on(tp->action_ruleno, 'l')) {
452                if (stat(tp->pathname,&targstat) != 0) {
453                    /*** the link exists but its target does not */
454                    /*** hopefully, we can make it exist ..... */
455                    chased = TRUE;
456                    exists = FALSE;
457                } else {
458                    chased = TRUE;
459                    switch (targstat.st_mode & S_IFMT) {
460#define X(a,b) case a: targtype = b ; break
461                        X(S_IFDIR,TYPE_D);
462                        X(S_IFCHR,TYPE_C);
463                        X(S_IFBLK,TYPE_B);
464                        X(S_IFREG,TYPE_R);
465                        X(S_IFLNK,TYPE_L);
466                        X(S_IFSOCK,TYPE_S);
467#undef X
468                    }
469                }
470            }
471        }
472
473#define srcstat (tp->sp->stat)
474        /*      targstat */
475
476        /* "switch.c" knows only about what is defined or mentioned between here and where it is included */
477
478#define typeofaction (rules[tp->action_ruleno].u.u_action.type)
479#define pflag   (option_on(tp->action_ruleno,'p'))
480#define fflag   (option_on(tp->action_ruleno,'f'))
481
482        /* modemask is not known to switch.c */
483#define modemask (pflag ? 07777 : 00777)
484
485#define srcname (tp->sp->pathname)
486#define srcdev  (int) (srcstat.st_rdev)
487#define srctype (tp->sp->type)
488#define srcuid  ((int) (srcstat.st_uid))
489
490#define targname (tp->pathname)
491#define targdev  (int) (targstat.st_rdev)
492#define targuid  ((int) (targstat.st_uid))
493#define targmode (int) (targstat.st_mode & modemask)
494#define tmark()  (tp->updated = TRUE)
495
496#define srcgid  ((gid_t) (srcstat.st_gid))
497#define targgid  ((gid_t) (targstat.st_gid))
498
499#if defined(_AIX) && defined(_IBMR2)
500#define srcmode (int)(srcstat.st_mode & ((srctype==TYPE_D) ? 07777 : modemask))
501#else
502#define srcmode (int)(srcstat.st_mode & modemask)
503#endif
504
505#define ERROR -1
506#define NODBDATE 0
507#define NODBDATE_P LOCAL                /* (forceupdatef? OUTOFDATE : LOCAL) */
508#define UPTODATE 1
509#define OUTOFDATE 2
510#define NEWERDATE 3
511#define LOCAL 4
512#define LOCAL_P LOCAL                   /* (forceupdatef? OUTOFDATE : LOCAL) */
513#define LOCALBUTOUTOFDATE 6
514#define LOCALBUTOUTOFDATE_P LOCAL       /* (forceupdatef? OUTOFDATE : LOCAL) */
515#define FIXMODE 6
516#define FIXOWNANDMODE 7
517
518#define filecheck3() \
519        ((srcstat.st_mtime > targstat.st_mtime) ? \
520         OUTOFDATE : \
521         ((srcstat.st_mtime < targstat.st_mtime) || \
522          (srcstat.st_size != targstat.st_size)) ? \
523         NEWERDATE : \
524         (pflag && ((srcuid != targuid) || (srcgid != targgid))) ? \
525         FIXOWNANDMODE : \
526         ((srcmode&modemask) != (targmode&modemask)) ? \
527         FIXMODE : \
528         UPTODATE)
529       
530#define dircheck() \
531        ((pflag && ((srcuid != targuid) || \
532                    ((srcgid >= 0) && (srcgid != targgid)))) \
533         ? FIXOWNANDMODE : \
534         (pflag && ((srcmode&modemask) != (targmode&modemask))) \
535         ? FIXMODE : UPTODATE)
536
537#define setdates() \
538        { time_t date = srcstat.st_mtime; \
539          struct timeval tv[2]; \
540          tv[0].tv_sec = tv[1].tv_sec = date; \
541          tv[0].tv_usec = tv[1].tv_usec = 0; \
542          (void) utimes(targname,tv); \
543          tmark(); \
544        }
545
546    if (verbosef > 2) {
547      printf("entering switch.c:  %s ----> %s\n", srcname, targname);
548      printf("              typeofaction   is %d\n", typeofaction);
549      printf("              pflag              is %d\n", pflag);
550      printf("              modemask       is %4.4o\n", modemask);
551      printf("              srcname            is %s\n", srcname);
552      printf("              srcmode            is %7.7o\n", srcstat.st_mode);
553      printf("              srcdev             is %4.4x\n", srcdev);
554      printf("              srctype            is %d\n", srctype);
555      printf("              srcuid             is %d\n", srcuid);
556      printf("              srcgid             is %d\n", srcgid);
557      printf("              targname       is %s\n", targname);
558      printf("              exists             is %d\n", exists);
559      if (exists) {
560        printf("              targmode   is %7.7o\n", targstat.st_mode);
561        printf("              targdev    is %4.4x\n", targdev);
562        printf("              targtype   is %d\n", targtype);
563        printf("              targuid    is %d\n", targuid);
564        printf("              targgid    is %d\n", targgid);
565      }
566      printf("\n");
567    }     
568
569#include "switch.c"
570  }
571 
572  /* do when rules */
573  alltargs(tp) {
574    if (tp->updated) {
575      void dowhenrule();
576      int whenruleno;
577      whenruleno = findrule(tp->pathname, R_WHEN, tp->sp->type, tp->sp->pathname);
578      if (whenruleno != -1)
579        dowhenrule(whenruleno, tp->pathname);
580    }
581  }
582
583    {
584        char * ptr = (char *)NULL;
585       
586        allsrcs(sp) {
587            free(ptr);
588            ptr = (char *) sp;
589            free(sp->pathname);
590            free(sp->name);
591        }
592        alltargs(tp) {
593            free(ptr);
594            ptr = (char *) tp;
595            free(tp->pathname);
596        }
597        free(ptr);
598    }
599   
600  poprules();
601  return 0;
602}
603
604char * destination_pathname(dstdir, sname, mapstring)
605     char *dstdir, *sname, *mapstring;
606{
607  static char buf[MAXPATHLEN];
608  register char *p,*bp,*sp;
609  register char *lastdot;
610  bp = buf;
611
612  if (verbosef>2) printf("destination_pathname(%s,%s,%s)\n",
613                         dstdir, sname, mapstring);
614
615  for (p = dstdir; *p != '\0'; p++) *bp++ = *p;
616
617  *bp++ = '/';
618
619  for (p = mapstring; *p != '\0'; p++) {
620    switch (*p) {
621    case '$':
622      lastdot = NULL;
623      for (sp = sname; *sp != '\0'; sp++)
624        if ((*bp++ = *sp) == '.')
625          lastdot = bp - 1;
626      p++;
627      switch(*p) {
628      default:
629        p--; continue;
630      case ':':
631        p++;
632        switch(*p) {
633        case '\0':
634          /***** maybe this should be an error */
635          p--; p--; continue;
636        case 'r':
637          if (lastdot)
638            bp = lastdot;
639          else
640            { ; /***** should be an error of some sort */ }
641          continue;
642        default:
643          /***** should be an error of some sort */
644          continue;
645        }
646      }
647      /*NOTREACHED*/
648    default:
649      *bp++ = *p;
650      continue;
651    }
652  }
653
654  *bp = '\0';
655 
656  if (verbosef>1) printf("destination_pathname returning %s\n",buf);
657
658  return buf;
659}
660
661
662void dowhenrule(ruleno, tname)
663     int ruleno;
664     char *tname;
665{
666    char *cp;
667    int pid;
668    char *shell, *arg1;
669    int pipefds[2];
670
671    if (rules[ruleno].type != R_WHEN)
672        panic("dowhenrule:  rule type is not R_WHEN");
673
674    switch(rules[ruleno].u.u_when.type) {
675    case WHEN_SH:
676        shell = "/bin/sh";
677        arg1  = "-s";
678        break;
679    case WHEN_CSH:
680        shell = "/bin/csh";
681        arg1  = "-fs";
682        break;
683    }
684   
685    if (pipe(pipefds) != 0)
686        epanic("dowhenrule:  pipe");
687
688    if (nflag == 0) {
689        pid = fork();
690   
691        if (pid == 0) {
692
693            cp = tname;
694            while (*cp != '\0') cp++;
695            while (*cp != '/') cp--;
696            *cp = '\0';
697            if (cp == tname)
698                chdir("/");
699            else
700                chdir(tname);
701            *cp = '/';
702
703            dup2(pipefds[0], 0);
704            execl(shell, shell, arg1, 0);
705            epanic("dowhenrule:  execl");
706        } else if (pid > 0) {
707            char **c;
708            FILE *f;
709
710            f = fdopen(pipefds[1],"w");
711            c = rules[ruleno].u.u_when.cmds;
712
713            while (*c) {
714                if (verbosef) printf("%c %s",
715                                     (rules[ruleno].u.u_when.type == WHEN_SH)
716                                     ? '$' : '%', *c);
717                fprintf(f,"%s\n", *(c++));
718            }
719            /* kludge killing the sh because I cannot figure out how to
720             * make it go away on end of file */
721            switch(rules[ruleno].u.u_when.type) {
722            case WHEN_SH:
723                fprintf(f,"kill $$\n");
724                break;
725            default:
726                break;
727            }
728            fclose(f);
729        } else if (pid < 0) {
730            epanic("dowhenrule: fork");
731        }
732    } else                              /* If noop */
733        {
734            char **c;
735            c = rules[ruleno].u.u_when.cmds;
736
737            while (*c) {
738                if (verbosef) printf("%c %s",
739                                     (rules[ruleno].u.u_when.type == WHEN_SH)? '$': '%',
740                                     *c);
741            }   
742        }
743}
744
Note: See TracBrowser for help on using the repository browser.