/*
 * Copyright (c) 2003, 2004, 2005 Nokia
 * Author: Timo Savola <tsavola@movial.fi>
 *
 * This program is licensed under GPL (see COPYING for details)
 */

#include "client.h"
#include "common.h"
#include "protocol.h"
#include "buffer.h"
#include "config.h"
#include "mount.h"
#include "version.h"

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#include <termios.h>
#include <signal.h>
#include <getopt.h>
#include <time.h>
#include <pwd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <limits.h>


#define REQUIRED_VERSION 6

extern char **environ;

/** The name of this program. */
static char *progname;

/** Path of the config file (command line option). */
static char *configpath = NULL;

/** The command line arguments. */
static char *target = NULL;
static char *remote_user = NULL;
static char *cwd = NULL;
static char **args = NULL;
static int action = PTYPE_COMMAND;

/** The socket fd. */
static int sd = -1;

/** Buffers used to copy data around. */
static buffer_t *buf_out;
static buffer_t *buf_err;
static char *tmp_buf;

/** Is the tty in raw mode? */
static bool_t rawmode = FALSE;

/** The original mode of the tty. */
static struct termios oldtio;

/** How much IN DATA has the server requested? */
static size_t inreq = 0;

/** Have we requested some data? */
static bool_t outwait = FALSE;
static bool_t errwait = FALSE;

/** The protocol version of the daemon. */
static int daemon_version = 0;

#ifdef DEBUG
static void debug(const char *msg, ...)
{
	va_list arg;

	fprintf(stderr, "%s (%d) debug: ", progname, getpid());

	va_start(arg, msg);
	vfprintf(stderr, msg, arg);
	va_end(arg);

	fprintf(stderr, "\n");
	fflush(stderr);
}
#else
# define debug(msg, ...)
#endif

/*
 * Prints progname, message and errno description to stderr.
 */
void error(const char *msg, ...)
{
	char *desc = NULL;
	va_list arg;

	if (errno > 0)
		desc = strerror(errno);

#ifdef DEBUG
	fprintf(stderr, "%s (%d): ", progname, getpid());
#else
	fprintf(stderr, "%s: ", progname);
#endif

	va_start(arg, msg);
	vfprintf(stderr, msg, arg);
	va_end(arg);

	if (desc)
		fprintf(stderr, " (%s)", desc);

	fprintf(stderr, "\n");
	fflush(stderr);
}

/**
 * Writes (some of) buffer to a file.
 * @param buf the buffer
 * @param fd the target file descriptor
 * @param wait is set to false when the buffer is empty
 * @return 0 on success, -1 on error
 */
static int write_buffer(buffer_t *buf, int fd, bool_t *wait)
{
	/* buf is never at EOF so fd never becomes -1. */
	if (buf_write_out(buf, &fd) < 0) {
		error(fd == STDOUT_FILENO ?
		      "Can't write buffer to stdout" :
		      "Can't write buffer to stderr");
		return -1;
	}

	if (buf_is_empty(buf))
		*wait = FALSE;

	return 0;
}

/**
 * Sends an IN DATA packet.
 * @return 0 usually, 1 if stdin reached EOF, -1 on error
 */
static int send_data(void)
{
	bool_t ok = FALSE;
	ssize_t len = BUFFER_SIZE;

	/*
	 * Get the amount of data we can read from a terminal.
	 */
	if (isatty(STDIN_FILENO)) {
		if (ioctl(STDIN_FILENO, FIONREAD, &len) < 0) {
			error("Can't check tty for available data");
			return -1;
		}

		if (len < 0) {
			error("ioctl(tty) gave invalid read length");
			return -1;
		}

		if (len == 0)
			return 0;

		if (len > BUFFER_SIZE)
			len = BUFFER_SIZE;
	}

	len = read(STDIN_FILENO, tmp_buf, len);
	if (len < 0) {
		error("Can't read from stdin");
		return -1;
	}

	if (len == 0) {
		debug("Stdin hit EOF");
		ok = 1;
		inreq = 0;
	}

	if (write_buf_packet(sd, PTYPE_IN_DATA, len, tmp_buf) < 0) {
		error("Can't write IN DATA packet to socket");
		return -1;
	}

	/* At EOF: 0-0=0 */
	inreq -= len;

	return ok;
}

/**
 * Reads bytes from socket to a buffer.
 * @param buf the target buffer
 * @return 0 on success, -1 on error
 */
static int receive_stream(buffer_t *buf)
{
	uint32_t len;
	if (read_uint32(sd, &len) < 0) {
		error("Unable to read data packet length");
		return -1;
	}

	if (len == 0) {
		errno = 0;
		error("Received empty data packet (EOF)");
		return -1;
	}

	if (buf_read_in(buf, sd, len) < 0) {
		error("Can't append data packet to buffer");
		return -1;
	}

	return 0;
}

/**
 * Reads message from socket and prints it.
 * @param type "error" or "warning"
 * @return 0 on success, -1 on error
 */
static int receive_message(ptype_t type)
{
	uint32_t len;
	if (read_uint32(sd, &len) < 0) {
		error("Unable to read message packet length");
		return -1;
	}

	if (read_buf(sd, tmp_buf, len) < 0) {
		error("Can't read message packet");
		return -1;
	}

	if (len == BUFFER_SIZE)
		len = BUFFER_SIZE - 1;

	tmp_buf[len] = '\0';

#ifdef DEBUG
	if (type == PTYPE_ERROR)
		fprintf(stderr, "%s (%d) server: %s\n", progname, getpid(), tmp_buf);
	else
		fprintf(stderr, "%s (%d): %s\n", progname, getpid(), tmp_buf);
#else
	if (type == PTYPE_ERROR)
		fprintf(stderr, "%s server: %s\n", progname, tmp_buf);
	else
		fprintf(stderr, "%s: %s\n", progname, tmp_buf);
#endif

	return 0;
}

/**
 * Reads a packet from the socket (sd) and does something about it.
 * The value of an RC packet is stored in the global rc variable.
 * @return 1 on RC, 0 on other valid packet type, -1 on error
 */
static int receive_packet(uint16_t *rc)
{
	ptype_t type = read_enum(sd);
	switch (type) {
	case -1:
		error("Can't read packet type from socket");
		return -1;

	case PTYPE_IN_REQ:
		inreq = BUFFER_SIZE;
		return 0;

	case PTYPE_OUT_DATA:
		return receive_stream(buf_out);

	case PTYPE_ERR_DATA:
		return receive_stream(buf_err);

	case PTYPE_RC:
		debug("Receiving RC packet");
		if (read_uint16(sd, rc) < 0) {
			error("Can't read RC packet from socket");
			return -1;
		}
		return 1;

	case PTYPE_MESSAGE:
		return receive_message(PTYPE_MESSAGE);

	case PTYPE_ERROR:
		return receive_message(PTYPE_ERROR);

	default:
		errno = 0;
		error("Received packet has unexpected type (0x%02x)", type);
		return -1;
	}
}

/**
 * Sends a request for more data.
 * @param ptype PTYPE_OUT_REQ or PTYPE_ERR_REQ
 * @param wait is set to true
 * @return 0 on success, -1 on error
 */
static int send_request(ptype_t ptype, bool_t *wait)
{
	if (write_enum(sd, ptype) < 0) {
		error("Can't write packet to socket");
		return -1;
	}

	*wait = TRUE;

	return 0;
}

/**
 * Manages stdin/stdout/stderr and waits for the return code.
 * @return -1 on error, return code on success
 */
static int manage(bool_t is_tty)
{
	fd_set readfds, writefds;
	bool_t inopen;
	int ok;

	/* Check if stdin is really available */
	if (is_tty || read(STDIN_FILENO,NULL,0)!=-1) {
		inopen = TRUE;
	} else {
		inopen = FALSE;
	}

	while (1) {
		FD_ZERO(&readfds);
		FD_ZERO(&writefds);

		FD_SET(sd, &readfds);

		if (inopen && inreq)
			FD_SET(STDIN_FILENO, &readfds);

		if (!buf_is_empty(buf_out) || !outwait)
			FD_SET(STDOUT_FILENO, &writefds);

		if (!is_tty && (!buf_is_empty(buf_err) || !errwait))
			FD_SET(STDERR_FILENO, &writefds);

		if (select(sd + 1, &readfds, &writefds, NULL, NULL) <= 0) {
			error("Can't select");
			return -1;
		}

		/* read packet from socket */
		if (FD_ISSET(sd, &readfds)) {
			uint16_t rc;

			ok = receive_packet(&rc);
			if (ok < 0)
				return -1;

			if (ok > 0)
				return rc;
		}

		/* send data from stdin if requested */
		if (inopen && inreq && FD_ISSET(STDIN_FILENO, &readfds)) {
			ok = send_data();
			if (ok < 0)
				return -1;
			if (ok > 0)
				inopen = FALSE;
		}

		/* flush buf_out to stdout or request more data */
		if (FD_ISSET(STDOUT_FILENO, &writefds)) {
			if (buf_is_empty(buf_out))
				ok = outwait || send_request(PTYPE_OUT_REQ, &outwait) >= 0;
			else
				ok = write_buffer(buf_out, STDOUT_FILENO, &outwait) >= 0;

			if (!ok)
				return -1;
		}

		/* flush buf_err to stderr or request more data */
		if (FD_ISSET(STDERR_FILENO, &writefds)) {
			if (buf_is_empty(buf_err))
				ok = errwait || send_request(PTYPE_ERR_REQ, &errwait) >= 0;
			else
				ok = write_buffer(buf_err, STDERR_FILENO, &errwait) >= 0;

			if (!ok)
				return -1;
		}
	}

	/* Not reached. */
	return -1;
}

/**
 * Sets stdin to raw mode.
 */
static int set_raw_mode(void)
{
	struct termios tio;

	if (tcgetattr(STDIN_FILENO, &tio) < 0) {
		error("Can't get termios");
		return -1;
	}

	oldtio = tio;

	tio.c_iflag |= IGNPAR;
	tio.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY);
	tio.c_lflag &= ~(IXOFF | ISIG | ICANON | ECHO | ECHOE | ECHOK);
	tio.c_lflag &= ~(ECHONL | IEXTEN | OPOST);
	tio.c_cc[VMIN] = 1;
	tio.c_cc[VTIME] = 0;

	debug("Entering raw mode");

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &tio) < 0) {
		error("Can't change termios");
		return -1;
	}

	rawmode = TRUE;

	return 0;
}

/**
 * Sets stdin in its original mode.
 */
static void set_old_mode(void)
{
	if (!rawmode) {
		debug("Not in raw mode");
		return;
	}

	debug("Leaving raw mode");

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &oldtio) < 0)
		error("Can't restore original termios");
}

static void usage(void)
{
	fprintf(stderr, "Usage: %s [-t|--target <target>]"
				 " [-c|--config <path>]"
				 " [-d|--directory <dir>]"
				 " [-r|--remote <uid>,<gid>,<username>]"
				 " [<command>]"
				 " [<args>]\n"
			"       %s [-t|--target <target>]"
				 " [-c|--config <path>]"
				 " --mount\n"
			"       %s [-t|--target <target>]"
				 " [-c|--config <path>]"
				 " --umount\n"
			"       %s -v|--version\n"
			"       %s -h|--help\n",
		progname, progname, progname, progname, progname);
}

static bool_t should_skip_parameter(char *arg, struct option *longopts)
{
	bool_t is_long;
	struct option *opt;

	is_long = strlen(arg) > 2;

	/* check if arg also has its value */
	if (is_long && strchr(arg, '=') != NULL)
		return FALSE;

	for (opt = longopts; opt->name; ++opt) {
		if (is_long) {
			if (strcmp(&arg[2], opt->name) != 0)
				continue;
		} else {
			if (arg[1] != opt->val)
				continue;
		}

		return opt->has_arg == required_argument;
	}

	return FALSE;
}

static char **modify_args(int old_argc, char **old_argv, int *new_argc, struct option *longopts)
{
	char **new_argv;
	int i = 0;

	new_argv = calloc(old_argc + 2, sizeof (char *));
	if (!new_argv) {
		oom_error();
		exit(1);
	}

	/* sbrsh's progname */
	new_argv[i++] = *old_argv++;

	if (old_argc > 1) {
		/* sbrsh's options and -- */
		while (i < old_argc) {
			char *arg = *old_argv++;

			if (arg[0] != '-') {
				new_argv[i++] = "--";
				new_argv[i++] = arg;
				break;
			}

			new_argv[i++] = arg;

			if (should_skip_parameter(arg, longopts) && i < old_argc)
				new_argv[i++] = *old_argv++;
		}

		/* command and its arguments */
		while (*old_argv)
			new_argv[i++] = *old_argv++;
	}

	*new_argc = i;
	return new_argv;
}

/**
 * Fills in the options from argv. Prints usage and exits on error.
 * @return TRUE if we should --umount-all and not run a command
 */
static void read_args(int orig_argc, char **orig_argv)
{
	char **argv;
	int argc;
	int skip_args=0;

	struct option longopts[] = {
		{ "help",       no_argument,       0, 'h' },
		{ "version",    no_argument,       0, 'v' },
		{ "sbox-call",    no_argument,     &skip_args, 1 },
		{ "target",     required_argument, 0, 't' },
		{ "config",     required_argument, 0, 'c' },
		{ "directory",  required_argument, 0, 'd' },
		{ "remote",     required_argument, 0, 'r' },
		{ "mount",      no_argument,       &action, PTYPE_MOUNT  },
		{ "umount",     no_argument,       &action, PTYPE_UMOUNT },
		{ 0 }
	};

	argv = modify_args(orig_argc, orig_argv, &argc, longopts);

	while (1) {
		int c;

		c = getopt_long(argc, argv, "hvt:c:d:r:", longopts, NULL);
		if (c < 0) {
			break;
		}

		switch (c) {
		case 't':
			target = optarg;
			break;

		case 'c':
			configpath = optarg;
			break;

		case 'd':
			cwd = optarg;
			break;

		case 'r':
			remote_user = optarg;
			break;

		case 0:
			/* --mount or --umount */
			break;

		case 'v':
			fprintf(stderr, "Scratchbox Remote Shell client %d%s\n",
				PROTOCOL_VERSION, REVISION);
			exit(0);

		case 'h':
			usage();
			exit(0);

		default:
			usage();
			exit(1);
		}
	}

	if (argv[optind] && argv[optind+skip_args])
		args = &argv[optind+skip_args];
}

/**
 * Called when the process exits.
 */
static void cleanup(void)
{
	/* see that stdout and stderr buffers are flushed */

	if (!buf_is_empty(buf_out)) {
		set_nonblocking(STDOUT_FILENO, FALSE);
		write_buffer(buf_out, STDOUT_FILENO, &outwait);
	}

	if (!buf_is_empty(buf_err)) {
		set_nonblocking(STDERR_FILENO, FALSE);
		write_buffer(buf_err, STDERR_FILENO, &errwait);
	}

	set_old_mode();

	debug("sbrsh exiting");
}

static int determine_user_info(uid_t *uidp, gid_t *gidp, char *username)
{
	uid_t uid;
	gid_t gid;

	if (remote_user != NULL) {
		/*
		 * Parse the list for uid, gid, and username (string placed at
		 * end for simplicity)
		 */
		if (sscanf(remote_user, "%d,%d,%s", &uid, &gid, username) != 3) {
			error("Invalid format for -r argument: %s", remote_user);
			return 1;
		}

	} else {
		char *str;

		uid = geteuid();
		gid = getegid();

		struct passwd *userstruct = getpwuid(uid);
		if (!userstruct) {
			error("Can't get user information about uid %d", uid);
			return 1;
		}

		strcpy(username, userstruct->pw_name);

		/*
		 * Check for environment overrides
		 */
		str = getenv("_SBOX_NONFAKE_USER");
		if (str) {
			strcpy(username, str);
		}

		str = getenv("_SBOX_NONFAKE_UID");
		if (str && (uid = atoi(str)) <= 0) {
			error("Invalid _SBOX_NONFAKE_UID: %d", uid);
			return 1;
		}

		str = getenv("_SBOX_NONFAKE_GID");
		if (str && (gid = atoi(str)) <= 0) {
			error("Invalid _SBOX_NONFAKE_GID: %d", gid);
			return 1;
		}
	}

	*uidp = uid;
	*gidp = gid;

	return 0;
}

/*
 * Reads options, resolves and connects to host, talks with it, returns rc.
 */
int main(int argc, char **argv)
{
	config_t *cfg;
	mount_info_t **mounts;
	int16_t rc;
	uid_t uid = 0;
	gid_t gid = 0;
	char user[NAME_MAX];

	progname = get_progname(argv[0]);

	debug("sbrsh version %d%s", PROTOCOL_VERSION, REVISION);

	/*
	 * Allocate buffers
	 */

	if (!(cfg     = config_alloc()) ||
	    !(buf_out = buf_alloc())    ||
	    !(buf_err = buf_alloc())    ||
	    !(tmp_buf = malloc(BUFFER_SIZE))) {
		oom_error();
		return 1;
	}

	/*
	 * Clean exit
	 */
	{
		struct sigaction act;

		act.sa_handler = exit;
		sigemptyset(&act.sa_mask);
		act.sa_flags = SA_ONESHOT;

		sigaction(SIGINT, &act, NULL);
		sigaction(SIGHUP, &act, NULL);
		sigaction(SIGTERM, &act, NULL);

		atexit(cleanup);
	}

	/*
	 * Parse arguments
	 */

	read_args(argc, argv);

	/*
	 * Read configuration file
	 */
	{
		char *home, *path = NULL;

		if (!configpath) {
			home = getenv("HOME");
			path = malloc(strlen(home) + strlen("/" CONFIG_NAME) + 1);
			if (!path) {
				oom_error();
				return 1;
			}
			strcpy(path, home);
			strcat(path, "/" CONFIG_NAME);

			configpath = path;
		}

		if (!config_read(cfg, configpath, target)) {
			if (target)
				error("Target %s not found in %s", target, configpath);
			else
				error("No targets found in %s", configpath);
			return 1;
		}

		if (path) {
			free(path);
			configpath = NULL;
		}
	}

	/*
	 * Parse and do stuff with mount entries
	 */
	{
		size_t len, i;

		len = calc_vec_len((void **) cfg->opts);

		mounts = calloc(len + 1, sizeof (mount_info_t *));
		if (!mounts) {
			oom_error();
			return 1;
		}

		for (i = 0; i < len; ++i) {
			mount_info_t *mi = mntinfo_parse(cfg->opts[i]);
			if (!mi)
				return 1;

			debug("type=%d device=%s point=%s opts=%s",
			      mi->type, mi->device, mi->point, mi->opts);

			if (action == PTYPE_COMMAND && (MTYPE_NFS == mi->type ||
			                                MTYPE_SSH == mi->type)) {
				if (mntinfo_stat_device(mi) < 0)
					return 1;

				debug("device_dev=%lld", mi->device_dev);
			}

			mounts[i] = mi;
		}
	}

	/*
	 * Connect to server
	 */
	{
		struct addrinfo *ai, *i, hints = { 0 };
		char *port = DEFAULT_PORT;
		int ret;

		hints.ai_flags = AI_ADDRCONFIG;
		hints.ai_socktype = SOCK_STREAM;

		if (cfg->port)
			port = cfg->port;

		ret = getaddrinfo(cfg->host, port, &hints, &ai);
		if (ret < 0) {
			error("Can't resolve host: %s", gai_strerror(ret));
			return 1;
		}

		for (i = ai; i; i = i->ai_next) {
			sd = socket(i->ai_family, i->ai_socktype,
			            i->ai_protocol);
			if (sd < 0)
				continue;

			if (connect(sd, i->ai_addr, i->ai_addrlen) == 0)
				break;

			close(sd);
			sd = -1;
		}

		if (sd < 0) {
			error("Can't connect");
			return 1;
		}

		freeaddrinfo(ai);
	}

	/*
	 * Version
	 */

	if (send_version(sd) < 0) {
		error("Can't write protocol version packet to socket");
		return 1;
	}

	daemon_version = get_version(sd);
	if (daemon_version < 0) {
		error("Can't read protocol version packet from socket");
		return 1;
	}

	if (daemon_version < REQUIRED_VERSION) {
		errno = 0;
		error("Server version %d is too old (version %d required)",
		      daemon_version, REQUIRED_VERSION);
		return 1;
	}

	/*
	 * Find out remote user ID, group ID, and username
	 */
	if (determine_user_info(&uid, &gid, user)) {
		return 1;
	}

	/*
	 * Send user name
	 */
	{

		debug("Sending USER packet: %s", user);
		if (write_str_packet(sd, PTYPE_USER, user) < 0) {
			error("Can't send USER packet");
			return 1;
		}
	}

	/*
	 * Read authentication reply or error messages
	 */
	while (1) {
		uint16_t val;
		ptype_t type = read_enum(sd);
		switch (type) {
			uint16_t auth;

		case -1:
			error("Can't read packet type from socket");
			return 1;

		case PTYPE_AUTH:
			debug("Receiving AUTH packet");
			if (read_uint16(sd, &auth) < 0) {
				error("Can't read AUTH packet from socket");
				return 1;
			}
			if (auth) {
				debug("Authentication ok");
				break;
			} else {
				errno = 0;
				error("Authentication failed");
				return 1;
			}

		case PTYPE_RC:
			debug("Receiving RC packet");
			if (read_uint16(sd, &val) < 0) {
				error("Can't read RC packet from socket");
				return 1;
			}
			rc = val;
			break;

		case PTYPE_MESSAGE:
			receive_message(PTYPE_MESSAGE);
			continue;

		case PTYPE_ERROR:
			receive_message(PTYPE_ERROR);
			continue;

		default:
			errno = 0;
			error("Received packet has unexpected type (0x%02x)", type);
			return 1;
		}

		break;
	}

	/*
	 * Send target name
	 */
	debug("Sending TARGET packet: %s", cfg->target);
	if (write_str_packet(sd, PTYPE_TARGET, cfg->target) < 0) {
		error("Can't send TARGET packet");
		return 1;
	}

	config_free(cfg);

	/*
	 * Send mounts
	 */
	debug("Sending MOUNTS packet");
	if (write_enum(sd, PTYPE_MOUNTS) < 0 || write_mountv(sd, mounts) < 0) {
		error("Can't send MOUNTS packet");
		return 1;
	}

	/*
	 * Action paths
	 */

	if (action == PTYPE_COMMAND) {
		bool_t is_tty;

		/*
		 * Send arguments
		 */
		if (args) {
			debug("Sending ARGS packet");
			if (write_enum(sd, PTYPE_ARGS) < 0 || write_strv(sd, args) < 0) {
				error("Can't send ARGS packet");
				return 1;
			}
		}

		/*
		 * Send current working directory
		 */
		if (cwd) {
			debug("Sending CWD packet");
			if (write_str_packet(sd, PTYPE_CWD, cwd) < 0) {
				error("Can't send CWD packet");
				return 1;
			}
		}

		/*
		 * Send environment
		 */
		{
			debug("Sending ENVIRON packet");
			if (write_enum(sd, PTYPE_ENVIRON) < 0 || write_strv(sd, environ) < 0) {
				error("Can't send ENVIRON packet");
				return 1;
			}
		}

		/*
		 * Send effective user and group ID.  If the user was not
		 * faked, then send supplementary group IDs; otherwise
		 * discover these on the remote side.
		 */
		{
			gid_t gids[NGROUPS_MAX];
			int num, i;
			uint16v_t *ids;

			if (!remote_user) {
				num = getgroups(NGROUPS_MAX, gids);
				if (num < 0) {
					error("Can't get supplementary group IDs");
					return 1;
				}
			} else {
				num = 0;
			}

			ids = uint16v_alloc(2 + num);
			if (!ids) {
				oom_error();
				return 1;
			}

			ids->vec[0] = uid;
			ids->vec[1] = gid;

			for (i = 0; i < num; ++i)
				ids->vec[i + 2] = gids[i];

			debug("Sending IDS packet");
			if (write_enum(sd, PTYPE_IDS) < 0 || write_uint16v(sd, ids) < 0) {
				error("Can't send IDS packet");
				return 1;
			}

			uint16v_free(ids);
		}

		/*
		 * Send umask
		 */
		{
			uint16_t mask = umask(0);
			umask(mask);

			debug("Sending UMASK packet");
			if (write_uint16_packet(sd, PTYPE_UMASK, mask) < 0) {
				error("Can't send UMASK packet");
				return 1;
			}
		}

		/*
		 * Send terminal size
		 */

		is_tty = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) && isatty(STDERR_FILENO);
		debug(is_tty ? "Terminal emulation enabled" : "Terminal emulation disabled");

		if (is_tty) {
			struct winsize ws;
			ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);

			debug("Sending WINSIZE packet");
			if (write_enum(sd, PTYPE_WINSIZE) < 0 || write_winsize(sd, &ws) < 0) {
				error("Can't send WINSIZE packet");
				return 1;
			}
		}

		/*
		 * Non-blocking I/O
		 */

		if (set_nonblocking(STDOUT_FILENO, TRUE) < 0) {
			error("Can't make stdout non-blocking");
			return 1;
		}

		if (is_tty) {
			if (set_nonblocking(STDERR_FILENO, TRUE) < 0) {
				error("Can't make stderr non-blocking");
				return 1;
			}

			if (set_raw_mode() < 0)
				return 1;
		}

		/*
		 * Send command action
		 */
		debug("Sending COMMAND packet");
		if (write_enum(sd, PTYPE_COMMAND) < 0) {
			error("Can't send COMMAND packet");
			return 1;
		}

		/*
		 * Main loop
		 */
		rc = manage(is_tty);
		if (rc < 0)
			return 1;

	} else {

		/*
		 * Send (un)mount action
		 */
		if (action == PTYPE_MOUNT) {
			debug("Sending MOUNT packet");
			if (write_enum(sd, PTYPE_MOUNT) < 0) {
				error("Can't send MOUNT packet");
				return 1;
			}
		} else {
			debug("Sending UMOUNT packet");
			if (write_enum(sd, PTYPE_UMOUNT) < 0) {
				error("Can't send UMOUNT packet");
				return 1;
			}
		}

		/*
		 * Wait until the server has (un)mounted
		 */
		while (1) {
			uint16_t val;
			ptype_t type = read_enum(sd);
			switch (type) {
			case -1:
				error("Can't read packet type from socket");
				return 1;

			case PTYPE_RC:
				debug("Receiving RC packet");
				if (read_uint16(sd, &val) < 0) {
					error("Can't read RC packet from socket");
					return 1;
				}
				rc = val;
				break;

			case PTYPE_MESSAGE:
				receive_message(PTYPE_MESSAGE);
				continue;

			case PTYPE_ERROR:
				receive_message(PTYPE_ERROR);
				continue;

			default:
				errno = 0;
				error("Received packet has unexpected type (0x%02x)", type);
				return 1;
			}

			break;
		}
	}

	/*
	 * Exit with correct code
	 */

	if (rc == INTERNAL_ERROR_CODE) {
		debug("Internal server error");
		return 1;
	} else {
		debug("Return code: %d", rc);
		return rc;
	}
}
