/********************************************************************
 *
 *		L I N U X   E J E C T   C O M M A N D
 *
 *                by Jeff Tranter (tranter@pobox.com)
 *
 ********************************************************************
 *
 * Copyright (C) 1994-1999 Jeff Tranter (tranter@pobox.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 ********************************************************************
 *
 * See the man page for a description of what this program does and what
 * the requirements to run it are.
 *
 */

#ifndef DEFAULTDEVICE
#error DEFAULTDEVICE not set, check Makefile
#endif

#include <linux/version.h>
/* handy macro found in 2.1 kernels, but not in older ones */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
#endif

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#ifdef GETOPTLONG
#include <getopt.h>
#endif /* GETOPTLONG */
#include <errno.h>
#include <regex.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/mtio.h>
#include <linux/cdrom.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,1,0)
#include <linux/ucdrom.h>
#endif
#include <linux/fd.h>
#include <scsi/scsi.h>
#include <scsi/sg.h>
#include <scsi/scsi_ioctl.h>

/* Global Variables */
const char *version = "2.0.2"; /* program version */
int a_option = 0; /* command flags and arguments */
int c_option = 0;
int d_option = 0;
int f_option = 0;
int h_option = 0;
int n_option = 0;
int q_option = 0;
int r_option = 0;
int s_option = 0;
int t_option = 0;
int v_option = 0;
int a_arg = 0;
int c_arg = 0;
char *programName; /* used in error messages */

/* These are the basenames of devices which can have multiple
   partitions per device. */
const char *partitionDevice[] = {
  "hd", 
  "sd", 
  "xd", 
  "dos_hd", 
  "mfm", 
  "ad", 
  "ed", 
  "ftl", 
  "pd", 
  0};


/* Display command usage on standard error and exit. */
static void usage()
{
  fprintf(stderr,
"Eject version %s by Jeff Tranter (tranter@pobox.com)\n"
"Usage:\n"
"  eject -h                             -- display command usage and exit\n"
"  eject [-vnrsfq] [<name>]             -- eject device\n"
"  eject [-vn] -d                       -- display default device\n"
"  eject [-vn] -a on|off|1|0 [<name>]   -- turn auto-eject feature on or off\n"
"  eject [-vn] -c <slot> [<name>]       -- switch discs on a CD-ROM changer\n"
"  eject [-vn] -t [<name>]              -- close tray\n"
"Options:\n"
"  -v                                   -- enable verbose output\n"
"  -n                                   -- don't eject, just show device found\n"
"  -r                                   -- eject CD-ROM\n"
"  -s                                   -- eject SCSI device\n"
"  -f                                   -- eject floppy\n"
"  -q                                   -- eject tape\n"
#ifdef GETOPTLONG
"Long options:\n"
"  -h --help   -v --verbose      -d --default\n"
"  -a --auto   -c --changerslot  -t --trayclose\n"
"  -r --cdrom  -s --scsi         -f --floppy\n"
"  -q --tape   -n --noop\n"
#endif /* GETOPTLONG */
"Parameter <name> can be a device file or a mount point.\n"
"If omitted, name defaults to `%s'.\n"
"By default tries -r, -s, -f, and -q in order until success.\n",
	  version, DEFAULTDEVICE); 
  exit(1);
}


/* Handle command line options. */
static void parse_args(int argc, char **argv, char **device)
{
  const char *flags = "a:c:dfhnqrstv";
#ifdef GETOPTLONG
  static struct option long_options[] =
  {
    {"help",        no_argument,       NULL, 'h'},
    {"verbose",     no_argument,       NULL, 'v'},
    {"default",     no_argument,       NULL, 'd'},
    {"auto",        required_argument, NULL, 'a'},
    {"changerslot", required_argument, NULL, 'c'},
    {"trayclose",   no_argument,       NULL, 't'},
    {"noop",        no_argument,       NULL, 'n'},
    {"cdrom",       no_argument,       NULL, 'r'},
    {"scsi",        no_argument,       NULL, 's'},
    {"floppy",      no_argument,       NULL, 'f'},
    {"tape",        no_argument,       NULL, 'q'},
    {0, 0, 0, 0}
  };
  int option_index;
#endif /* GETOPTLONG */
  int c;

#ifdef GETOPTLONG
  while ((c = getopt_long(argc, argv, flags, long_options, &option_index)) != EOF) {
#else
  while ((c = getopt(argc, argv, flags)) != EOF) {
#endif /* GETOPTLONG */
    switch (c) {
    case 'a':
      a_option = 1;
      if (!strcmp(optarg, "0"))
	a_arg = 0;
      else if (!strcmp(optarg, "off"))
	a_arg = 0;
      else if (!strcmp(optarg, "1"))
	a_arg = 1;
      else if (!strcmp(optarg, "on"))
	a_arg = 1;
      else {
	fprintf(stderr, "%s: invalid argument to --auto/-a option\n", programName);
	exit(1);
      }
      break;
    case 'c':
      c_option = 1;
      /* atoi() returns 0 on error, so "0" must be parsed separately */  
      if (!strcmp(optarg, "0"))
	c_arg = 0;
      else {           
	c_arg = atoi(optarg);
	if (c_arg <= 0) {
	  fprintf(stderr, "%s: invalid argument to --changerslot/-c option\n", programName);
	  exit(1);
	}
      }
      break;
    case 'd':
      d_option = 1;
      break;
    case 'f':
      f_option = 1;
      break;
    case 'h':
      usage();
      exit(0);
      break;
    case 'n':
      n_option = 1;
      break;
    case 'q':
      q_option = 1;
      break;
    case 'r':
      r_option = 1;
      break;
    case 's':
      s_option = 1;
      break;
    case 't':
      t_option = 1;
      break;
    case 'v':
      v_option = 1;
      break;
    case '?':
      exit(1);
      break;
    }
  }
  /* check for a single additional argument */
  if ((argc - optind) > 1) {
    fprintf(stderr, "%s: too many arguments\n", programName);
    exit(1);
  }
  if ((argc - optind) == 1) { /* one argument */
    *device = strdup(argv[optind]);
  }
}


/* Return 1 if file/device exists, 0 otherwise. */
static int FileExists(const char *name)
{
  int fd;

  fd = open(name, O_RDONLY|O_NONBLOCK);
  if (fd == -1) {
    return 0;
  } else {
    close(fd);
    return 1;
  }
}


/*
Given name, such as foo, see if any of the following exist:
- foo (if foo starts with '.' or '/')
- /dev/foo
- /mnt/foo
- /dev/dsk/foo
- /dev/rdsk/foo
- /mnt/foo
- ./foo
If found, return the full path. If not found, return 0.
Returns pointer to dynamically allocated string.
*/
static char *FindDevice(const char *name)
{
  char *buf;

  buf = (char *) malloc(strlen(name)+9);

  if ((name[0] == '.') || (name[0] == '/')) {
    strcpy(buf, name);
    if (FileExists(buf))
      return buf;
  }

  strcpy(buf, "/dev/");
  strcat(buf, name);
  if (FileExists(buf))
      return buf;

  strcpy(buf, "/mnt/");
  strcat(buf, name);
  if (FileExists(buf))
      return buf;

  /* for systems that use devfs such as Solaris and probably Linux in future */
  strcpy(buf, "/dev/rdsk/");
  strcat(buf, name);
  if (FileExists(buf))
      return buf;

  strcpy(buf, "/dev/dsk/");
  strcat(buf, name);
  if (FileExists(buf))
      return buf;

  strcpy(buf, "./");
  strcat(buf, name);
  if (FileExists(buf))
      return buf;

  free(buf);
  buf = 0;
  return 0;
}


/* Set or clear auto-eject mode. */
static void AutoEject(int fd, int onOff)
{
  int status;

  status = ioctl(fd, CDROMEJECT_SW, onOff);
  if (status != 0) {
    fprintf(stderr, "%s: CD-ROM auto-eject command failed: %s\n", programName, strerror(errno));
    exit(1);
  }
}


/*
Changer select. CDROM_SELECT_DISC is preferred, older kernels used
CDROMLOADFROMSLOT.
*/
static void ChangerSelect(int fd, int slot)
{
  int status;

#ifdef CDROM_SELECT_DISC
  status = ioctl(fd, CDROM_SELECT_DISC, slot);
  if (status < 0) {
    fprintf(stderr, "%s: CD-ROM select disc command failed: %s\n", programName, strerror(errno));
    exit(1);
  }
#elif defined CDROMLOADFROMSLOT
  status = ioctl(fd, CDROMLOADFROMSLOT, slot);
  if (status != 0) {
    fprintf(stderr, "%s: CD-ROM load from slot command failed: %s\n", programName, strerror(errno));
    exit(1);
  }
#else
    fprintf(stderr, "%s: IDE/ATAPI CD-ROM changer not supported by this kernel\n", programName);
#endif
}


/*
Close tray. Not supported by older kernels.
*/
static void CloseTray(int fd)
{
  int status;

#ifdef CDROMCLOSETRAY
  status = ioctl(fd, CDROMCLOSETRAY);
  if (status != 0) {
    fprintf(stderr, "%s: CD-ROM tray close command failed: %s\n", programName, strerror(errno));
    exit(1);
  }
#else
    fprintf(stderr, "%s: CD-ROM tray close command not supported by this kernel\n", programName);
#endif
}


/*
Eject using CDROMEJECT ioctl. Return 1 if successful, 0 otherwise.
*/
static int EjectCdrom(int fd)
{
  int status;

  status = ioctl(fd, CDROMEJECT);
  return (status == 0);
}


/*
Eject using SCSI commands. Return 1 if successful, 0 otherwise.
*/
static int EjectScsi(int fd)
{
  int status;
  struct sdata {
    int  inlen;
    int  outlen;
    char cmd[256];
  } scsi_cmd;

  scsi_cmd.inlen  = 0;
  scsi_cmd.outlen = 0;
  scsi_cmd.cmd[0] = ALLOW_MEDIUM_REMOVAL;
  scsi_cmd.cmd[1] = 0;
  scsi_cmd.cmd[2] = 0;
  scsi_cmd.cmd[3] = 0;
  scsi_cmd.cmd[4] = 0;
  scsi_cmd.cmd[5] = 0;
  status = ioctl(fd, SCSI_IOCTL_SEND_COMMAND, (void *)&scsi_cmd);
  if (status != 0)
    return 0;

  scsi_cmd.inlen  = 0;
  scsi_cmd.outlen = 0;
  scsi_cmd.cmd[0] = START_STOP;
  scsi_cmd.cmd[1] = 0;
  scsi_cmd.cmd[2] = 0;
  scsi_cmd.cmd[3] = 0;
  scsi_cmd.cmd[4] = 1;
  scsi_cmd.cmd[5] = 0;
  status = ioctl(fd, SCSI_IOCTL_SEND_COMMAND, (void *)&scsi_cmd);
  if (status != 0)
    return 0;

  scsi_cmd.inlen  = 0;
  scsi_cmd.outlen = 0;
  scsi_cmd.cmd[0] = START_STOP;
  scsi_cmd.cmd[1] = 0;
  scsi_cmd.cmd[2] = 0;
  scsi_cmd.cmd[3] = 0;
  scsi_cmd.cmd[4] = 2;
  scsi_cmd.cmd[5] = 0;
  status = ioctl(fd, SCSI_IOCTL_SEND_COMMAND, (void *)&scsi_cmd);
  return (status == 0);
}


/*
Eject using FDEJECT ioctl. Return 1 if successful, 0 otherwise.
*/
static int EjectFloppy(int fd)
{
  int status;

  status = ioctl(fd, FDEJECT);
  return (status == 0);
}


/*
Eject using tape ioctl. Return 1 if successful, 0 otherwise.
*/
static int EjectTape(int fd)
{
  int status;
  struct mtop op;

  op.mt_op = MTOFFL; /* rewind and eject */
  op.mt_count = 0;   /* not used */
  status = ioctl(fd, MTIOCTOP, &op);
  return (status == 0);
}


/* Unmount a device. */
static void Unmount(const char *fullName)
{
  int status;

  switch (fork()) {
  case 0: /* child */
    seteuid(getuid()); /* reduce likelyhood of security holes when running setuid */
    execl("/bin/umount", "/bin/umount", fullName, NULL);
    fprintf(stderr, "%s: unable to exec /bin/umount of `%s': %s\n",
	    programName, fullName, strerror(errno));
    exit(1);
    break;
  case -1:
    fprintf(stderr, "%s: unable to fork: %s\n", programName, strerror(errno));
    break;
  default: /* parent */
    wait(&status);
    if (WIFEXITED(status) == 0) {
      fprintf(stderr, "%s: unmount of `%s' did not exit normally\n", programName, fullName);
      exit(1);
    }
    if (WEXITSTATUS(status) != 0) {
      fprintf(stderr, "%s: unmount of `%s' failed\n", programName, fullName);
      exit(1);
    }
    break;
  }
}


/* Open a device file. */
static int OpenDevice(const char *fullName)
{
  int fd = open(fullName, O_RDONLY|O_NONBLOCK);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s'\n", programName, fullName);
    exit(1);
  }
  return fd;
}


/*
See if device has been mounted by looking in /etc/mtab.  If so, set
device name and mount point name, and return 1, otherwise return 0.
*/
static int MountedDevice(const char *name, char **mountName, char **deviceName)
{
  FILE *fp;
  char line[1024];
  char s1[1024];
  char s2[1024];
  int rc;

  fp = fopen("/etc/mtab", "r");
  if (fp == NULL) {
    fprintf(stderr, "%s: unable to open /etc/mtab: %s\n", programName, strerror(errno));
    exit(1);
  }

  while (fgets(line, sizeof(line), fp) != 0) {
    rc = sscanf(line, "%1023s %1023s", s1, s2);
    if (rc >= 2 && ((strcmp(s1, name) == 0) || (strcmp(s2, name) == 0))) {
      fclose(fp);
      *deviceName = strdup(s1);
      *mountName = strdup(s2);
      return 1;
    }
  }
  *deviceName = 0;
  *mountName = 0;
  fclose(fp);
  return 0;
}


/*
See if device can be mounted by looking in /etc/fstab.
If so, set device name and mount point name, and return 1,
otherwise return 0.
*/
static int MountableDevice(const char *name, char **mountName, char **deviceName)
{
  FILE *fp;
  char line[1024];
  char s1[1024];
  char s2[1024];
  int rc;

  fp = fopen("/etc/fstab", "r");
  if (fp == NULL) {
    fprintf(stderr, "%s: unable to open /etc/fstab: %s\n", programName, strerror(errno));
    exit(1);
  }
  
  while (fgets(line, sizeof(line), fp) != 0) {
    rc = sscanf(line, "%1023s %1023s", s1, s2);
    if (rc >= 2 && strcmp(s2, name) == 0) {
      fclose(fp);
      *deviceName = strdup(s1);
      *mountName = strdup(s2);
      return 1;
    }
  }
  fclose(fp);
  return 0;
}


/*
Step through /etc/mtab and unmount all devices that match a regular
expression.
*/
static void UnmountDevices(const char *pattern)
{
  regex_t preg;
  FILE *fp;
  char s1[1024];
  char s2[1024];
  char line[1024];
  int status;

  regcomp(&preg, pattern, REG_EXTENDED);

  fp = fopen("/etc/mtab", "r");
  if (fp == NULL) {
    fprintf(stderr, "%s: unable to open /etc/mtab: %s\n", programName, strerror(errno));
    exit(1);
  }
  
  while (fgets(line, sizeof(line), fp) != 0) {
    status = sscanf(line, "%1023s %1023s", s1, s2);
    if (status >= 2) {
      status = regexec(&preg, s1, 0, 0, 0);
      if (status == 0) {
	if (v_option)
	  printf("%s: unmounting `%s'\n", programName, s1);
	Unmount(s1);
	regfree(&preg);
      }
    }
  }
  fclose(fp);
}


/* Check if name is a symbolic link. If so, return what it points to. */
static char *SymLink(const char *name)
{
  int status;
  char s1[1024];
  char s2[1024];
  char *s3;

  status = readlink(name, s1, sizeof(s1));

  if (status == -1)
    return 0;

  s1[status] = 0;
  if (s1[0] == '/') { /* absolute link */
    return strdup(s1);
  } else { /* relative link, add base name */
    strncpy(s2, name, sizeof(s2)-1);
    s3 = strrchr(s2, '/');
    if (s3 != 0) {
      s3[1] = 0;
      strcat(s2, s1);
    }
  }
  return strdup(s2);
}


/*
Given a name, see if it matches a pattern for a device that can have
multiple partitions.  If so, return a regular expression that matches
partitions for that device, otherwise return 0.
*/
static char *MultiplePartitions(const char *name)
{
  int i = 0;
  int status;
  regex_t preg;
  char pattern[256];
  char *result = 0;

  for (i = 0; partitionDevice[i] != 0; i++) {
    /* look for ^/dev/foo[a-z]([0-9]?[0-9])?$, e.g. /dev/hda1 */
    strcpy(pattern, "^/dev/");
    strcat(pattern, partitionDevice[i]);
    strcat(pattern, "[a-z]([0-9]?[0-9])?$");
    regcomp(&preg, pattern, REG_EXTENDED|REG_NOSUB);
    status = regexec(&preg, name, 1, 0, 0);
    regfree(&preg);
    if (status == 0) {
      result = (char *) malloc(strlen(name) + 25);
      strcpy(result, name);
      result[strlen(partitionDevice[i]) + 6] = 0;
      strcat(result, "([0-9]?[0-9])?$");
      if (v_option)
	printf("%s: `%s' is a multipartition device\n", programName, name);
      return result;
    }
  }
  if (v_option)
    printf("%s: `%s' is not a multipartition device\n", programName, name);
  return 0;
}


/* main program */
int main(int argc, char **argv)
{
  const char *defaultDevice = DEFAULTDEVICE;  /* default if no name passed by user */
  int worked = 0;    /* set to 1 when successfully ejected */
  char *device = 0;  /* name passed from user */
  char *fullName;    /* expanded name */
  char *deviceName;  /* name of device */
  char *linkName;    /* name of device's symbolic link */
  char *mountName;   /* name of device's mount point */
  int fd;            /* file descriptor for device */
  int mounted = 0;   /* true if device is mounted */
  int mountable = 0; /* true if device is in /etc/fstab */
  char *pattern;     /* regex for device if multiple partitions */

  /* program name is global variable used by other procedures */
  programName = strdup(argv[0]);

  /* parse the command line arguments */
  parse_args(argc, argv, &device);

  /* handle -d option */
  if (d_option) {
    printf("%s: default device: `%s'\n", programName, defaultDevice);
    exit(0);
  }

  /* if no device, use default */
  if (device == 0) {
    device = strdup(defaultDevice);
    if (v_option)
      printf("%s: using default device `%s'\n", programName, device);
  }

  /* Strip any trailing slash from name in case user used bash/tcsh
     style filename completion (e.g. /mnt/cdrom/) */
  if (device[strlen(device)-1] == '/')
    device[strlen(device)-1] = 0;

  if (v_option)
    printf("%s: device name is `%s'\n", programName, device);

  /* figure out full device or mount point name */
  fullName = FindDevice(device);
  if (fullName == 0) {
    fprintf(stderr, "%s: unable to find or open device for: `%s'\n", programName, device);
    exit(1);
  }
  if (v_option)
    printf("%s: expanded name is `%s'\n", programName, fullName);

  /* check for a symbolic link */
  linkName = SymLink(fullName);
  if (linkName != 0) {
    if (v_option)
      printf("%s: `%s' is a link to `%s'\n", programName, fullName, linkName);
    free(fullName);
    fullName = strdup(linkName);
    free(linkName);
    linkName = 0;
  }

  /* if mount point, get device name */
  mounted = MountedDevice(fullName, &mountName, &deviceName);
  if (v_option) {
    if (mounted)
      printf("%s: `%s' is mounted at `%s'\n", programName, deviceName, mountName);
    else
      printf("%s: `%s' is not mounted\n", programName, fullName);
  }
  if (!mounted) {
    deviceName = strdup(fullName);
  }

  /* if not currently mounted, see if it is a possible mount point */
  if (!mounted) {
    mountable = MountableDevice(fullName, &mountName, &deviceName);
    if (v_option) {
      if (mountable)
	printf("%s: `%s' can be mounted at `%s'\n", programName, deviceName, mountName);
      else
	printf("%s: `%s' is not a mount point\n", programName, fullName);
    }
  }

  /* handle -n option */
  if (n_option) {
    printf("%s: device is `%s'\n", programName, deviceName);
    if (v_option)
      printf("%s: exiting due to -n/--noop option\n", programName);
    exit(0);
  }

  /* handle -a option */
  if (a_option) {
    if (v_option) {
      if (a_arg)
	printf("%s: enabling auto-eject mode for `%s'\n", programName, deviceName);
      else
	printf("%s: disabling auto-eject mode for `%s'\n", programName, deviceName);
    }
    fd = OpenDevice(deviceName);
    AutoEject(fd, a_arg);
    exit(0);
  }

  /* handle -t option */
  if (t_option) {
    if (v_option)
      printf("%s: closing tray\n", programName);
    fd = OpenDevice(deviceName);
    CloseTray(fd);
    exit(0);
  }

  /* unmount device if mounted */
  if (mounted) {
    if (v_option)
      printf("%s: unmounting `%s'\n", programName, deviceName);
    Unmount(deviceName);
  }

  /* if it is a multipartition device, unmount any other partitions on
     the device */
  pattern = MultiplePartitions(deviceName);
  if (pattern != 0)
    UnmountDevices(pattern);

  /* handle -c option */
  if (c_option) {
    if (v_option)
      printf("%s: selecting CD-ROM disc #%d\n", programName, c_arg);
    fd = OpenDevice(deviceName);
    ChangerSelect(fd, c_arg);
    exit(0);
  }

  /* if user did not specify type of eject, try all four methods */
  if ((r_option + s_option + f_option + q_option) == 0) {
    r_option = s_option = f_option = q_option = 1;
  }

  /* open device */
  fd = OpenDevice(deviceName);

  /* try various methods of ejecting until it works */
  if (r_option) {
    if (v_option)
      printf("%s: trying to eject `%s' using CD-ROM eject command\n", programName, deviceName);
    worked = EjectCdrom(fd);
    if (v_option) {
      if (worked)
	printf("%s: CD-ROM eject command succeeded\n", programName);
      else
	printf("%s: CD-ROM eject command failed\n", programName);
    }
  }

  if (s_option && !worked) {
    if (v_option)
      printf("%s: trying to eject `%s' using SCSI commands\n", programName, deviceName);
    worked = EjectScsi(fd);
    if (v_option) {
      if (worked)
	printf("%s: SCSI eject succeeded\n", programName);
      else
	printf("%s: SCSI eject failed\n", programName);
    }
  }

  if (f_option && !worked) {
    if (v_option)
      printf("%s: trying to eject `%s' using floppy eject command\n", programName, deviceName);
    worked = EjectFloppy(fd);
    if (v_option) {
      if (worked)
	printf("%s: floppy eject command succeeded\n", programName);
      else
	printf("%s: floppy eject command failed\n", programName);
    }
  }

  if (q_option && !worked) {
    if (v_option)
      printf("%s: trying to eject `%s' using tape offline command\n", programName, deviceName);
    worked = EjectTape(fd);
    if (v_option) {
      if (worked)
	printf("%s: tape offline command succeeded\n", programName);
      else
	printf("%s: tape offline command failed\n", programName);
    }
  }
  
  if (!worked) {
    fprintf(stderr, "%s: unable to eject, last error: %s\n", programName, strerror(errno));
    exit(1);
  }
  
  /* cleanup */
  close(fd);
  free(device);
  free(deviceName);
  free(fullName);
  free(linkName);
  free(mountName);
  free(pattern);
  exit(0);
}
