root/usr/src/cmd/filesync/main.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * module:
 *      main.c
 *
 * purpose:
 *      argument handling and top level dispatch
 *
 * contents:
 *      main            argument handling and main loop
 *      usage           (static) print out usage message
 *      confirm         prompt the user for a confirmation and get it
 *      nomem           fatal error handler for malloc failures
 *      findfiles       (static) locate our baseline and rules files
 *      cleanup         (static) unlock baseline and delete temp file
 *      check_access    (static) do we have adequate access to a file/directory
 *      whoami          (static) get uid/gid/umask
 */

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>

#include "filesync.h"
#include "database.h"
#include "messages.h"
#include "debug.h"

/*
 * local routines in this module:
 */
static errmask_t findfiles();           /* find rule and baseline files */
static void cleanup(int);               /* cleanup locks and temps      */
static errmask_t check_access(char *, int *); /* check access to file   */
static void whoami();                   /* gather information about me  */
static void usage(void);                /* general usage                */


/*
 * globals exported to the rest of the program
 */
bool_t  opt_mtime;      /* preserve modification times on propagations  */
bool_t  opt_notouch;    /* don't actually make any changes              */
bool_t  opt_quiet;      /* disable reconciliation command output        */
bool_t  opt_verbose;    /* enable analysis descriptions                 */
side_t  opt_force;      /* designated winner for conflicts              */
side_t  opt_oneway;     /* one way only propagation                     */
side_t  opt_onesided;   /* permit one-sided evaluation                  */
bool_t  opt_everything; /* everything must agree (modes/uid/gid)        */
bool_t  opt_yes;        /* pre-confirm massive deletions are OK         */
bool_t  opt_acls;       /* always scan for acls on all files            */
bool_t  opt_errors;     /* simulate errors on specified files           */
bool_t  opt_halt;       /* halt on propagation errors                   */
dbgmask_t opt_debug;    /* debug mask                                   */

uid_t   my_uid;         /* default UID for files I create               */
gid_t   my_gid;         /* default GID for files I create               */

static char *file_rules; /* name of rules file                          */
static char *file_base; /* name of baseline file                        */

static int new_baseline; /* are we creating a new baseline              */
static int new_rules;   /* are we creating a new rules file             */
static int my_umask;    /* default UMASK for files I create             */
static int lockfd;      /* file descriptor for locking baseline         */

static char *rlist[MAX_RLIST];
static int num_restrs = 0;

/*
 * routine:
 *      main
 *
 * purpose:
 *      argument processing and primary dispatch
 *
 * returns:
 *      error codes per filesync.1 (ERR_* in filesync.h)
 *
 * notes:
 *      read filesync.1 in order to understand the argument processing
 *
 *      most of the command line options just set some opt_ global
 *      variable that is later looked at by the code that actually
 *      implements the features.  Only file names are really processed
 *      in this routine.
 */
int
main(int argc, char **argv)
{       int i;
        int c;
        errmask_t errs = ERR_OK;
        int do_prune = 0;
        char *srcname = 0;
        char *dstname = 0;
        struct base *bp;

        /* keep the error messages simple       */
        argv[0] = "filesync";

        /* gather together all of the options   */
        while ((c = getopt(argc, argv, "AaehmnqvyD:E:r:s:d:f:o:")) != EOF)
                switch (c) {
                        case 'a':       /* always scan for acls */
                                opt_acls = TRUE;
                                break;
                        case 'e':       /* everything agrees    */
                                opt_everything = TRUE;
                                break;
                        case 'h':       /* halt on error        */
                                opt_halt = TRUE;
                                break;
                        case 'm':       /* preserve modtimes    */
                                opt_mtime = TRUE;
                                break;
                        case 'n':       /* notouch              */
                                opt_notouch = TRUE;
                                break;
                        case 'q':       /* quiet                */
                                opt_quiet = TRUE;
                                break;
                        case 'v':       /* verbose              */
                                opt_verbose = TRUE;
                                break;
                        case 'y':       /* yes                  */
                                opt_yes = TRUE;
                                break;
                        case 'D':       /* debug options        */
                                if (!isdigit(optarg[0])) {
                                        dbg_usage();
                                        exit(ERR_INVAL);
                                }
                                opt_debug |= strtol(optarg, (char **)NULL, 0);
                                break;

                        case 'E':       /* error simulation     */
                                if (dbg_set_error(optarg)) {
                                        err_usage();
                                        exit(ERR_INVAL);
                                }
                                opt_errors = TRUE;
                                break;

                        case 'f':       /* force conflict resolution    */
                                switch (optarg[0]) {
                                        case 's':
                                                opt_force = OPT_SRC;
                                                break;
                                        case 'd':
                                                opt_force = OPT_DST;
                                                break;
                                        case 'o':
                                                opt_force = OPT_OLD;
                                                break;
                                        case 'n':
                                                opt_force = OPT_NEW;
                                                break;
                                        default:
                                                fprintf(stderr,
                                                        gettext(ERR_badopt),
                                                        c, optarg);
                                                errs |= ERR_INVAL;
                                                break;
                                }
                                break;

                        case 'o':       /* one way propagation          */
                                switch (optarg[0]) {
                                        case 's':
                                                opt_oneway = OPT_SRC;
                                                break;
                                        case 'd':
                                                opt_oneway = OPT_DST;
                                                break;
                                        default:
                                                fprintf(stderr,
                                                        gettext(ERR_badopt),
                                                        c, optarg);
                                                errs |= ERR_INVAL;
                                                break;
                                }
                                break;

                        case 'r':       /* restricted reconciliation    */
                                if (num_restrs < MAX_RLIST)
                                        rlist[ num_restrs++ ] = optarg;
                                else {
                                        fprintf(stderr, gettext(ERR_tomany),
                                                MAX_RLIST);
                                        errs |= ERR_INVAL;
                                }
                                break;

                        case 's':
                                if ((srcname = qualify(optarg)) == 0)
                                        errs |= ERR_MISSING;
                                break;

                        case 'd':
                                if ((dstname = qualify(optarg)) == 0)
                                        errs |= ERR_MISSING;
                                break;

                        default:
                        case '?':
                                errs |= ERR_INVAL;
                                break;
                }

        if (opt_debug & DBG_MISC)
                fprintf(stderr, "MISC: DBG=%s\n", showflags(dbgmap, opt_debug));

        /* if we have file names, we need a source and destination */
        if (optind < argc) {
                if (srcname == 0) {
                        fprintf(stderr, gettext(ERR_nosrc));
                        errs |= ERR_INVAL;
                }
                if (dstname == 0) {
                        fprintf(stderr, gettext(ERR_nodst));
                        errs |= ERR_INVAL;
                }
        }

        /* check for simple usage errors        */
        if (errs & ERR_INVAL) {
                usage();
                exit(errs);
        }

        /* locate our baseline and rules files  */
        if (c = findfiles())
                exit(c);

        /* figure out file creation defaults    */
        whoami();

        /* read in our initial baseline         */
        if (!new_baseline && (c = read_baseline(file_base)))
                errs |= c;

        /* read in the rules file if we need or have rules      */
        if (optind >= argc && new_rules) {
                fprintf(stderr, ERR_nonames);
                errs |= ERR_INVAL;
        } else if (!new_rules)
                errs |= read_rules(file_rules);

        /* if anything has failed with our setup, go no further */
        if (errs) {
                cleanup(errs);
                exit(errs);
        }

        /*
         * figure out whether or not we are willing to do a one-sided
         * analysis (where we don't even look at the other side.  This
         * is an "I'm just curious what has changed" query, and we are
         * only willing to do it if:
         *      we aren't actually going to do anything
         *      we have a baseline we can compare against
         * otherwise, we are going to insist on being able to access
         * both the source and destination.
         */
        if (opt_notouch && !new_baseline)
                opt_onesided = opt_oneway;

        /*
         * there are two interested usage scenarios:
         *      file names specified
         *              create new rules for the specified files
         *              evaulate and reconcile only the specified files
         *      no file names specified
         *              use already existing rules
         *              consider restricting them to specified subdirs/files
         */
        if (optind < argc) {
                /* figure out what base pair we're working on   */
                bp = add_base(srcname, dstname);

                /* perverse default rules to avoid trouble      */
                if (new_rules) {
                        errs |= add_ignore(0, SUFX_RULES);
                        errs |= add_ignore(0, SUFX_BASE);
                }

                /* create include rules for each file/dir arg   */
                while (optind < argc)
                        errs |= add_include(bp, argv[ optind++ ]);

                /*
                 * evaluate the specified base on each side,
                 * being careful to limit evaulation to new rules
                 */
                errs |= evaluate(bp, OPT_SRC, TRUE);
                errs |= evaluate(bp, OPT_DST, TRUE);
        } else {
                /* note any possible evaluation restrictions    */
                for (i = 0; i < num_restrs; i++)
                        errs |= add_restr(rlist[i]);

                /*
                 * we can only prune the baseline file if we have done
                 * a complete (unrestricted) analysis.
                 */
                if (i == 0)
                        do_prune = 1;

                /* evaulate each base on each side              */
                for (bp = bases; bp; bp = bp->b_next) {
                        errs |= evaluate(bp, OPT_SRC, FALSE);
                        errs |= evaluate(bp, OPT_DST, FALSE);
                }
        }

        /* if anything serious happened, skip reconciliation    */
        if (errs & ERR_FATAL) {
                cleanup(errs);
                exit(errs);
        }

        /* analyze and deal with the differenecs                */
        errs |= analyze();

        /* see if there is any dead-wood in the baseline        */
        if (do_prune) {
                c = prune();

                if (c > 0 && opt_verbose)
                        fprintf(stdout, V_prunes, c);
        }

        /* print out a final summary                            */
        summary();

        /* update the rules and baseline files (if needed)      */
        (void) umask(my_umask);
        errs |= write_baseline(file_base);
        errs |= write_rules(file_rules);

        if (opt_debug & DBG_MISC)
                fprintf(stderr, "MISC: EXIT=%s\n", showflags(errmap, errs));

        /* just returning ERR_RESOLVABLE upsets some people     */
        if (errs == ERR_RESOLVABLE && !opt_notouch)
                errs = 0;

        /* all done     */
        cleanup(0);
        return (errs);
}


/*
 * routine:
 *      usage
 *
 * purpose:
 *      print out a usage message
 *
 * parameters:
 *      none
 *
 * returns:
 *      none
 *
 * note:
 *      the -D and -E switches are for development/test/support
 *      use only and do not show up in the general usage message.
 */
static void
usage(void)
{
        fprintf(stderr, "%s\t%s %s\n", gettext(ERR_usage), "filesync",
                                        gettext(USE_simple));
        fprintf(stderr, "\t%s %s\n", "filesync", gettext(USE_all));
        fprintf(stderr, "\t-a .......... %s\n", gettext(USE_a));
        fprintf(stderr, "\t-e .......... %s\n", gettext(USE_e));
        fprintf(stderr, "\t-h .......... %s\n", gettext(USE_h));
        fprintf(stderr, "\t-m .......... %s\n", gettext(USE_m));
        fprintf(stderr, "\t-n .......... %s\n", gettext(USE_n));
        fprintf(stderr, "\t-q .......... %s\n", gettext(USE_q));
        fprintf(stderr, "\t-v .......... %s\n", gettext(USE_v));
        fprintf(stderr, "\t-y .......... %s\n", gettext(USE_y));
        fprintf(stderr, "\t-s dir ...... %s\n", gettext(USE_s));
        fprintf(stderr, "\t-d dir ...... %s\n", gettext(USE_d));
        fprintf(stderr, "\t-r dir ...... %s\n", gettext(USE_r));
        fprintf(stderr, "\t-f [sdon].... %s\n", gettext(USE_f));
        fprintf(stderr, "\t-o src/dst... %s\n", gettext(USE_o));
}

/*
 * routine:
 *      confirm
 *
 * purpose:
 *      to confirm that the user is willing to do something dangerous
 *
 * parameters:
 *      warning message to be printed
 *
 * returns:
 *      void
 *
 * notes:
 *      if this is a "notouch" or if the user has pre-confirmed,
 *      we should not obtain the confirmation and just return that
 *      the user has confirmed.
 */
void
confirm(char *message)
{       FILE *ttyi, *ttyo;
        char ansbuf[ MAX_LINE ];

        /* if user pre-confirmed, we don't have to ask  */
        if (opt_yes || opt_notouch)
                return;

        ttyo = fopen("/dev/tty", "w");
        ttyi = fopen("/dev/tty", "r");
        if (ttyi == NULL || ttyo == NULL)
                exit(ERR_OTHER);

        /* explain the problem and prompt for confirmation      */
        fprintf(ttyo, message);
        fprintf(ttyo, gettext(WARN_proceed));

        /* if the user doesn't kill us, we can continue         */
        (void) fgets(ansbuf, sizeof (ansbuf), ttyi);

        /* close the files and return                           */
        (void) fclose(ttyi);
        (void) fclose(ttyo);
}

void
nomem(char *reason)
{
        fprintf(stderr, gettext(ERR_nomem), reason);
        exit(ERR_OTHER);
}

/*
 * routine:
 *      findfiles
 *
 * purpose:
 *      to locate our baseline and rules files
 *
 * parameters:
 *      none
 *
 * returns:
 *      error mask
 *      settings of file_base and file_rules
 *
 * side-effects:
 *      in order to keep multiple filesyncs from running in parallel
 *      we put an advisory lock on the baseline file.  If the baseline
 *      file does not exist we create one.  The unlocking (and deletion
 *      of extraneous baselines) is handled in cleanup.
 */
static errmask_t
findfiles(void)         /* find rule and baseline files */
{       char *s, *where;
        char namebuf[MAX_PATH];
        int ret;
        errmask_t errs = 0;

        /* figure out where the files should be located */
        s = getenv("FILESYNC");
        where = (s && *s) ? expand(s) : expand(DFLT_PRFX);

        /* see if we got a viable name          */
        if (where == 0) {
                fprintf(stderr, gettext(ERR_nofsync));
                return (ERR_FILES);
        }

        /* try to form the name of the rules file */
        strcpy(namebuf, where);
        strcat(namebuf, SUFX_RULES);
        s = strdup(namebuf);
        errs = check_access(namebuf, &new_rules);

        /* if we cannot find a proper rules file, look in the old place */
        if (new_rules && errs == 0) {
                strcpy(namebuf, where);
                strcat(namebuf, SUFX_OLD);
                file_rules = strdup(namebuf);
                errs = check_access(namebuf, &new_rules);

                /* if we couldn't find that either, go with new name    */
                if (new_rules && errs == 0)
                        file_rules = s;
        } else
                file_rules = s;

        /* try to form the name of the baseline file */
        strcpy(namebuf, where);
        strcat(namebuf, SUFX_BASE);
        file_base = strdup(namebuf);
        errs |= check_access(namebuf, &new_baseline);

        if (opt_debug & DBG_FILES) {
                fprintf(stderr, "FILE: %s rules file: %s\n",
                        new_rules ? "new" : "existing", file_rules);

                fprintf(stderr, "FILE: %s base file:  %s\n",
                        new_baseline ? "new" : "existing", file_base);
        }

        /*
         * in order to lock out other filesync programs we need some
         * file we can lock.  We do an advisory lock on the baseline
         * file.  If no baseline file exists, we create an empty one.
         */
        if (new_baseline)
                lockfd = creat(file_base, 0666);
        else
                lockfd = open(file_base, O_RDWR);

        if (lockfd < 0) {
                fprintf(stderr, new_baseline ? ERR_creat : ERR_open,
                        TXT_base, file_base);
                errs |= ERR_FILES;
        } else {
                ret = lockf(lockfd, F_TLOCK, 0L);
                if (ret < 0) {
                        fprintf(stderr, ERR_lock, TXT_base, file_base);
                        errs |= ERR_FILES;
                } else if (opt_debug & DBG_FILES)
                        fprintf(stderr, "FILE: locking baseline file %s\n",
                                file_base);
        }

        return (errs);
}

/*
 * routine:
 *      cleanup
 *
 * purpose:
 *      to clean up temporary files and locking prior to exit
 *
 * paremeters:
 *      error mask
 *
 * returns:
 *      void
 *
 * notes:
 *      if there are no errors, the baseline file is assumed to be good.
 *      Otherwise, if we created a temporary baseline file (just for
 *      locking) we will delete it.
 */
static void
cleanup(errmask_t errmask)
{
        /* unlock the baseline file     */
        if (opt_debug & DBG_FILES)
                fprintf(stderr, "FILE: unlock baseline file %s\n", file_base);
        (void) lockf(lockfd, F_ULOCK, 0);

        /* see if we need to delete a temporary copy    */
        if (errmask && new_baseline) {
                if (opt_debug & DBG_FILES)
                        fprintf(stderr, "FILE: unlink temp baseline file %s\n",
                                file_base);
                (void) unlink(file_base);
        }
}

/*
 * routine:
 *      check_access
 *
 * purpose:
 *      to determine whether or not we can access an existing file
 *      or create a new one
 *
 * parameters:
 *      name of file (in a clobberable buffer)
 *      pointer to new file flag
 *
 * returns:
 *      error mask
 *      setting of the new file flag
 *
 * note:
 *      it is kind of a kluge that this routine clobbers the name,
 *      but it is only called from one place, it needs a modified
 *      copy of the name, and the one caller doesn't mind.
 */
static errmask_t
check_access(char *name, int *newflag)
{       char *s;

        /* start out by asking for what we want         */
        if (access(name, R_OK|W_OK) == 0) {
                *newflag = 0;
                return (0);
        }

        /* if the problem is isn't non-existence, lose  */
        if (errno != ENOENT) {
                *newflag = 0;
                fprintf(stderr, gettext(ERR_rdwri), name);
                return (ERR_FILES);
        }

        /*
         * the file doesn't exist, so there is still hope if we can
         * write in the directory that should contain the file
         */
        *newflag = 1;

        /* truncate the file name to its containing directory */
        for (s = name; s[1]; s++);
        while (s > name && *s != '/')
                s--;
        if (s > name)
                *s = 0;
        else if (*s == '/')
                s[1] = 0;
        else
                name = ".";

        /* then see if we have write access to the directory    */
        if (access(name, W_OK) == 0)
                return (0);

        fprintf(stderr, gettext(ERR_dirwac), name);
        return (ERR_FILES);
}

/*
 * routine:
 *      whoami
 *
 * purpose:
 *      to figure out who I am and what the default modes/ownership
 *      is on files that I create.
 */
static void
whoami()
{
        my_uid = geteuid();
        my_gid = getegid();
        my_umask = umask(0);

        if (opt_debug & DBG_MISC)
                fprintf(stderr, "MISC: my_uid=%u, my_gid=%u, my_umask=%03o\n",
                        my_uid, my_gid, my_umask);
}