/*
** 1999-01-20 -	This module contains gentoo's support for automounting devices. It's really
**		quite simple; we just buffer the contents of some number of fs tables (on
**		my system, /etc/fstab is the only one). A hook in the DirEnter command then
**		calls this module as we enter a dir, and we check if the directory to be
**		entered is known to be a mountpoint. If it is, we issue a mount(8) command.
** 1999-01-27 -	Removed use of Linux <mntent> API, now uses a wrapper in the new mntent
**		module to better cope with different systems. I hope.
*/

#include "gentoo.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "errors.h"
#include "strutil.h"
#include "dialog.h"
#include "fileutil.h"
#include "mntent.h"
#include "mount.h"

/* ----------------------------------------------------------------------------------------- */

#define	FD_INPUT	(0)
#define	FD_OUTPUT	(1)
#define	FD_ERROR	(2)

typedef struct {
	GHashTable	*mdir;		/* System fstab entries hashed on directory name. */
	GList		*mntlist;	/* List of names of mounted devices; umount at exit. */
} MntInfo;

static MntInfo	the_info = { NULL, NULL };

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-20 -	Scan through entries in <mtab> (opened by setmntent()) for an entry whose
**		mnt_dir component matches <path>. Returns TRUE if found, FALSE otherwise.
*/
static gboolean scan_for_dir(FILE *mtab, const gchar *path)
{
	MntEnt	*me;

	while((me = mne_getmntent(mtab)) != NULL)
	{
		if(strcmp(mne_get_mountpoint(me), path) == 0)
			return TRUE;
	}
	return FALSE;
}

/* 1999-01-21 -	Find out whether <path> is already mounted or not. Does this by inspecting
**		(using everybody's fav setmntent() API) the mount table. On Linux (and any
**		other systems having a compatible "/proc/mounts" file), using that is highly
**		recommended for performance reasons (it's an in-memory file, then).
*/
static gboolean dir_is_mounted(MainInfo *min, const gchar *path)
{
	FILE		*mtab;
	gchar		*mplist = min->cfg.path.path[PTID_MTAB]->str, mp[PATH_MAX];
	gboolean	ret = 0;
	gint		old_errno = errno;

	for(; !ret && fut_path_component(mplist, mp); mplist = NULL)
	{
		if((mtab = mne_setmntent(mp, "r")) != NULL)
		{
			ret = scan_for_dir(mtab, path);
			mne_endmntent(mtab);
		}
	}

	errno = old_errno;
	return ret;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-20 -	A GHashTable callback to free a mount entry. Pretty simple. */
static void free_mntent(gpointer key, gpointer value, gpointer user)
{
	mne_destroy(value);
}

/* 1999-01-20 -	Clear the internal information about available mountpoints. */
static void clear_info(MainInfo *min, MntInfo *mni)
{
	if(mni == NULL)
		return;

	if(mni->mdir != NULL)
	{
		g_hash_table_foreach(mni->mdir, free_mntent, NULL);
		g_hash_table_destroy(mni->mdir);
		mni->mdir = NULL;
	}
}

/* 1999-01-20 -	Initialize information about available file systems. This must have been called
**		once before any attempt to automount is made, or the mounting will fail. This can
**		also be called at a later time, to refresh the data.
** 1999-01-27 -	Now actually traverses the entire path list.
*/
gint mnt_init(MainInfo *min)
{
	char	*fplist, fp[PATH_MAX];
	FILE	*fstab;
	MntInfo	*mni = &the_info;
	MntEnt	*me, *ne;
	int	cnt = 0;

	clear_info(min, mni);
	if((mni->mdir = g_hash_table_new(g_str_hash, g_str_equal)) != NULL)
	{
		for(fplist = min->cfg.path.path[PTID_FSTAB]->str; fut_path_component(fplist, fp); fplist = NULL)
		{
			if((fstab = mne_setmntent(fp, "r")) != NULL)
			{
				while((me = mne_getmntent(fstab)) != NULL)
				{
					if((ne = mne_copy(me)) != NULL)
					{
						g_hash_table_insert(mni->mdir, (gpointer) mne_get_mountpoint(ne), ne);
						cnt++;
					}
				}
				mne_endmntent(fstab);
			}
		}
	}
	return mni->mdir != NULL;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-20 -	Initialize default values of configuration. */
void mnt_init_defaults(CfgInfo *cfg)
{
	MountInfo	*mi;

	if(cfg != NULL)
	{
		mi = &cfg->mount;

		mi->mode	  = MNT_NEVER;
		mi->check_nlink	  = 1;
		mi->show_stderr   = 1;
		mi->umount_exit	  = 1;
		str_strncpy(mi->cmd_mount,  "/bin/mount",  sizeof mi->cmd_mount);
		str_strncpy(mi->cmd_umount, "/bin/umount", sizeof mi->cmd_umount);
	}
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-20 -	Envoke the command whose full name is in <cmd>, with the single string argument
**		of <arg>. Set up a pipe to capture command's stderr. If <show_err> is TRUE, the
**		stderr text is shown in a dialog, if it's FALSE, it is simply dropped.
**		Returns 1 if command succeeded, 0 if it failed somehow.
*/
static int run_cmd(MainInfo *min, char *cmd, char *arg, gint show_err)
{
	gchar	*cname;
	int	child, fd_err[2], ret = 0;

	if((cname = strrchr(cmd, '/')) != NULL)
		cname++;
	else
		cname = cmd;

	if(pipe(fd_err) != 0)
		return 0;

	if((child = fork()) == 0)
	{
		assert(close(FD_ERROR) == 0);
		assert(dup(fd_err[FD_OUTPUT]) == FD_ERROR);
		assert(close(fd_err[FD_INPUT]) == 0);
		execl(cmd, cname, arg, NULL);
		perror("");
		exit(EXIT_FAILURE);
	}
	else if(child > 0)
	{
		int	status;

		assert(close(fd_err[FD_OUTPUT]) == 0);
		if(waitpid(child, &status, 0) == child)
		{
			if(WIFEXITED(status))
			{
				if(WEXITSTATUS(status) == 0)		/* Mount went OK? */
					ret = 1;
				else if(show_err)			/* Nope. Do we care enough? */
				{
					char	buf[1024];
					int	togo, got, offs = 0;

					offs = g_snprintf(buf, sizeof buf, "Execution of \"%s %s\" failed:\n", cmd, arg);
					for(togo = sizeof buf - 1 - offs; togo > 0 && (got = read(fd_err[FD_INPUT], buf + offs, togo)) > 0;)
					{
						offs += got;
						togo -= got;
					}
					buf[offs] = '\0';
					while(buf[offs - 1] == '\n')
						buf[--offs] = '\0';
					dlg_dialog_error(buf);
				}
			}
			else
				perror("MOUNT: Command execution failed");
		}
		close(fd_err[FD_INPUT]);
	}
	else if(child == -1)
		perror("MOUNT: Couldn't fork");

	if(errno)
		err_set(min, errno, cmd, arg);
	else
		err_clear(min);

	return ret && (errno == 0);
}

/* 1999-01-20 -	We are about to enter the directory whose full path is <path>. If necessary,
**		mount it. Returns 1 if the dir was mounted, 0 if not. Note that a return value
**		of 0 doesn't signal an error; perhaps <path> wasn't a mount point.
** 1999-01-21 -	Added support for checking the 'nlink' field of <path> before mounting, and
**		abandon attempt if it's not 2 (<=> the path is not an empty dir).
** 1999-01-28 -	Added the <ref> argument, which needs to be == the current mounting mode in
**		order for the mount to be attempted.
*/
gint mnt_entering(MainInfo *min, const gchar *path, MntMode ref)
{
	MntInfo	*mni = &the_info;
	MntEnt	*ment;

	if((ref != min->cfg.mount.mode) || (mni->mdir == NULL))
		return 0;
	if(min->cfg.mount.check_nlink)
	{
		struct stat	st;
	
		if((stat(path, &st) == 0) && (st.st_nlink != 2))
			return 0;
	}
	if(dir_is_mounted(min, path))
		return 0;

	if((ment = g_hash_table_lookup(mni->mdir, path)) != NULL)
	{
		err_printf(min, "Mounting \"%s\" on \"%s\"...", mne_get_device(ment), mne_get_mountpoint(ment));
		if(run_cmd(min, min->cfg.mount.cmd_mount, (char *) mne_get_mountpoint(ment), min->cfg.mount.show_stderr))
		{
			mni->mntlist = g_list_append(mni->mntlist, g_strdup((gchar *) mne_get_mountpoint(ment)));
			return 1;
		}
		err_show(min);
	}

	return 0;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-20 -	Clear away all internally buffered information. When this has been called,
**		all attempts to mount will fail until mnt_init() has been called. Note that
**		this will NOT unmount any filesystems mounted by this module up till now.
*/
void mnt_clear(MainInfo *min)
{
	clear_info(min, &the_info);
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-21 -	Shut down the mounting system. This unmounts all mounted volumes. */
void mnt_shutdown(MainInfo *min)
{
	MntInfo	*mni = &the_info;

	if(chdir("/") == 0)	/* Avoids having a mounted dir current, I hope. */
	{
		GList	*iter;

		for(iter = mni->mntlist; iter != NULL; iter = g_list_next(iter))
		{
			run_cmd(min, min->cfg.mount.cmd_umount, (char *) iter->data, FALSE);
			g_free(iter->data);
		}
		g_list_free(mni->mntlist);
		mni->mntlist = NULL;
	}
	else
		perror("MOUNT: Couldn't chdir() to / for unmounting");
}
