Skip to content
Permalink
249d2aff50
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
1510 lines (1385 sloc) 49.2 KB
#define _GNU_SOURCE
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <pwd.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include <assert.h>
#include <fnmatch.h>
#include <stdarg.h>
#include <sys/utsname.h>
#include <utime.h>
#include <sys/time.h>
#include <ctype.h>
#include <getopt.h>
// http://owww.molgen.mpg.de/~buczek/glib-doc/
#include <glib.h>
#define STEAL_POINTER(SRC) ({ void *tmp=*(SRC); *(SRC)=NULL; tmp; })
static const char *log_prefix;
static int noisy_abort;
static G_GNUC_PRINTF(1, 2) void warn(const char *restrict fmt, ...) {
if (log_prefix)
fprintf(stderr, "%s: ", log_prefix);
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
static G_NORETURN G_GNUC_PRINTF(1, 2) void die(const char *restrict fmt, ...) {
if (fmt) {
if (log_prefix)
fprintf(stderr, "%s: ", log_prefix);
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
if (noisy_abort)
fputs("cmirror aborted\n", stderr);
_exit(1);
}
static GRegex *compile_pattern(char *pattern) {
GError *error = NULL;
GRegex *regex = g_regex_new(pattern, 0, 0, &error);
if (!regex)
die("%s\n", error->message);
return regex;
}
static char **match_re(GRegex *regex, char *string) {
g_autoptr(GMatchInfo) match_info = NULL;
if (!g_regex_match(regex, string, 0, &match_info))
return NULL;
char **ret;
int count = g_match_info_get_match_count(match_info);
if (count == -1)
die("match_info_count == -1\n");
ret = g_new(char *, count+1);
for (int i=0; i<count; i++) {
ret[i] = g_match_info_fetch(match_info, i);
}
ret[count] = NULL;
return ret;
}
#define MATCH_RE(pattern, string) ({ \
static GRegex *regex; \
if (!regex) \
regex = compile_pattern(pattern); \
match_re(regex, string); \
})
static char *fn_escape(char *fn) {
GString *out = g_string_sized_new(80);
for (char c = *fn++ ; c ; c = *fn++) {
if (isgraph(c) && c != '\\')
g_string_append_c(out, c);
else
g_string_append_printf(out, "\\x%02x", (unsigned char)c);
}
return g_string_free(out, FALSE);
}
static gboolean fn_unescape_cb(const GMatchInfo *match_info, GString *result, gpointer user_data) {
(void)user_data;
g_autofree char *c = g_match_info_fetch (match_info, 1);
assert(c && *c);
g_string_append_printf(result, "%c", (int)strtol(c, NULL, 16));
return FALSE;
}
static char *fn_unescape(char *fn) {
static GRegex *regex = NULL;
if (!regex)
regex = compile_pattern("\\\\x([0-9a-f]{0,2})");
GError *error;
char *ret = g_regex_replace_eval(regex, fn, -1, 0, 0, fn_unescape_cb, NULL, &error);
if (!ret)
die("%s\n", error->message);
return ret;
}
static char st_mode_to_typechar(mode_t type, char *filename) {
if (type == S_IFREG)
return 'F';
else if (type == S_IFDIR)
return 'D';
else if (type == S_IFLNK)
return 'L';
else if (type == S_IFBLK)
return 'B';
else if (type == S_IFCHR)
return 'C';
else if (type == S_IFIFO)
return 'P';
else if (type == S_IFSOCK)
return 'S';
else
die("%s: unsupported file type %d\n", filename, type);
}
struct FileInfo {
char *name;
dev_t dev;
ino_t ino;
char type;
mode_t perm; // S_IMODE(st_mode)
nlink_t nlink;
uid_t uid;
gid_t gid;
dev_t rdev;
off_t size;
time_t mtime;
char *target;
};
typedef struct FileInfo FileInfo;
static struct FileInfo *fileinfo_lstat(char *path) {
struct stat sb;
int res = lstat(path, &sb);
if (res == -1) {
if (errno == ENOENT || errno == ENOTDIR)
return NULL;
die("%s: %m\n", path);
}
struct FileInfo *fi = g_malloc(sizeof *fi);
fi->name = g_strdup(path);
fi->dev = sb.st_dev;
fi->ino = sb.st_ino;
fi->type = st_mode_to_typechar(sb.st_mode & S_IFMT, path);
fi->perm = sb.st_mode & 07777;
fi->nlink = sb.st_nlink;
fi->uid = sb.st_uid;
fi->gid = sb.st_gid;
fi->rdev = sb.st_rdev;
fi->size = sb.st_size;
fi->mtime = sb.st_mtime;
if (fi->type == 'L') {
fi->target = g_malloc(sb.st_size + 1);
int res = readlink(path, fi->target, sb.st_size+1);
if (res == -1)
die("readlink %s: %m\n", path);
if (res > sb.st_size)
die("readlink %s: target string to long\n", path);
fi->target[res] = '\0';
} else {
fi->target = NULL;
}
return fi;
}
static void fileinfo_free(struct FileInfo *fi) {
g_free(fi->name);
g_free(fi->target);
g_free(fi);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FileInfo, fileinfo_free)
static char *fileinfo_export(struct FileInfo *fi) {
char *out;
g_autofree char *escaped_fn = fn_escape(fi->name);
g_autofree char *extra;
if (fi->type == 'F')
extra = g_strdup_printf("%ld", fi->size);
else if (fi->type == 'L')
extra = fn_escape(fi->target);
else if (fi->type == 'B' || fi->type == 'C')
extra = g_strdup_printf("%lu", fi->rdev);
else
extra = g_strdup("-");
out = g_strdup_printf(
"%c %s %d %d %d %s %ld",
fi->type,
escaped_fn,
fi->perm,
fi->uid,
fi->gid,
extra,
fi->mtime
);
return out;
}
/*
# index file format:
0 1 2 3 4 5 6 7
F path perm uid gid size mtime hardlink
D path perm uid gid - mtime -
L path perm uid gid target mtime hardlink
B path perm uid gid rdev mtime hardlink
C path perm uid gid rdev mtime hardlink
P path perm uid gid - mtime hardlink
S path perm uid gid - mtime hardlink
*/
// assume valid !
static struct FileInfo *fileinfo_new_import(GStrv vec) {
struct FileInfo *fi = g_malloc(sizeof *fi);
/* Make sure, we use the right variants of atoi, atol */
G_STATIC_ASSERT(sizeof(fi->size) == sizeof(long));
G_STATIC_ASSERT(sizeof(fi->perm) == sizeof(int));
G_STATIC_ASSERT(sizeof(fi->uid) == sizeof(int));
G_STATIC_ASSERT(sizeof(fi->gid) == sizeof(int));
G_STATIC_ASSERT(sizeof(fi->mtime) == sizeof(long));
fi->name = fn_unescape(vec[1]);
fi->dev = 0;
fi->ino = 0;
fi->type = vec[0][0];
fi->perm = atoi(vec[2]);
fi->nlink = 0;
fi->uid = atoi(vec[3]);
fi->gid = atoi(vec[4]);
fi->rdev = (fi->type == 'C' || fi->type == 'B') ? atoi(vec[5]) : 0;
fi->size = fi->type == 'F' ? atol(vec[5]) : 0;
fi->mtime = atol(vec[6]);
fi->target = fi->type == 'L' ? strdup(vec[5]) : NULL;
return fi;
}
/* options */
static int slave_mode;
static int local_slave;
static int slave_unprivileged;
static int fileop_noop;
static int quiet;
static int fileop_debug;
static int nodelete; // noop
static int delete;
static int debug;
static char *lockident;
static int safety;
static char *identity_file;
static int mkdir_slave;
static char **excepts;
static int reduce;
static int force_status;
static int bandwidth;
static int allowremotefs;
static char **ssh_opts;
static int cksum;
static int opt_nice;
static int unix_socket;
static char *unix_socket_name;
static int noatime;
static int ignore_permissions;
/* globals */
static GPtrArray *EXCEPTS;
static GHashTable *LOCAL_DEV;
static GHashTable *CLEAN;
static GSList *CLEAN_DIRS;
static char data_buffer[10240+2];
static void fileop_mknod(char *path, char type, dev_t rdev) {
if (fileop_debug)
warn("fileop: mknod %s %c %lu\n", path, type, rdev);
if (fileop_noop)
return;
mode_t mode;
if (type == 'P')
mode = S_IFIFO; // note: rdev ignored
else if (type == 'C')
mode = S_IFCHR;
else if (type == 'B')
mode = S_IFBLK;
else
die("%s: fileop_mknod() called for unexpected file type %c\n", path, type);
if (mknod(path, mode, rdev) == -1)
die("%s: %m\n", path);
}
static void fileop_chmod(mode_t perm, char *path) {
if (fileop_debug)
warn("fileop: chmod 0%03o %s\n", perm, path);
if (fileop_noop)
return;
if (chmod(path, perm) == -1)
die("%s: %m\n", path);
}
static void fileop_rm(char *path) {
if (fileop_debug)
warn("fileop: rm %s\n", path);
if (fileop_noop)
return;
if (unlink(path) == -1)
die("%s: %m\n", path);
}
static void execvp_checked(char **argv) {
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
if (errno == EACCES)
die("excec %s: no such file\n", argv[0]);
die("%m\n");
}
if (pid == -1)
die("fork %m\n");
int wstatus;
waitpid(pid, &wstatus, 0);
if (wstatus)
die(NULL);
}
static void fileop_rmdir_recurse(char *path) {
if (fileop_debug)
warn("fileop: rm -r %s\n", path);
if (fileop_noop)
return;
char *argv[] = { "rm", "-rf", path, NULL };
execvp_checked(argv);
}
static void fileop_mv(char *from, char*to) {
if (fileop_debug)
warn("fileop: mv %s %s\n", from, to);
if (fileop_noop)
return;
if (rename(from, to) == -1)
die("%s: %m\n", to);
}
static void fileop_mkdir(char *path) {
if (fileop_debug)
warn("fileop: mkdir %s\n", path);
if (fileop_noop)
return;
if (mkdir(path, 0) == -1)
die("%s: %m\n", path);
}
static void fileop_symlink(char *from, char *to) {
if (fileop_debug)
warn("fileop: ln -s %s %s\n", from, to);
if (fileop_noop)
return;
if (symlink(from, to) == -1)
die("%s: %m\n", to);
}
static void fileop_lchown(uid_t uid, gid_t gid, char *path) {
if (fileop_debug)
warn("fileop: lchown %d:%d %s\n", uid, gid, path);
if (fileop_noop)
return;
if (lchown(path, uid, gid) == -1)
die("%s: %m\n", path);
}
static void fileop_lmtime(time_t mtime, char *path) {
if (fileop_debug)
warn("fileop: lmtime %ld %s\n", mtime, path);
if (fileop_noop)
return;
struct timespec timespec[2] = { { 0, UTIME_OMIT } , { mtime, 0 } };
if (utimensat(AT_FDCWD, path, timespec, AT_SYMLINK_NOFOLLOW) == -1)
die("%s: %m\n", path);
}
/* Disable fileop_cp, because it is untested, dangerous and probably
* never needed in real life. Verify correctness before enabling */
#define DISABLE_FILEOP_CP (1)
static void fileop_cp(char *from, char *to) {
if (DISABLE_FILEOP_CP)
die("%s: fileop_cp disabled\n", to);
if (fileop_debug)
warn("fileop: cp -p %s %s\n", from, to);
if (fileop_noop)
return;
int fd_in = open(from, O_RDONLY|O_NOATIME|O_NOFOLLOW);
if (fd_in == -1)
die("%s: %m\n", from);
struct stat statbuf;
if (fstat(fd_in, &statbuf) == -1)
die("%s: %m\n", from);
int fd_out = open(to, O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, statbuf.st_mode & 07777);
if (fd_out == -1)
die("%s: %m\n", to);
ssize_t len = statbuf.st_size;
while (len>0) {
ssize_t done = copy_file_range(fd_in, NULL, fd_out, NULL, len, 0);
if (done == -1)
die("%s: %m\n", to);
len -= done;
}
close(fd_in);
close(fd_out);
}
static void fileop_ln_or_cp(char *from, char *to) {
if (fileop_debug)
warn("fileop: ln_or_cp %s %s\n", from, to);
if (fileop_noop)
return;
if (link(from, to) == -1) {
if (errno == EXDEV)
fileop_cp(from, to);
else
die("%s: %m\n", from);
}
}
static void check_perm(FileInfo *is, FileInfo *want) {
if (!slave_unprivileged && (is->uid != want->uid || is->gid != want->gid) ) {
if (!quiet)
warn("chown %d:%d %s\n", want->uid, want->gid, is->name);
fileop_lchown(want->uid, want->gid, is->name);
}
if (!ignore_permissions && want->perm != is->perm) {
if (!quiet)
warn("chmod %03o %s\n", want->perm, is->name);
fileop_chmod(want->perm, is->name);
}
}
static void make_dir(char *path, mode_t perm, uid_t uid, gid_t gid) {
if (!quiet)
warn("mkdir %s owner %d:%d mode 0%03o\n", path, uid, gid, perm);
fileop_mkdir(path);
if (!slave_unprivileged)
fileop_lchown(uid, gid, path);
fileop_chmod(perm, path);
}
static int is_excepted(char *filename) {
for (guint i=0 ; i<EXCEPTS->len ; i++) {
char *pattern = (char *)EXCEPTS->pdata[i];
int m = fnmatch(pattern, filename, FNM_PATHNAME|FNM_PERIOD);
if (m == 0)
return 1;
if (m != FNM_NOMATCH)
die("unexpected return value from fnmatch: %d\n", m);
}
return 0;
}
static void add_clean_dir(char *dir) {
GError *error;
GDir *d = g_dir_open(dir, 0, &error);
if (!d)
die("%s: %s\n", dir, error->message);
const char *entry;
while ( (entry = g_dir_read_name(d)) ) {
char *path = g_strdup_printf("%s/%s", dir, entry);
if (is_excepted(path))
free(path);
else
g_hash_table_insert(CLEAN, path, path);
}
g_dir_close(d);
}
static void out_of_the_way(FileInfo *fi) {
if (fi->type == 'D' && !fileop_noop) {
char *deleteme = g_strdup_printf("%s.deleteme", fi->name);
fileop_mv(fi->name, deleteme);
CLEAN_DIRS = g_slist_prepend(CLEAN_DIRS, deleteme);
} else {
fileop_rm(fi->name);
}
}
static int is_local_fs(char *fs) {
static GRegex *local_fs_regex;
if (!local_fs_regex)
local_fs_regex = compile_pattern("^(advfs|ufs|ext\\d+|reiserfs|xfs|fuseblk|vfat|efs|tmpfs|btrfs|ecryptfs)$");
return g_regex_match(local_fs_regex, fs, 0, 0);
}
// './bla/lall/troeoet/lalala.txt' -> './bla/lall/troeoet/pmirror.1234.tmp'
static char *tmpfilename(char *filename) {
g_autofree char *dirname = g_path_get_dirname(filename);
return(g_strdup_printf("%s/pmirror.%d.tmp", dirname, getpid()));
}
static ssize_t receive_record(FILE *in, char *buffer, size_t buflen) {
char b[2];
size_t l1 = fread(b, 2, 1, in);
if (!l1)
return 0;
unsigned int len = ntohs(*(uint16_t *)b);
if (len > buflen)
die("data overrun %u > %lu\n", len, buflen);
size_t l2 = fread(buffer, 1, len, in);
if (l2 != len)
die("data underrun %lu != %lu\n", l1, l2);
return len;
}
static void receive_file(FILE *in, char *filename, off_t expected_size, mode_t perm) {
int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, perm);
if (fd == -1)
die("%s: %m\n", filename);
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
posix_fadvise(fd, 0, 0, POSIX_FADV_NOREUSE);
while (1) {
ssize_t l1 = receive_record(in, data_buffer, sizeof(data_buffer));
if (!l1)
break;
ssize_t l2 = write(fd, data_buffer, l1);
if(l2 == -1)
die("%s: %m\n", filename);
if (l2 != l1)
die("%s: short write\n", filename);
expected_size -= l2;
}
fdatasync(fd);
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
close(fd);
if (expected_size > 0)
warn("%s:master sent less file data than expected\n", filename);
else if (expected_size < 0)
warn("%s:master sent more file data than expected\n", filename);
}
__attribute__ ((noreturn))
static void usage(char *argv0) {
fprintf(stderr,
"usage: %s [options] path [node:]path\n"
" %s --slave [--local-slave] [--socket-name path] [options] path\n"
"\n"
" options:\n"
" --noop dont change anything\n"
" --quiet don't say anything\n"
" --fileop_debug log every file operation\n"
" --delete delete files on client which are not on server\n"
" --debug log some debugging info\n"
" --lock IDENT get exclusive lock on /var/lock/IDENT or die (not for slave)\n"
" --safety check existance of /path/.PMIRROR_ENABLE\n"
" --identity_file FILE use this identity file for ssh\n"
" --mkdir create directory on target\n"
" --exclude path exclude path ( use pattern \"./name1/name\" only) , max be specified multiple times\n"
" --reduce do not create new data on mirror (but do remove obsolete data with --delete) ...\n"
" --force_status force creation of .PMIRROR_STATUS on target\n"
" --bandwidth bandwidth\n"
" --allowremotefs allow mirror over remote fs\n"
" --ssh-opt OPT additional ssh option\n"
" --cksum compare existing files with CRC checksum\n"
" --unprivileged do not attempt to set file ownership, even if root\n"
" --nice EXPERIMENTAL nice\n"
" --unix-socket EXPERIMENTAL establish data channel over ssh via AF unix sockets\n"
" --socket-name PATH EXPERIMENTAL use PATH as name for AF unix sockets\n"
" --noatime don't touch atime on sender\n"
" --ignore-permissions do not attempt to change file permissions\n",
argv0, argv0);
_exit(1);
}
static struct option options[] = {
{ "slave", 0, &slave_mode, 1 },
{ "local-slave", 0, &local_slave, 1 },
{ "unprivileged", 0, &slave_unprivileged, 1 },
{ "noop", 0, &fileop_noop, 1 },
{ "quiet", 0, &quiet, 1 },
{ "fileop_debug", 0, &fileop_debug, 1 },
{ "nodelete", 0, &nodelete, 1 }, // noop
{ "delete", 0, &delete, 1 },
{ "debug", 0, &debug, 1 },
{ "lock", 1, NULL, 100 },
{ "safety", 0, &safety, 1 },
{ "identity_file", 1, NULL, 101 },
{ "mkdir", 0, &mkdir_slave, 1 },
{ "exclude", 1, NULL, 102 },
{ "reduce", 0, &reduce, 1 },
{ "force_status", 0, &force_status, 1 },
{ "bandwidth", 0, &bandwidth, 1 },
{ "allowremotefs", 0, &allowremotefs, 1 },
{ "ssh-opt", 1, NULL, 103 },
{ "cksum", 0, &cksum, 1 },
{ "nice", 0, &opt_nice, 1 },
{ "unix-socket", 0, &unix_socket, 1 },
{ "socket-name", 1, NULL, 104 },
{ "noatime", 0, &noatime, 1 },
{ "ignore-permissions", 0, &ignore_permissions, 1 },
{ NULL }
};
struct DirMtime {
char *path;
time_t mtime;
};
typedef struct DirMtime DirMtime;
static void dir_mtime_free(DirMtime *dm) {
g_free(dm->path);
g_free(dm);
}
static DirMtime *dir_mtime_new(char *path, time_t mtime) {
DirMtime *dm = g_new(DirMtime,1);
dm->path = g_strdup(path);
dm->mtime = mtime;
return dm;
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(DirMtime, dir_mtime_free)
static void slave(char *slave_path) {
FILE *in = stdin;
FILE *out = stdout;
log_prefix = g_get_host_name();
EXCEPTS = g_ptr_array_new();
CLEAN = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
g_autolist(DirMtime) DIR_MTIME_QUEUE = NULL;
umask(0);
if (mkdir_slave && !fileop_noop) {
char *argv[] = { "mkdir", "-p", slave_path, NULL };
gint wait_status;
GError *error = NULL;
g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_LEAVE_DESCRIPTORS_OPEN, \
NULL, NULL, NULL, NULL, &wait_status, &error);
if (error)
die("mkdir: %s\n", error->message);
if (wait_status)
die(NULL);
}
int res = chdir(slave_path);
if (res == -1)
die("%s: %m\n", slave_path);
if (!local_slave) {
int s_listen;
int s_data;
if (unix_socket_name) {
unlink(unix_socket_name);
s_listen = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un sockaddr;
sockaddr.sun_family = AF_UNIX;
strncpy(sockaddr.sun_path, unix_socket_name, sizeof(sockaddr.sun_path));
sockaddr.sun_path[sizeof(sockaddr.sun_path)-1] = '\0';
int res = bind(s_listen, &sockaddr, sizeof(sockaddr));
if (res == -1)
die("%s: %m\n", unix_socket_name);
listen(s_listen, 1);
fprintf(out, "LISTEN %s\n", unix_socket_name);
} else {
struct sockaddr_in sockaddr;
socklen_t sockaddr_len = sizeof(sockaddr);
s_listen = socket(AF_INET, SOCK_STREAM, 0);
listen(s_listen, 1);
int res = getsockname(s_listen, &sockaddr, &sockaddr_len);
if (res == -1)
die("listen: %m\n");
fprintf(out, "LISTEN %d\n", ntohs(sockaddr.sin_port));
}
fflush(out);
s_data = accept(s_listen, NULL, NULL);
if (s_data == -1)
die("accept: %m\n");
in = fdopen(s_data, "r");
out = fdopen(s_data, "w");
}
int reduce_saved_fileop_noop = fileop_noop;
if (reduce)
fileop_noop = 1;
int crc_missmatch = 0;
while(1) {
fflush(out);
static char *line = NULL;
static size_t line_buflen = 0;
ssize_t len = getline(&line, &line_buflen, in);
if (len == -1) {
if (ferror(in))
die("master disconnected: %m\n");
else
die("master disconnected\n");
}
// warn("XXX %s", line);
if (line[0] == '%')
break;
g_strchomp(line);
g_auto(GStrv) word = g_strsplit_set(line, " ", -1);
int words = g_strv_length(word);
if (words == 2 && strcmp(word[0], "!") == 0) {
g_ptr_array_add (EXCEPTS, strdup(word[1]));
continue;
}
if (words != 9)
die("wrong number of words in index record from master: %s\n", line);
g_autoptr(FileInfo) fi_want = fileinfo_new_import(word);
char *filename = fi_want->name;
if (is_excepted(filename)) {
if (fi_want->type == 'F') {
fputs("CONTINUE\n", out);
continue;
}
}
g_hash_table_remove(CLEAN, filename);
g_autoptr(FileInfo) fi_is = fileinfo_lstat(filename);
if (fi_want->type == 'D') {
if (fi_is && fi_is->type == 'D') {
check_perm(fi_is, fi_want);
if (delete)
add_clean_dir(filename);
} else {
if (fi_is)
fileop_rm(filename);
make_dir(filename, fi_want->perm, fi_want->uid, fi_want->gid);
}
DIR_MTIME_QUEUE = g_list_prepend(DIR_MTIME_QUEUE, dir_mtime_new(filename, fi_want->mtime));
} else {
if (strcmp(word[7], "-")) {
g_autofree char *hardlink = fn_unescape(word[7]);
g_autoptr(FileInfo) fi_src = fileinfo_lstat(hardlink);
if (!fi_src) {
// source removed, i/o error, --reduce or logic error. We could
// request the file here, but the above cases don't seem to
// ustify this.
warn("%s: hardlink source %s: %m\n", filename, hardlink);
} else if (!fi_is || fi_is->dev != fi_src->dev || fi_is->ino != fi_src->ino) {
if (fi_is)
out_of_the_way(fi_is);
if (!quiet)
warn("ln %s %s\n", hardlink, filename);
fileop_ln_or_cp(hardlink, filename);
}
if (fi_want->type == 'F')
fputs("CONTINUE\n", out);
} else if (fi_want->type == 'F') {
if (!fi_is || fi_is->type != 'F' || fi_is->size != fi_want->size || fi_is->mtime != fi_want->mtime) {
if (!fileop_noop) {
g_autofree char *tmpfile = tmpfilename(filename);
mode_t tmp_perm = slave_unprivileged ? fi_want->perm : 0;
if (slave_unprivileged && g_file_test(tmpfile, G_FILE_TEST_EXISTS)) {
if (unlink(tmpfile) == -1)
die("%s: %m\n", tmpfile);
}
if (fi_want->size) {
fputs("SEND\n", out);
fflush(out);
if (fileop_debug)
warn("fileop: receiving %s for %s\n", tmpfile, filename);
receive_file(in, tmpfile, fi_want->size, tmp_perm);
} else {
fputs("CONTINUE\n", out);
if (!quiet)
warn("creating empty %s\n", filename);
if (fileop_debug)
warn("fileop: creating empty %s for %s\n", tmpfile, filename);
int fd = open(tmpfile, O_CREAT|O_WRONLY|O_TRUNC|O_EXCL, tmp_perm);
if (fd == -1)
die("%s: %m\n", tmpfile);
close(fd);
}
if (!slave_unprivileged) {
fileop_lchown(fi_want->uid, fi_want->gid, tmpfile);
fileop_chmod(fi_want->perm, tmpfile);
}
if (fi_is && fi_is->type == 'D')
out_of_the_way(fi_is);
fileop_mv(tmpfile, filename);
struct utimbuf utimbuf = { fi_want->mtime, fi_want->mtime };
utime(filename, &utimbuf);
} else {
if (!quiet)
warn("mirror %s\n", filename);
fputs("CONTINUE\n", out);
}
} else {
if (!reduce) {
if (cksum) {
(void)crc_missmatch;
die("chksum not implemented\n");
}
check_perm(fi_is, fi_want);
}
fputs("CONTINUE\n", out);
}
} else if (fi_want->type== 'L') {
if (!fi_is || fi_is->type != 'L' || strcmp(fi_is->target, fi_want->target)) {
if (fi_is)
out_of_the_way(fi_is);
if (!quiet)
warn("ln -s %s %s\n", fi_want->target, filename);
fileop_symlink(fi_want->target, filename);
if (!slave_unprivileged)
fileop_lchown(fi_want->uid, fi_want->gid, filename);
fileop_lmtime(fi_want->mtime, filename);
} else {
if (!slave_unprivileged && (fi_is->uid != fi_want->uid || fi_is->gid != fi_want->gid)) {
if (!quiet)
warn("lchown %d:%d %s\n", fi_want->uid, fi_want->gid, filename);
fileop_lchown(fi_want->uid, fi_want->gid, filename);
}
if (fi_is->mtime != fi_want->mtime) {
if (!quiet)
warn("set mtime of %s\n", filename);
fileop_lmtime(fi_want->mtime, filename);
}
}
} else if (fi_want->type== 'P') {
if (!fi_is || fi_is->type != 'P') {
if (fi_is)
out_of_the_way(fi_is);
if (!quiet)
warn("mknod %s P\n", filename);
fileop_mknod(filename, fi_want->type, 0);
if (!slave_unprivileged)
fileop_lchown(fi_want->uid, fi_want->gid, filename);
fileop_chmod(fi_want->perm, filename);
}
} else if (fi_want->type== 'S') {
warn("%s: is a socket(ignored)\n", filename);
} else if (fi_want->type == 'C' || fi_want->type == 'B') {
if (!fi_is || fi_is->type != fi_want->type || fi_is->rdev != fi_want->rdev) {
if (fi_is)
out_of_the_way(fi_is);
if (!quiet)
warn("mknod %s %c\n", filename, fi_want->type);
fileop_mknod(filename, fi_want->type, fi_want->rdev);
if (!slave_unprivileged)
fileop_lchown(fi_want->uid, fi_want->gid, filename);
fileop_chmod(fi_want->perm, filename);
} else {
check_perm(fi_is, fi_want);
}
} else {
die("%s: type %c not yet implemented\n", filename, fi_want->type);
}
}
}
if (reduce)
fileop_noop = reduce_saved_fileop_noop;
if (delete) {
if (debug)
warn("cleanup\n");
GHashTableIter iter;
g_hash_table_iter_init (&iter, CLEAN);
char *path;
while (g_hash_table_iter_next (&iter, (gpointer *)&path, NULL)) {
struct stat statbuf;
int res = lstat(path, &statbuf);
if (res != 0)
continue;
if (S_ISDIR(statbuf.st_mode)) {
if (!quiet)
warn("rm -r %s\n", path);
fileop_rmdir_recurse(path);
} else {
if (!quiet)
warn("rm %s\n", path);
fileop_rm(path);
}
}
}
if (reduce)
return;
GSList *d = CLEAN_DIRS;
while (d) {
char *path = (char *)d->data;
if (!quiet)
warn("rm -r %s\n", path);
fileop_rmdir_recurse(path);
d = d->next;
}
DIR_MTIME_QUEUE = g_list_reverse(DIR_MTIME_QUEUE);
for (GList *e = DIR_MTIME_QUEUE ; e != NULL ; e = e->next ) {
DirMtime *mt = e->data;
struct stat statbuf;
if (lstat(mt->path, &statbuf) == -1) {
if (!fileop_noop)
warn("%s: %m\n", mt->path);
continue;
}
if (!S_ISDIR(statbuf.st_mode))
continue;
if (statbuf.st_mtime == mt->mtime)
continue;
if (!quiet)
warn("fix directory mtime of %s -> %ld\n", mt->path, mt->mtime);
if (!fileop_noop) {
struct timespec timespec[2] = { { 0, UTIME_OMIT } , { mt->mtime, 0 } };
if (utimensat(AT_FDCWD, mt->path, timespec, AT_SYMLINK_NOFOLLOW) == -1)
warn("%s: %m\n", mt->path);
}
}
if (crc_missmatch)
die("completed with %d crc errors\n", crc_missmatch);
if (!fileop_noop && (force_status || g_file_test(".PMIRROR_STATUS", G_FILE_TEST_EXISTS))) {
time_t now = time(NULL);
char *s = ctime(&now); // \n at end
FILE *sfile = fopen(".PMIRROR_STATUS", "w");
if (!sfile)
die("%s: %m\n", ".PMIRROR_STATUS");
fprintf(sfile,"OK %ld %s", now, s);
fclose(sfile);
}
}
static void cache_local_fs(void) {
char *argv[] = {"mount", NULL };
gint wait_status;
GError *error = NULL;
int pipefd;
FILE *file;
char *line = NULL;
size_t linelen = 0;
GPid pid;
if (!g_spawn_async_with_pipes(NULL, argv, NULL, \
G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_LEAVE_DESCRIPTORS_OPEN, \
NULL, NULL, &pid, NULL, &pipefd, NULL, &error)) {
die("%s\n", error->message);
}
file = fdopen(pipefd, "r");
while(getline(&line, &linelen, file) > 0) {
char **m = MATCH_RE("(.+) on (/.*) type (\\S+)", line);
if (m) {
// char *fs = m[1];
char *path = m[2];
char *type = m[3];
if (is_local_fs(type)) {
struct stat statbuf;
int res = lstat(path, &statbuf);
if (res == -1) {
warn("%s: %m\n", path);
} else {
gint64 key=(gint64)statbuf.st_dev;
if (!g_hash_table_contains(LOCAL_DEV, &key)) {
gint64 *p=g_memdup2(&key, sizeof(key));
g_hash_table_insert(LOCAL_DEV, p, NULL);
}
}
}
g_strfreev(m);
}
}
free(line);
fclose(file);
waitpid(pid, &wait_status, 0);
g_spawn_close_pid(pid);
if (!g_spawn_check_wait_status(wait_status, &error)) {
die("mount: %s\n", error->message);
}
}
struct DevInode {
dev_t dev;
ino_t ino;
};
static guint di_hash(gconstpointer key) {
const struct DevInode *di = key;
gint i = (di->dev << 4) + di->ino;
return g_int_hash(&i);
}
static gboolean di_equal (gconstpointer a,gconstpointer b) {
const struct DevInode *di_a = a;
const struct DevInode *di_b = b;
return (di_a->dev == di_b->dev && di_a->ino == di_b->ino);
}
static GHashTable *hardlink_hash_new() {
return g_hash_table_new_full(
di_hash,
di_equal,
g_free,
g_free
);
};
static void send_file(FILE *out, struct FileInfo *fi) {
char *filename = fi->name;
off_t size = fi->size;
int fd = open(filename, O_RDONLY | (noatime ? O_NOATIME : 0));
if (fd == -1) {
if (errno == ENOENT) {
warn("%s: file has been removed on master after client requested it. aborting transfer\n", filename);
uint16_t nlen = 0;
if (fwrite(&nlen, 2, 1, out) != 1)
die("data write: %m\n");
close(fd);
return;
} else {
die("%s: %m\n", filename);
}
}
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
posix_fadvise(fd, 0, 0, POSIX_FADV_NOREUSE);
g_autoptr(GTimer) timer = NULL;
static double bw_column = 0;
if (bandwidth && !quiet) {
int oc = 8 + strlen(filename);
if (oc > bw_column)
bw_column = oc + 1.0;
bw_column -= 0.02;
g_autofree char *size_str = g_format_size(size);
fprintf(stderr, " %*s ",(int)(bw_column - oc) + 7, size_str);
timer = g_timer_new();
}
while (1) {
ssize_t len = read(fd, data_buffer+2, 10240);
if (len == -1)
die("%s: %m\n", filename);
*(uint16_t *)data_buffer = htons(len);
if (fwrite(data_buffer, len+2, 1, out) != 1)
die("data write: %m\n");
if (len == 0)
break;
}
if (bandwidth && !quiet) {
gdouble delta = g_timer_elapsed(timer, NULL) + 0.00001;
g_autofree char *speed_str = g_format_size((double)size/delta);
fprintf(stderr, " in %5.2fs %7s/s", delta, speed_str);
}
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
close(fd);
}
static gint cmp_g_array_str_rev(gconstpointer a, gconstpointer b) {
return 0-strcmp( *(const char **)a, *(const char **)b);
}
static void master(char *master_path, char *target) {
g_autofree char *slave;
g_autofree char *slave_path;
g_autofree char *slave_user;
if (!quiet)
noisy_abort = 1;
EXCEPTS = g_ptr_array_new();
g_ptr_array_add (EXCEPTS, "./quota.group");
g_ptr_array_add (EXCEPTS, "./quota.user");
g_ptr_array_add (EXCEPTS, "./.tags");
g_ptr_array_add (EXCEPTS, "./.PMIRROR_ENABLED");
g_ptr_array_add (EXCEPTS, "./.PMIRROR_STATUS");
if (excepts) {
for (char **p = excepts; *p; p++) {
g_ptr_array_add (EXCEPTS, *p);
}
}
LOCAL_DEV = g_hash_table_new_full(g_int64_hash, g_int64_equal, g_free, NULL);
{
g_auto(GStrv) match1 = MATCH_RE("^([^:]*):(.+)$", target); // system:/path
if (match1) {
slave_path = STEAL_POINTER(&(match1[2]));
g_auto(GStrv) match2 = MATCH_RE("^([^@]+)@(.+)$", match1[1]); // user@system
if (match2) {
slave_user = STEAL_POINTER(&match2[1]);
slave = STEAL_POINTER(&match2[2]);
} else {
slave_user = g_strdup("root");
slave = STEAL_POINTER(&match1[1]);
}
} else {
slave = g_strdup("");
slave_user = g_strdup("root");
slave_path = g_strdup(target);
}
}
if (unix_socket) {
if (unix_socket_name) {
char *p=unix_socket_name;
unix_socket_name = g_strdup(p);
} else {
char *tmpdir = getenv("TMPDIR");
if (!tmpdir)
tmpdir = "/tmp";
struct passwd *passwd=getpwuid(getuid());
if (!unix_socket_name)
unix_socket_name = g_strdup_printf("%s/pmirror_setup_%s_%05d", tmpdir, passwd->pw_name, getpid());
}
}
if (chdir(master_path) != 0)
die("%s: %m\n", master_path);
cache_local_fs();
if (safety) {
g_autofree char *safety_file = g_strdup_printf("%s/.PMIRROR_ENABLED", master_path);
if (!g_file_test(safety_file, G_FILE_TEST_EXISTS))
die("safety file %s not found. Terminating\n", safety_file);
}
if (lockident) {
g_autofree char *lockfilename = g_strdup_printf("/var/lock/pmirror.%s.lock", lockident);
int fd = open(lockfilename, O_WRONLY | O_CREAT, 0777);
if (fd == -1)
die("%s: %m\n", lockfilename);
int res = flock(fd, LOCK_EX | LOCK_NB);
if (res == -1) {
if (errno == 11 || errno == 35)
die("mirror %s already running\n", lockident);
else
die("%s: %m\n", lockfilename);
}
}
if (!allowremotefs) {
struct stat statbuf;
int res = lstat(".", &statbuf);
if (res == -1)
die ("%s: %m\n", master_path);
gint64 key=(gint64)statbuf.st_dev;
if (!g_hash_table_contains(LOCAL_DEV, &key))
die ("%s: remote filesystem\n", master_path);
}
int master_to_slave[2];
int slave_to_master[2];
int res;
res = pipe2(master_to_slave, 0);
if (res < 0)
die("pipe: %m\n");
res = pipe2(slave_to_master, 0);
if (res < 0)
die("pipe: %m\n");
pid_t pid = fork();
if (pid == 0) {
close(master_to_slave[1]);
close(slave_to_master[0]);
dup2(master_to_slave[0], 0);
dup2(slave_to_master[1], 1);
close(master_to_slave[0]);
close(slave_to_master[1]);
GPtrArray *args = g_ptr_array_sized_new(16);
if (slave && *slave) {
g_ptr_array_add(args, "ssh");
if (identity_file) {
g_ptr_array_add(args, "-i");
g_ptr_array_add(args, identity_file);
}
g_ptr_array_add(args, "-x");
g_ptr_array_add(args, "-l");
g_ptr_array_add(args, slave_user);
if (unix_socket_name) {
g_ptr_array_add(args, "-L");
char *s = g_strdup_printf("%s:%s", unix_socket_name, unix_socket_name);
g_ptr_array_add(args, s);
}
if (ssh_opts)
for (char **p = ssh_opts ; *p ; p++)
g_ptr_array_add(args, *p);
g_ptr_array_add(args, slave);
g_ptr_array_add(args, "/usr/bin/cmirror");
g_ptr_array_add(args, "--slave");
} else {
g_ptr_array_add(args, "/usr/bin/cmirror");
g_ptr_array_add(args, "--slave");
g_ptr_array_add(args, "--local-slave");
}
if (quiet) g_ptr_array_add(args, "--quiet");
if (fileop_noop) g_ptr_array_add(args, "--noop");
if (fileop_debug) g_ptr_array_add(args, "--fileop_debug");
if (debug) g_ptr_array_add(args, "--debug");
if (delete) g_ptr_array_add(args, "--delete");
if (mkdir_slave) g_ptr_array_add(args, "--mkdir");
if (reduce) g_ptr_array_add(args, "--reduce");
if (force_status) g_ptr_array_add(args, "--force_status");
if (bandwidth) g_ptr_array_add(args, "--bandwidth");
if (allowremotefs) g_ptr_array_add(args, "--allowremotefs");
if (cksum) g_ptr_array_add(args, "--cksum");
if (opt_nice) g_ptr_array_add(args, "--nice");
if (unix_socket_name) {
g_ptr_array_add(args, "--socket-name");
g_ptr_array_add(args, unix_socket_name);
}
if (slave_unprivileged || strcmp(slave_user, "root"))
g_ptr_array_add(args, "--unprivileged");
if (ignore_permissions)
g_ptr_array_add(args, "--ignore-permissions");
g_ptr_array_add(args, slave_path);
g_ptr_array_add(args, NULL);
if (debug) {
warn("executing ");
for (char **p=(char **)args->pdata; *p; p++)
warn("%s ", *p);
warn("\n");
}
if (unix_socket_name) {
struct stat statbuf;
int res = lstat(unix_socket_name, &statbuf);
if (res == 0 && S_ISSOCK(statbuf.st_mode))
unlink(unix_socket_name);
}
unsetenv("SSH_ORIGINAL_COMMAND");
execvp(args->pdata[0], (char **)args->pdata);
die("exec %s: %m\n", (char *)args->pdata[0]);
}
else if (pid == -1)
die("fork: %m");
close(master_to_slave[0]);
close(slave_to_master[1]);
FILE *in = fdopen(slave_to_master[0], "r");
FILE *out = fdopen(master_to_slave[1], "w");
if (slave && *slave) {
int s_data;
char *line = NULL;
size_t linebuf_len = 0;
errno = 0;
ssize_t len = getline(&line, &linebuf_len, in);
if (len == -1) {
if (errno)
die ("client disconnected: %m\n");
else
die ("client disconnected\n");
}
if (unix_socket_name) {
char **match = MATCH_RE("^LISTEN (\\S+)$", line);
if (!match)
die("protocol error; expected 'LISTEN path' got '%s'\n", line);
if (debug)
warn("connecting to slave port via %s\n", unix_socket_name);
s_data = socket(AF_UNIX, SOCK_STREAM, 0);
if (s_data == -1)
die("socket: %m\n");
struct sockaddr_un sockaddr;
sockaddr.sun_family = AF_UNIX;
strncpy(sockaddr.sun_path, unix_socket_name, sizeof(sockaddr.sun_path));
sockaddr.sun_path[sizeof(sockaddr.sun_path)-1] = '\0';
int res = connect(s_data, &sockaddr, sizeof(sockaddr));
if (res == -1)
die ("connect to slave port: %m\n");
unlink(unix_socket_name);
g_strfreev(match);
} else {
char **match = MATCH_RE("^LISTEN (\\d+)$", line);
if (!match)
die("protocol error; expected 'LISTEN port' got '%s'\n", line);
in_port_t port = strtoul(match[1], NULL, 10);
if (debug) {
warn("connecting to slave port %d\n", port);
}
s_data = socket(AF_INET, SOCK_STREAM, 0);
if (s_data == -1)
die("socket: %m\n");
struct hostent *hostent = gethostbyname(slave);
if (!hostent)
die("%s; %m", slave);
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(port);
sockaddr.sin_addr.s_addr = *(uint32_t *)hostent->h_addr;
int res = connect(s_data, &sockaddr, sizeof(sockaddr));
if (res == -1)
die ("connect to slave port: %m\n");
g_strfreev(match);
}
free(line);
in = fdopen(s_data, "r");
out = fdopen(s_data, "w");
}
for (guint i=0 ; i<EXCEPTS->len ; i++) {
g_autofree char *escaped_fn = fn_escape((char *)EXCEPTS->pdata[i]);
fprintf(out, "! %s\n", escaped_fn);
}
GQueue *TODO = g_queue_new();
GHashTable *HARDLINK = hardlink_hash_new();
g_queue_push_tail(TODO, g_strdup("."));
g_autoptr(GArray) entries = g_array_sized_new(FALSE, FALSE, sizeof(char *), 128);
while (1) {
g_autofree char *path = g_queue_pop_head(TODO);
if (path == NULL)
break;
if (is_excepted(path))
continue;
g_autoptr(FileInfo) fi = fileinfo_lstat(path);
if (!fi)
continue;
gint64 key=(gint64)fi->dev;
if (!allowremotefs && !g_hash_table_contains(LOCAL_DEV, &key)) {
warn("%s: remote filesystem\n", path);
continue;
}
if (fi->type == 'D' && MATCH_RE("/(package|project|confidential|home|scratch|src)/[^/]+\\.DELETEME$", path))
continue;
char *hardlink;
if (fi->type != 'D' && fi->nlink > 1) {
struct DevInode di = { fi->dev, fi->ino };
char *p = g_hash_table_lookup(HARDLINK, &di);
if (p)
hardlink = p;
else {
g_hash_table_insert(HARDLINK, g_memdup2(&di, sizeof(di)), fn_escape(path));
hardlink = "-";
}
} else {
hardlink = "-";
}
char *sum = "-";
if (cksum && fi->type == 'F') {
die("chksum not yet implemented\n");
}
g_autofree char *export = fileinfo_export(fi);
fprintf(out, "%s %s %s\n", export, hardlink, sum);
if (fi->type == 'F') {
fflush(out);
static char *response = NULL;
static size_t response_buflen = 0;
ssize_t len = getline(&response, &response_buflen, in);
if (len == -1) {
if (ferror(in)) {
die ("client disconnected: %m\n");
}
die ("client disconnected\n");
}
if (len && response[len-1] == '\n')
response[len-1] = '\0';
if (strcmp(response, "CONTINUE") == 0) {
;
} else if(strcmp(response, "SEND") == 0) {
if (bandwidth && !quiet) {
warn("sending %s", path);
send_file(out, fi);
warn("\n");
} else {
if (!quiet)
warn("sending %s\n", path);
send_file(out, fi);
}
} else {
die ("client sent unexpected response: %s\n", response);
}
} else if (fi->type == 'D') {
DIR *dir = opendir(path);
if (dir == NULL) {
if (errno == ENOENT || errno == ENOTDIR)
continue;
die("%s: %m\n", path);
}
errno = 0;
while (1) {
struct dirent *dirent = readdir(dir);
if (dirent == NULL) {
if (errno == 0)
break;
else
die("%s: %m\n", path);
}
if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0)
continue;
char *s = g_strdup_printf("%s/%s", path, dirent->d_name);
g_array_append_val(entries, s);
}
closedir(dir);
g_array_sort(entries, cmp_g_array_str_rev);
for (guint i=0; i<entries->len;i++) {
g_queue_push_head(TODO, ((char **)entries->data)[i]);
}
entries->len = 0;
}
}
fputs("% complete\n", out);
fclose(out);
fclose(in);
g_queue_free(TODO);
g_hash_table_unref(HARDLINK);
int wstatus;
res = waitpid(pid, &wstatus, 0);
if (!res)
die("%m");
if (wstatus) {
die(NULL);
}
}
int main(int argc, char **argv) {
char *argv0 = argv[0];
g_auto(GStrv) argv_from_env = NULL;
char *ssh_original_command = getenv("SSH_ORIGINAL_COMMAND");
if (ssh_original_command) {
argv_from_env = g_strsplit(ssh_original_command, " ", 0);
argv = argv_from_env;
argc = g_strv_length(argv);
}
g_autoptr(GStrvBuilder) excepts_builder = g_strv_builder_new();
g_autoptr(GStrvBuilder) ssh_opts_builder = g_strv_builder_new();
while (1) {
int opt = getopt_long(argc, argv, "", options, NULL);
if (opt == -1)
break;
else if (opt == 0)
continue;
else if (opt == 100)
lockident = g_strdup(optarg);
else if (opt == 101)
identity_file = g_strdup(optarg);
else if (opt == 102)
g_strv_builder_add(excepts_builder, optarg);
else if (opt == 103)
g_strv_builder_add(ssh_opts_builder, optarg);
else if (opt == 104)
unix_socket_name = g_strdup(optarg);
else if (opt == '?')
usage(argv0);
else
die("internal error: getopt returned %d\n", opt);
}
excepts = g_strv_builder_end(excepts_builder);
ssh_opts = g_strv_builder_end(ssh_opts_builder);
argv = &argv[optind];
argc -= optind;
if (noatime && cksum) {
warn("%s: --cksum together with --noatime not implemented\n", argv0);
_exit(1);
}
if (opt_nice) {
g_autofree char *cmd = g_strdup_printf("ionice -c idle -p %d", getpid());
int __attribute__((unused)) status = system(cmd);
}
if (slave_mode) {
if (argc != 1)
usage(argv0);
slave(argv[0]);
} else {
if (argc != 2)
usage(argv0);
master(argv[0], argv[1]);
}
}