/* -*- linux-c -*-
 *
 * automountd.c
 *
 *  Automount daemon for Linux autofs
 *
 */

#include <dirent.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <paths.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <dlfcn.h>
#include <syslog.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <linux/auto_fs.h>

#include "automount.h"

char *program;

static pid_t my_pgrp;			/* The "magic" process group */
static pid_t my_pid;			/* The pid of this process */

static lookup_init_t lookup_init;
static lookup_mount_t lookup_mount;
static lookup_done_t lookup_done;

static int mounted = 0;

struct pending_mount {
	pid_t pid;		/* Which process is mounting for us */
	unsigned long wait_queue_token;	/* Associated kernel wait token */
	struct pending_mount *next;
};

static struct autofs_point {
	char *path;		/* Mount point name */
	int pipefd;		/* File descriptor for pipe */
	int ioctlfd;		/* File descriptor for ioctls */
	dev_t dev;		/* "Device" number assigned by kernel */
	struct pending_mount *mounts; /* Pending mount queue */
	void *lookup_context;	/* lookup library context pointer */
} ap;

static int umount_all(void)
{
	int left, rv;
	struct stat st;
	struct dirent *de;
	DIR *dp;
	char path_buf[PATH_MAX];

	chdir("/");

	dp = opendir(ap.path);
	if ( !dp ) {
		syslog(LOG_ERR, "umount_all: opendir: %m");
		return -1;
	}

	left = 0;

	while ( (de = readdir(dp)) != NULL ) {
		if ( strcmp(de->d_name,".") && strcmp(de->d_name,"..") ) {
			sprintf(path_buf, "%s//%s", ap.path, de->d_name);
			if ( !lstat(path_buf,&st) ) {
				if ( S_ISDIR(st.st_mode) ) {
					if ( st.st_dev != ap.dev ) {
						rv = spawnl(_PATH_UMOUNT, _PATH_UMOUNT, path_buf, NULL);
						if ( rv ) 
							left++;
						else
							rmdir(path_buf);
					} else
						rmdir(path_buf);
				} else 
					unlink(path_buf);
			}
		}
	}
	closedir(dp);

	return left;
}	

static int umount_autofs(int force)
{
	int rv;

	if ( !mounted )
		return -1;

	if ( umount_all() && !force )
		return -1;

	if (ap.ioctlfd >= 0) {
		ioctl(ap.ioctlfd, AUTOFS_IOC_CATATONIC, 0);
		close(ap.ioctlfd);
	}
	if (ap.pipefd >= 0)
		close(ap.pipefd);
	rv = spawnl(_PATH_UMOUNT, _PATH_UMOUNT, ap.path, NULL);
	free(ap.path);
	mounted = 0;
	return rv;
}

static int mount_autofs(char *path)
{
	int pipefd[2];
	char options[128];
	char our_name[128];
	struct stat st;

	if ( mounted ) {
		/* This can't happen */
		syslog(LOG_ERR, "mount_autofs: already mounted");
		return -1;
	}

	if ( path[0] != '/' ) {
		errno = EINVAL;	/* Must be an absolute pathname */
		return -1;
	}

	ap.path = strdup(path);
	if ( !ap.path ) {
		errno = ENOMEM;
		return -1;
	}
	ap.pipefd = ap.ioctlfd = -1;

	/* In case the directory doesn't exist, try to mkdir it */
	if ( mkdir(path, 0555) < 0 && errno != EEXIST )
		return -1;

	if ( pipe(pipefd) < 0 )
		return -1;
	
	sprintf(options, "fd=%d,pgrp=%u,minproto=%d,maxproto=%d", pipefd[1],
		(unsigned)my_pgrp, AUTOFS_PROTO_VERSION, AUTOFS_PROTO_VERSION);
	sprintf(our_name, "automountd(pid%u)", (unsigned)my_pid);

	if ( spawnl(_PATH_MOUNT, _PATH_MOUNT, "-t", "autofs", "-o",
		    options, our_name, path, NULL) ) {
		close(pipefd[0]);
		close(pipefd[1]);
		return -1;
	}

	close(pipefd[1]);	/* Close kernel pipe end */
	ap.pipefd = pipefd[0];
	
	chdir(path);
	ap.ioctlfd = open(".", O_RDONLY); /* Root directory for ioctl()'s */
	chdir("/");
	if ( ap.ioctlfd < 0 ) {
		umount_autofs(1);
		return -1;
	}

	stat(path,&st);
	ap.dev = st.st_dev;	/* Device number for mount point checks */

	ap.mounts = NULL;	/* No pending mounts */
	mounted = 1;

	return 0;
}

static sig_atomic_t shutdown = 0;
static sig_atomic_t prune = 0;

static void sig_shutdown(int sig)
{
	shutdown = sig;
}

static void sig_prune(int sig)
{
	prune = 1;
}

static int get_pkt(int fd, struct autofs_packet_missing *pkt)
{
	char *buf = (char *) pkt;
	int bytes = sizeof(*pkt);
	
	do {
		int i = read(fd, buf, bytes);
		if (!i)
			break;
		if (i < 0) {
			if (errno == EINTR) {
				if ( prune ) {
					umount_all();
					prune = 0;
				} 
				if ( !shutdown )
					continue;
			}
			break;
		}
		buf += i;
		bytes -= i;
	} while (bytes);
	return bytes;
}

static int send_ready(unsigned int wait_queue_token)
{
	if ( ioctl(ap.ioctlfd, AUTOFS_IOC_READY, wait_queue_token) < 0 ) {
		syslog(LOG_ERR, "AUTOFS_IOC_READY: %m");
		return 1;
	} else
		return 0;
}

static int send_fail(unsigned int wait_queue_token)
{
	if ( ioctl(ap.ioctlfd, AUTOFS_IOC_FAIL, wait_queue_token) < 0 ) {
		syslog(LOG_ERR, "AUTOFS_IOC_READY: %m");
		return 1;
	} else
		return 0;
}

static int handle_packet(void)
{
	struct autofs_packet_missing pkt;
	struct stat st;
	pid_t f;

	if (get_pkt(ap.pipefd, &pkt))
		return -1;

	chdir(ap.path);
	if ( lstat(pkt.name,&st) || (S_ISDIR(st.st_mode) && st.st_dev == ap.dev) ) {
		/* Need to mount or symlink */
		struct pending_mount *mt;

		if ( !(mt = malloc(sizeof(struct pending_mount))) ) {
			syslog(LOG_ERR, "handle_packet: malloc: %m");
			send_fail(pkt.wait_queue_token);
			return 1;
		}

		syslog(LOG_INFO, "attempting to mount entry %s", pkt.name);
		
		f = fork();
		if ( f == -1 ) {
			syslog(LOG_ERR, "handle_packet: fork: %m");
			send_fail(pkt.wait_queue_token);
			return 1;
		} else if ( !f ) {
			int i;
			struct sigaction sa;
			sa.sa_handler = SIG_DFL;
			sigemptyset(&sa.sa_mask);
			sa.sa_flags = 0;
			for ( i = 1 ; i < NSIG ; i++ )
				sigaction(i, &sa, NULL);

			/* Wait for go-ahead from main process */
			kill(getpid(), SIGSTOP);
			i = lookup_mount(ap.path,pkt.name,pkt.len,ap.lookup_context);
			_exit(i ? 1 : 0);
		} else {
			int status;
			sigset_t ss;
			
			sigemptyset(&ss);
			sigaddset(&ss, SIGCHLD);
			
			mt->pid = f;
			mt->wait_queue_token = pkt.wait_queue_token;
			sigprocmask(SIG_BLOCK, &ss, NULL);
			mt->next = ap.mounts;
			ap.mounts = mt;
			if ( waitpid(f, &status, WUNTRACED) != f ||
			     !WIFSTOPPED(status) ) {
				sigprocmask(SIG_UNBLOCK, &ss, NULL);
				send_fail(pkt.wait_queue_token);
				return 1;
			}
			sigprocmask(SIG_UNBLOCK, &ss, NULL);
			kill(f, SIGCONT); /* Go for it */
		}
	} else {
		/* Already there (can happen if a process connects to a
		   directory while we're still working on it) */
		chdir("/");
		send_ready(pkt.wait_queue_token);
	}
	return 0;
}

void sig_child(int sig)
{
	pid_t pid;
	int status;
	struct pending_mount *mt, **mtp;

	pid = waitpid(-1, &status, WNOHANG);
	if ( pid > 0 ) {
		for ( mtp = &ap.mounts ; (mt = *mtp) ; mtp = &mt->next ) {
			if ( mt->pid == pid ) {
				if ( !WIFEXITED(status) && !WIFSIGNALED(status) )
					break;
				else if ( WIFSIGNALED(status) || WEXITSTATUS(status) != 0 )
					send_fail(mt->wait_queue_token);
				else
					send_ready(mt->wait_queue_token);
				*mtp = mt->next; /* Unlink */
				break;
			}
		}
	}
}

static void initialize(void)
{
	pid_t pid;

	/* Don't BUSY any directories unneccessarily */
	chdir("/");

	/* Detach */
	pid = fork();
	if ( pid > 0 )
		exit(0);
	else if ( pid < 0 ) {
		fprintf(stderr, "%s: Could not detach process\n", program);
		exit(1);
	}
	
	/* Open syslog */ 
	openlog("automount", LOG_PID, LOG_DAEMON);

	/* Initialize global data */
	my_pid = getpid();

	/* Make our own process group for "magic" reason */
	if ( setpgrp() ) {
		syslog(LOG_CRIT, "setpgrp: %m");
		exit(1);
	}
	my_pgrp = getpgrp();

}

int main(int argc, char *argv[])
{
	char *path, *map, *mapfmt, **maparg;
	void *lookup_lib;
	struct sigaction sa;
	char map_buf[PATH_MAX];
	int *lookup_version;

	program = argv[0];

	if ( argc < 3 ) {
		fprintf(stderr, "Usage: %s path map_type [args...]\n", program);
		exit(1);
	}

	initialize();

	path =    argv[1];
	map  =    argv[2];
	maparg = &argv[3];

	syslog(LOG_INFO, "starting automounter, path = %s, maptype = %s, mapname = %s\n", path, map, (argc < 4) ? "none" : maparg[0]);

	if ( (mapfmt = strchr(map,',')) )
		*(mapfmt++) = '\0';

	sprintf(map_buf, "%s//lookup_%s.so", MYLIBDIR, map);

	if ( !(lookup_lib = dlopen(map_buf, RTLD_NOW)) ) {
		syslog(LOG_CRIT, "cannot load resolver module \"%s\"\n", map);
		exit(1);
	}
	
	lookup_version = (int *) dlsym(lookup_lib, "lookup_version");
	lookup_init = (lookup_init_t) dlsym(lookup_lib, "lookup_init");
	lookup_mount = (lookup_mount_t) dlsym(lookup_lib, "lookup_mount");
	lookup_done = (lookup_done_t) dlsym(lookup_lib, "lookup_done");

	if ( !lookup_version || *lookup_version != AUTOFS_LOOKUP_VERSION ||
		!lookup_init || !lookup_mount || !lookup_done ) {
		syslog(LOG_CRIT, "resolver library (%s) version mismatch\n",
		       map);
		dlclose(lookup_lib);
		exit(1);
	}

	if (lookup_init(mapfmt, argc-3, maparg, &ap.lookup_context)) {
		exit(1);
	}

	if (mount_autofs(path) < 0) {
		syslog(LOG_CRIT, "%s: mount failed!\n", path);
		exit(1);
	}	

	sa.sa_handler = sig_shutdown;
	sigemptyset(&sa.sa_mask);
	sigaddset(&sa.sa_mask, SIGCHLD);
	sa.sa_flags = 0;
	sigaction(SIGINT, &sa, NULL);
	sigaction(SIGQUIT, &sa, NULL);
	sigaction(SIGILL, &sa, NULL);
	sigaction(SIGTRAP, &sa, NULL);
	sigaction(SIGABRT, &sa, NULL);
	sigaction(SIGIOT, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);
	sigaction(SIGFPE, &sa, NULL);
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGPIPE, &sa, NULL);
	sigaction(SIGALRM, &sa, NULL);
	sigaction(SIGTERM, &sa, NULL);
	sigaction(SIGSTKFLT, &sa, NULL);
	sigaction(SIGIO, &sa, NULL);
	sigaction(SIGPWR, &sa, NULL);
	sigaction(SIGUSR2, &sa, NULL);

	sa.sa_handler = sig_child;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sigaction(SIGCHLD, &sa, NULL);

	sa.sa_handler = sig_prune;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sigaction(SIGUSR1, &sa, NULL);

	while ( !shutdown ) {
		if (handle_packet() && errno != EINTR)
			break;
		if (shutdown == SIGUSR2 && umount_autofs(0))
			shutdown = 0;
	}
	umount_autofs(1);

	lookup_done(ap.lookup_context);
	dlclose(lookup_lib);

	closelog();

	return 0;
}
