summaryrefslogtreecommitdiff
path: root/extras/scsi_id/scsi_id.c
diff options
context:
space:
mode:
Diffstat (limited to 'extras/scsi_id/scsi_id.c')
-rw-r--r--extras/scsi_id/scsi_id.c827
1 files changed, 827 insertions, 0 deletions
diff --git a/extras/scsi_id/scsi_id.c b/extras/scsi_id/scsi_id.c
new file mode 100644
index 0000000000..d34d9284e5
--- /dev/null
+++ b/extras/scsi_id/scsi_id.c
@@ -0,0 +1,827 @@
+/*
+ * scsi_id.c
+ *
+ * Main section of the scsi_id program
+ *
+ * Copyright (C) IBM Corp. 2003
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+#include <syslog.h>
+#include <stdarg.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#include <sys/libsysfs.h>
+#include "scsi_id.h"
+
+#ifndef VERSION
+#warning No version
+#define VERSION "unknown"
+#endif
+
+/*
+ * temporary names for mknod.
+ */
+#define TMP_DIR "/tmp"
+#define TMP_PREFIX "scsi"
+
+#define CONFIG_FILE "/etc/scsi_id.config"
+
+#define MAX_NAME_LEN 72
+
+#define MAX_SERIAL_LEN 128
+
+static const char short_options[] = "bc:d:ef:gip:s:vV";
+static const struct option long_options[] = {
+ {"broken", no_argument, NULL, 'b'}, /* also per dev */
+ {"callout", required_argument, NULL, 'c'}, /* also per dev */
+ {"device", required_argument, NULL, 'd'},
+ {"stderr", no_argument, NULL, 'e'},
+ {"file", required_argument, NULL, 'f'},
+ {"good", no_argument, NULL, 'g'}, /* also per dev */
+ {"busid", no_argument, NULL, 'i'}, /* also per dev */
+ {"page", required_argument, NULL, 'p'}, /* also per dev */
+ {"devpath", required_argument, NULL, 's'},
+ {"verbose", no_argument, NULL, 'v'},
+ {"version", no_argument, NULL, 'V'},
+ {0, 0, 0, 0}
+};
+/*
+ * Just duplicate per dev options.
+ */
+static const char dev_short_options[] = "bc:gp:";
+static const struct option dev_long_options[] = {
+ {"broken", no_argument, NULL, 'b'}, /* also per dev */
+ {"callout", required_argument, NULL, 'c'}, /* also per dev */
+ {"good", no_argument, NULL, 'g'}, /* also per dev */
+ {"page", required_argument, NULL, 'p'}, /* also per dev */
+ {0, 0, 0, 0}
+};
+
+char sysfs_mnt_path[SYSFS_PATH_MAX];
+
+static int all_good;
+static char *default_callout;
+static int dev_specified;
+static int sys_specified;
+static char config_file[MAX_NAME_LEN] = CONFIG_FILE;
+static int display_bus_id;
+static int default_page_code;
+static int use_stderr;
+static int debug;
+static int hotplug_mode;
+
+void log_message (int level, const char *format, ...)
+{
+ va_list args;
+
+ if (!debug && level == LOG_DEBUG)
+ return;
+
+ va_start (args, format);
+ if (!hotplug_mode || use_stderr) {
+ vfprintf(stderr, format, args);
+ } else {
+ static int logging_init = 0;
+ if (!logging_init) {
+ openlog ("scsi_id", LOG_PID, LOG_DAEMON);
+ logging_init = 1;
+ }
+
+ vsyslog(level, format, args);
+ }
+ va_end (args);
+ return;
+}
+
+static int sysfs_get_actual_dev(const char *sysfs_path, char *dev, int len)
+{
+ dprintf("%s\n", sysfs_path);
+ strncpy(dev, sysfs_path, len);
+ strncat(dev, "/device", len);
+ if (sysfs_get_link(dev, dev, len)) {
+ if (!hotplug_mode)
+ log_message(LOG_WARNING, "%s: %s\n", dev,
+ strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+/*
+ * sysfs_is_bus: Given the sysfs_path to a device, return 1 if sysfs_path
+ * is on bus, 0 if not on bus, and < 0 on error
+ */
+static int sysfs_is_bus(const char *sysfs_path, const char *bus)
+{
+ char bus_dev_name[SYSFS_PATH_MAX];
+ char bus_id[SYSFS_NAME_LEN];
+ struct stat stat_buf;
+ ino_t dev_inode;
+
+ dprintf("%s\n", sysfs_path);
+
+ if (sysfs_get_name_from_path(sysfs_path, bus_id, SYSFS_NAME_LEN))
+ return -1;
+
+ snprintf(bus_dev_name, MAX_NAME_LEN, "%s/%s/%s/%s/%s", sysfs_mnt_path,
+ SYSFS_BUS_DIR, bus, SYSFS_DEVICES_NAME, bus_id);
+
+ if (stat(sysfs_path, &stat_buf))
+ return -1;
+ dev_inode = stat_buf.st_ino;
+
+ if (stat(bus_dev_name, &stat_buf)) {
+ if (errno == ENOENT)
+ return 0;
+ else
+ return -1;
+ }
+ if (dev_inode == stat_buf.st_ino)
+ return 1;
+ else
+ return 0;
+}
+
+static int get_major_minor(const char *devpath, int *major, int *minor)
+{
+ struct sysfs_class_device *class_dev;
+ char dev_value[SYSFS_NAME_LEN];
+ char *dev;
+
+ dprintf("%s\n", devpath);
+ class_dev = sysfs_open_class_device(devpath);
+ if (!class_dev) {
+ log_message(LOG_WARNING, "open class %s failed: %s\n", devpath,
+ strerror(errno));
+ return -1;
+ }
+
+ dev = sysfs_get_attr(class_dev, "dev");
+ if (dev)
+ strncpy(dev_value, dev, SYSFS_NAME_LEN);
+ sysfs_close_class_device(class_dev);
+ if (!dev) {
+ /*
+ * XXX This happens a lot, since sg has no dev attr.
+ * Someday change this back to a LOG_WARNING.
+ */
+ log_message(LOG_DEBUG, "%s could not get dev attribute: %s\n",
+ devpath, strerror(errno));
+ return -1;
+ }
+ dev = NULL;
+
+ dprintf("dev %s", dev_value); /* dev_value has a trailing \n */
+ if (sscanf(dev_value, "%u:%u", major, minor) != 2) {
+ log_message(LOG_WARNING, "%s: invalid dev major/minor\n",
+ devpath);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int create_tmp_dev(const char *devpath, char *tmpdev, int dev_type)
+{
+ int major, minor;
+
+ dprintf("(%s)\n", devpath);
+
+ if (get_major_minor(devpath, &major, &minor))
+ return -1;
+ snprintf(tmpdev, MAX_NAME_LEN, "%s/%s-maj%d-min%d-%u",
+ TMP_DIR, TMP_PREFIX, major, minor, getpid());
+
+ dprintf("tmpdev '%s'\n", tmpdev);
+
+ if (mknod(tmpdev, 0600 | dev_type, makedev(major, minor))) {
+ log_message(LOG_WARNING, "mknod failed: %s\n", strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int has_sysfs_prefix(const char *path, const char *prefix)
+{
+ char match[MAX_NAME_LEN];
+
+ strncpy(match, sysfs_mnt_path, MAX_NAME_LEN);
+ strncat(match, prefix, MAX_NAME_LEN);
+ if (strncmp(path, match, strlen(match)) == 0)
+ return 1;
+ else
+ return 0;
+}
+
+/*
+ * get_value:
+ *
+ * buf points to an '=' followed by a quoted string ("foo") or a string ending
+ * with a space or ','.
+ *
+ * Return a pointer to the NUL terminated string, returns NULL if no
+ * matches.
+ */
+static char *get_value(char **buffer)
+{
+ static char *quote_string = "\"\n";
+ static char *comma_string = ",\n";
+ char *val;
+ char *end;
+
+ if (**buffer == '"') {
+ /*
+ * skip leading quote, terminate when quote seen
+ */
+ (*buffer)++;
+ end = quote_string;
+ } else {
+ end = comma_string;
+ }
+ val = strsep(buffer, end);
+ if (val && end == quote_string)
+ /*
+ * skip trailing quote
+ */
+ (*buffer)++;
+
+ while (isspace(**buffer))
+ (*buffer)++;
+
+ return val;
+}
+
+static int argc_count(char *opts)
+{
+ int i = 0;
+ while (*opts != '\0')
+ if (*opts++ == ' ')
+ i++;
+ return i;
+}
+
+/*
+ * get_file_options:
+ *
+ * If vendor == NULL, find a line in the config file with only "OPTIONS=";
+ * if vendor and model are set find the first OPTIONS line in the config
+ * file that matches. Set argc and argv to match the OPTIONS string.
+ *
+ * vendor and model can end in '\n'.
+ */
+static int get_file_options(char *vendor, char *model, int *argc,
+ char ***newargv)
+{
+ char buffer[256];
+ FILE *fd;
+ char *buf;
+ char *str1;
+ char *vendor_in, *model_in, *options_in; /* read in from file */
+ int lineno;
+ int c;
+ int retval = 0;
+ static char *prog_string = "arg0";
+
+ dprintf("vendor='%s'; model='%s'\n", vendor, model);
+ fd = fopen(config_file, "r");
+ if (fd == NULL) {
+ dprintf("can't open %s\n", config_file);
+ if (errno == ENOENT) {
+ return 1;
+ } else {
+ log_message(LOG_WARNING, "can't open %s: %s\n",
+ config_file, strerror(errno));
+ return -1;
+ }
+ }
+
+ *newargv = NULL;
+ lineno = 0;
+
+ while (1) {
+ vendor_in = model_in = options_in = NULL;
+
+ buf = fgets(buffer, sizeof(buffer), fd);
+ if (buf == NULL)
+ break;
+ lineno++;
+
+ while (isspace(*buf))
+ buf++;
+
+ if (*buf == '\0')
+ /*
+ * blank or all whitespace line
+ */
+ continue;
+
+ if (*buf == '#')
+ /*
+ * comment line
+ */
+ continue;
+
+#ifdef LOTS
+ dprintf("lineno %d: '%s'\n", lineno, buf);
+#endif
+ str1 = strsep(&buf, "=");
+ if (str1 && strcasecmp(str1, "VENDOR") == 0) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = -1;
+ break;
+ }
+ vendor_in = str1;
+
+ str1 = strsep(&buf, "=");
+ if (str1 && strcasecmp(str1, "MODEL") == 0) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = -1;
+ break;
+ }
+ model_in = str1;
+ str1 = strsep(&buf, "=");
+ }
+ }
+
+ if (str1 && strcasecmp(str1, "OPTIONS") == 0) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = -1;
+ break;
+ }
+ options_in = str1;
+ }
+ dprintf("config file line %d:"
+ " vendor '%s'; model '%s'; options '%s'\n",
+ lineno, vendor_in, model_in, options_in);
+ /*
+ * Only allow: [vendor=foo[,model=bar]]options=stuff
+ */
+ if (!options_in || (!vendor_in && model_in)) {
+ log_message(LOG_WARNING,
+ "Error parsing config file line %d '%s'\n",
+ lineno, buffer);
+ retval = -1;
+ break;
+ }
+ if (vendor == NULL) {
+ if (vendor_in == NULL) {
+ dprintf("matched global option\n");
+ break;
+ }
+ } else if ((vendor_in && strncmp(vendor, vendor_in,
+ strlen(vendor_in)) == 0) &&
+ (!model_in || (strncmp(model, model_in,
+ strlen(model_in)) == 0))) {
+ /*
+ * Matched vendor and optionally model.
+ *
+ * Note: a short vendor_in or model_in can
+ * give a partial match (that is FOO
+ * matches FOOBAR).
+ */
+ dprintf("matched vendor/model\n");
+ break;
+ } else {
+ dprintf("no match\n");
+ }
+ }
+
+ if (retval == 0) {
+ if (vendor_in != NULL || model_in != NULL ||
+ options_in != NULL) {
+ /*
+ * Something matched. Allocate newargv, and store
+ * values found in options_in.
+ */
+ c = argc_count(options_in) + 2;
+ *newargv = calloc(c, sizeof(**newargv));
+ if (!*newargv) {
+ log_message(LOG_WARNING,
+ "Can't allocate memory\n");
+ retval = -1;
+ } else {
+ *argc = c;
+ c = 0;
+ (*newargv)[c] = prog_string; /* nothing */
+ for (c = 1; c < *argc; c++)
+ (*newargv)[c] = strsep(&options_in, " ");
+ }
+ } else {
+ /*
+ * No matches.
+ */
+ retval = 1;
+ }
+ }
+ fclose(fd);
+ return retval;
+}
+
+static int set_options(int argc, char **argv, const char *short_opts,
+ const struct option *long_opts, char *target,
+ char *maj_min_dev)
+{
+ int option;
+ int option_ind;
+
+ /*
+ * optind is a global extern used by getopt_long. Since we can
+ * call set_options twice (once for command line, and once for
+ * config file) we have to reset this back to 0.
+ */
+ optind = 0;
+ while (1) {
+ option = getopt_long(argc, argv, short_options, long_options,
+ &option_ind);
+ if (option == -1)
+ break;
+
+ if (optarg)
+ dprintf("option '%c' arg '%s'\n", option, optarg);
+ else
+ dprintf("option '%c'\n", option);
+
+ switch (option) {
+ case 'b':
+ all_good = 0;
+ break;
+
+ case 'c':
+ default_callout = optarg;
+ break;
+
+ case 'd':
+ dev_specified = 1;
+ strncpy(maj_min_dev, optarg, MAX_NAME_LEN);
+ break;
+
+ case 'e':
+ use_stderr = 1;
+ break;
+
+ case 'f':
+ strncpy(config_file, optarg, MAX_NAME_LEN);
+ break;
+
+ case 'g':
+ all_good = 1;
+ break;
+
+ case 'i':
+ display_bus_id = 1;
+ break;
+
+ case 'p':
+ if (strcmp(optarg, "0x80") == 0) {
+ default_page_code = 0x80;
+ } else if (strcmp(optarg, "0x83") == 0) {
+ default_page_code = 0x83;
+ } else {
+ log_message(LOG_WARNING,
+ "Unknown page code '%s'\n", optarg);
+ return -1;
+ }
+ break;
+
+ case 's':
+ sys_specified = 1;
+ strncpy(target, sysfs_mnt_path, MAX_NAME_LEN);
+ strncat(target, optarg, MAX_NAME_LEN);
+ break;
+
+ case 'v':
+ debug++;
+ break;
+
+ case 'V':
+ log_message(LOG_WARNING, "scsi_id version: %s\n",
+ VERSION);
+ exit(0);
+ break;
+
+ default:
+ log_message(LOG_WARNING,
+ "Unknown or bad option '%c' (0x%x)\n",
+ option, option);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int per_dev_options(struct sysfs_class_device *scsi_dev, int *good_bad,
+ int *page_code, char *callout)
+{
+ int retval;
+ int newargc;
+ char **newargv = NULL;
+ char *vendor;
+ char *model;
+ int option;
+ int option_ind;
+
+
+ *good_bad = all_good;
+ *page_code = default_page_code;
+ if (default_callout && (callout != default_callout))
+ strncpy(callout, default_callout, MAX_NAME_LEN);
+ else
+ callout[0] = '\0';
+
+ vendor = sysfs_get_attr(scsi_dev, "vendor");
+ if (!vendor) {
+ log_message(LOG_WARNING, "%s: no vendor attribute\n",
+ scsi_dev->name);
+ return -1;
+ }
+
+ model = sysfs_get_attr(scsi_dev, "model");
+ if (!vendor) {
+ log_message(LOG_WARNING, "%s: no model attribute\n",
+ scsi_dev->name);
+ return -1;
+ }
+
+ retval = get_file_options(vendor, model, &newargc, &newargv);
+
+ optind = 0; /* global extern, reset to 0 */
+ while (retval == 0) {
+ option = getopt_long(newargc, newargv, dev_short_options,
+ dev_long_options, &option_ind);
+ if (option == -1)
+ break;
+
+ if (optarg)
+ dprintf("option '%c' arg '%s'\n", option, optarg);
+ else
+ dprintf("option '%c'\n", option);
+
+ switch (option) {
+ case 'b':
+ *good_bad = 0;
+ break;
+
+ case 'c':
+ strncpy(callout, default_callout, MAX_NAME_LEN);
+ break;
+
+ case 'g':
+ *good_bad = 1;
+ break;
+
+ case 'p':
+ if (strcmp(optarg, "0x80") == 0) {
+ *page_code = 0x80;
+ } else if (strcmp(optarg, "0x83") == 0) {
+ *page_code = 0x83;
+ } else {
+ log_message(LOG_WARNING,
+ "Unknown page code '%s'\n", optarg);
+ retval = -1;
+ }
+ break;
+
+ default:
+ log_message(LOG_WARNING,
+ "Unknown or bad option '%c' (0x%x)\n",
+ option, option);
+ retval = -1;
+ break;
+ }
+ }
+
+ if (newargv)
+ free(newargv);
+ return retval;
+}
+
+/*
+ * scsi_id: try to get an id, if one is found, printf it to stdout.
+ * returns a value passed to exit() - 0 if printed an id, else 1. This
+ * could be expanded, for example, if we want to report a failure like no
+ * memory etc. return 2, and return 1 for expected cases (like broken
+ * device found) that do not print an id.
+ */
+static int scsi_id(const char *target_path, char *maj_min_dev)
+{
+ int retval;
+ int dev_type = 0;
+ char full_dev_path[MAX_NAME_LEN];
+ char serial[MAX_SERIAL_LEN];
+ struct sysfs_class_device *scsi_dev; /* of scsi_device full_dev_path */
+ int good_dev;
+ int page_code;
+ char callout[MAX_NAME_LEN];
+
+ dprintf("target_path %s\n", target_path);
+
+ /*
+ * Ugly: depend on the sysfs path to tell us whether this is a
+ * block or char device. This should probably be encoded in the
+ * "dev" along with the major/minor.
+ */
+ if (has_sysfs_prefix(target_path, "/block")) {
+ dev_type = S_IFBLK;
+ } else if (has_sysfs_prefix(target_path, "/class")) {
+ dev_type = S_IFCHR;
+ } else {
+ if (!hotplug_mode) {
+ log_message(LOG_WARNING,
+ "Non block or class device '%s'\n",
+ target_path);
+ return 1;
+ } else {
+ /*
+ * Expected in some cases.
+ */
+ dprintf("Non block or class device\n");
+ return 0;
+ }
+ }
+
+ if (sysfs_get_actual_dev(target_path, full_dev_path, MAX_NAME_LEN))
+ return 1;
+
+ dprintf("full_dev_path %s\n", full_dev_path);
+
+ /*
+ * Allow only scsi devices (those that have a matching device
+ * under /bus/scsi/devices).
+ *
+ * Other block devices can support SG IO, but only ide-cd does, so
+ * for now, don't bother with anything else.
+ */
+ retval = sysfs_is_bus(full_dev_path, "scsi");
+ if (retval == 0) {
+ if (hotplug_mode)
+ /*
+ * Expected in some cases.
+ */
+ dprintf("%s is not a scsi device\n", target_path);
+ else
+ log_message(LOG_WARNING, "%s is not a scsi device\n",
+ target_path);
+ return 1;
+ } else if (retval < 0) {
+ log_message(LOG_WARNING, "sysfs_is_bus failed: %s\n",
+ strerror(errno));
+ return 1;
+ }
+
+ /*
+ * mknod a temp dev to communicate with the device.
+ */
+ if (!dev_specified && create_tmp_dev(target_path, maj_min_dev,
+ dev_type)) {
+ dprintf("create_tmp_dev failed\n");
+ return 1;
+ }
+
+ scsi_dev = sysfs_open_class_device(full_dev_path);
+ if (!scsi_dev) {
+ log_message(LOG_WARNING, "open class %s failed: %s\n",
+ full_dev_path, strerror(errno));
+ return 1;
+ }
+
+ /*
+ * Get any per device (vendor + model) options from the config
+ * file.
+ */
+ retval = per_dev_options(scsi_dev, &good_dev, &page_code, callout);
+ dprintf("per dev options: good %d; page code 0x%x; callout '%s'\n",
+ good_dev, page_code, callout);
+
+ if (!good_dev) {
+ retval = 1;
+ } else if (callout[0] != '\0') {
+ /*
+ * exec vendor callout, pass it only the "name" to be used
+ * for error messages, and the dev to open.
+ *
+ * This won't work if we need to pass on the original
+ * command line (when not hotplug mode) since the option
+ * parsing and per dev parsing modify the argv's.
+ *
+ * XXX Not implemented yet. And not fully tested ;-)
+ */
+ retval = 1;
+ } else if (scsi_get_serial(scsi_dev, maj_min_dev, page_code,
+ serial, MAX_SERIAL_LEN)) {
+ retval = 1;
+ } else {
+ retval = 0;
+ }
+ if (!retval) {
+ if (display_bus_id)
+ printf("%s ", scsi_dev->name);
+ printf("%s", serial);
+ if (!hotplug_mode)
+ printf("\n");
+ dprintf("%s\n", serial);
+ retval = 0;
+ }
+ fflush(stdout);
+ sysfs_close_class_device(scsi_dev);
+
+ if (!dev_specified)
+ unlink(maj_min_dev);
+
+ return retval;
+}
+
+int main(int argc, char **argv)
+{
+ int retval;
+ char *devpath;
+ char target_path[MAX_NAME_LEN];
+ char maj_min_dev[MAX_NAME_LEN];
+ int newargc;
+ char **newargv;
+
+ if (getenv("DEBUG"))
+ debug++;
+
+ if ((argc == 2) && (argv[1][0] != '-')) {
+ hotplug_mode = 1;
+ dprintf("hotplug assumed\n");
+ }
+
+ dprintf("argc is %d\n", argc);
+ if (sysfs_get_mnt_path(sysfs_mnt_path, MAX_NAME_LEN)) {
+ log_message(LOG_WARNING, "sysfs_get_mnt_path failed: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ if (hotplug_mode) {
+ /*
+ * There is a kernel race creating attributes, if called
+ * directly, uncomment the sleep.
+ */
+ /* sleep(1); */
+
+ devpath = getenv("DEVPATH");
+ if (!devpath) {
+ log_message(LOG_WARNING, "DEVPATH is not set\n");
+ exit(1);
+ }
+ sys_specified = 1;
+
+ strncpy(target_path, sysfs_mnt_path, MAX_NAME_LEN);
+ strncat(target_path, devpath, MAX_NAME_LEN);
+ } else {
+ if (set_options(argc, argv, short_options, long_options,
+ target_path, maj_min_dev) < 0)
+ exit(1);
+ }
+
+ /*
+ * Override any command line options set via the config file. This
+ * is the only way to set options when in hotplug mode.
+ */
+ newargv = NULL;
+ retval = get_file_options(NULL, NULL, &newargc, &newargv);
+ if (retval < 0) {
+ exit(1);
+ } else if (newargv && (retval == 0)) {
+ if (set_options(newargc, newargv, short_options, long_options,
+ target_path, maj_min_dev) < 0)
+ exit(1);
+ free(newargv);
+ }
+
+ if (!sys_specified) {
+ log_message(LOG_WARNING, "-s must be specified\n");
+ exit(1);
+ }
+
+ retval = scsi_id(target_path, maj_min_dev);
+ exit(retval);
+}