summaryrefslogtreecommitdiffstats
path: root/sys-utils/rfkill.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sys-utils/rfkill.c751
1 files changed, 751 insertions, 0 deletions
diff --git a/sys-utils/rfkill.c b/sys-utils/rfkill.c
new file mode 100644
index 0000000..a93e8ba
--- /dev/null
+++ b/sys-utils/rfkill.c
@@ -0,0 +1,751 @@
+/*
+ * /dev/rfkill userspace tool
+ *
+ * Copyright 2009 Johannes Berg <johannes@sipsolutions.net>
+ * Copyright 2009 Marcel Holtmann <marcel@holtmann.org>
+ * Copyright 2009 Tim Gardner <tim.gardner@canonical.com>
+ * Copyright 2017 Sami Kerola <kerolasa@iki.fi>
+ * Copyright (C) 2017 Karel Zak <kzak@redhat.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <ctype.h>
+#include <getopt.h>
+#include <libsmartcols.h>
+#include <linux/rfkill.h>
+#include <poll.h>
+#include <sys/syslog.h>
+#include <sys/time.h>
+
+#include "c.h"
+#include "closestream.h"
+#include "nls.h"
+#include "optutils.h"
+#include "pathnames.h"
+#include "strutils.h"
+#include "timeutils.h"
+#include "widechar.h"
+#include "xalloc.h"
+
+
+/*
+ * NFC supported by kernel since v3.10 (year 2013); FM and another types are from
+ * year 2009 (2.6.33) or older.
+ */
+#ifndef RFKILL_TYPE_NFC
+# ifndef RFKILL_TYPE_FM
+# define RFKILL_TYPE_FM RFKILL_TYPE_GPS + 1
+# endif
+# define RFKILL_TYPE_NFC RFKILL_TYPE_FM + 1
+# undef NUM_RFKILL_TYPES
+# define NUM_RFKILL_TYPES RFKILL_TYPE_NFC + 1
+#endif
+
+struct rfkill_type_str {
+ enum rfkill_type type; /* ID */
+ const char *name; /* generic name */
+ const char *desc; /* human readable name */
+};
+
+static const struct rfkill_type_str rfkill_type_strings[] = {
+ { .type = RFKILL_TYPE_ALL, .name = "all" },
+ { .type = RFKILL_TYPE_WLAN, .name = "wlan", .desc = "Wireless LAN" },
+ { .type = RFKILL_TYPE_WLAN, .name = "wifi" }, /* alias */
+ { .type = RFKILL_TYPE_BLUETOOTH, .name = "bluetooth", .desc = "Bluetooth" },
+ { .type = RFKILL_TYPE_UWB, .name = "uwb", .desc = "Ultra-Wideband" },
+ { .type = RFKILL_TYPE_UWB, .name = "ultrawideband" }, /* alias */
+ { .type = RFKILL_TYPE_WIMAX, .name = "wimax", .desc = "WiMAX" },
+ { .type = RFKILL_TYPE_WWAN, .name = "wwan", .desc = "Wireless WAN" },
+ { .type = RFKILL_TYPE_GPS, .name = "gps", .desc = "GPS" },
+ { .type = RFKILL_TYPE_FM, .name = "fm", .desc = "FM" },
+ { .type = RFKILL_TYPE_NFC, .name = "nfc", .desc = "NFC" },
+ { .type = NUM_RFKILL_TYPES, .name = NULL }
+};
+
+struct rfkill_id {
+ union {
+ enum rfkill_type type;
+ uint32_t index;
+ };
+ enum {
+ RFKILL_IS_INVALID,
+ RFKILL_IS_TYPE,
+ RFKILL_IS_INDEX,
+ RFKILL_IS_ALL
+ } result;
+};
+
+/* supported actions */
+enum {
+ ACT_LIST,
+ ACT_HELP,
+ ACT_EVENT,
+ ACT_BLOCK,
+ ACT_UNBLOCK,
+
+ ACT_LIST_OLD
+};
+
+static char *rfkill_actions[] = {
+ [ACT_LIST] = "list",
+ [ACT_HELP] = "help",
+ [ACT_EVENT] = "event",
+ [ACT_BLOCK] = "block",
+ [ACT_UNBLOCK] = "unblock"
+};
+
+/* column IDs */
+enum {
+ COL_DEVICE,
+ COL_ID,
+ COL_TYPE,
+ COL_DESC,
+ COL_SOFT,
+ COL_HARD
+};
+
+/* column names */
+struct colinfo {
+ const char *name; /* header */
+ double whint; /* width hint (N < 1 is in percent of termwidth) */
+ int flags; /* SCOLS_FL_* */
+ const char *help;
+};
+
+/* columns descriptions */
+static const struct colinfo infos[] = {
+ [COL_DEVICE] = {"DEVICE", 0, 0, N_("kernel device name")},
+ [COL_ID] = {"ID", 2, SCOLS_FL_RIGHT, N_("device identifier value")},
+ [COL_TYPE] = {"TYPE", 0, 0, N_("device type name that can be used as identifier")},
+ [COL_DESC] = {"TYPE-DESC", 0, 0, N_("device type description")},
+ [COL_SOFT] = {"SOFT", 0, SCOLS_FL_RIGHT, N_("status of software block")},
+ [COL_HARD] = {"HARD", 0, SCOLS_FL_RIGHT, N_("status of hardware block")}
+};
+
+static int columns[ARRAY_SIZE(infos) * 2];
+static size_t ncolumns;
+
+struct control {
+ struct libscols_table *tb;
+ unsigned int
+ json:1,
+ no_headings:1,
+ raw:1;
+};
+
+static int column_name_to_id(const char *name, size_t namesz)
+{
+ size_t i;
+
+ assert(name);
+
+ for (i = 0; i < ARRAY_SIZE(infos); i++) {
+ const char *cn = infos[i].name;
+
+ if (!strncasecmp(name, cn, namesz) && !*(cn + namesz))
+ return i;
+ }
+ warnx(_("unknown column: %s"), name);
+ return -1;
+}
+
+static int get_column_id(size_t num)
+{
+ assert(num < ncolumns);
+ assert(columns[num] < (int)ARRAY_SIZE(infos));
+ return columns[num];
+}
+
+static const struct colinfo *get_column_info(int num)
+{
+ return &infos[get_column_id(num)];
+}
+
+static int string_to_action(const char *str)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(rfkill_actions); i++)
+ if (strcmp(str, rfkill_actions[i]) == 0)
+ return i;
+
+ return -EINVAL;
+}
+
+static int rfkill_ro_open(int nonblock)
+{
+ int fd;
+
+ fd = open(_PATH_DEV_RFKILL, O_RDONLY);
+ if (fd < 0) {
+ warn(_("cannot open %s"), _PATH_DEV_RFKILL);
+ return -errno;
+ }
+
+ if (nonblock && fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
+ warn(_("cannot set non-blocking %s"), _PATH_DEV_RFKILL);
+ close(fd);
+ return -errno;
+ }
+
+ return fd;
+}
+
+/* returns: 0 success, 1 read again, < 0 error */
+static int rfkill_read_event(int fd, struct rfkill_event *event)
+{
+ ssize_t len = read(fd, event, sizeof(*event));
+
+ if (len < 0) {
+ if (errno == EAGAIN)
+ return 1;
+ warn(_("cannot read %s"), _PATH_DEV_RFKILL);
+ return -errno;
+ }
+
+ if (len < RFKILL_EVENT_SIZE_V1) {
+ warnx(_("wrong size of rfkill event: %zu < %d"), len, RFKILL_EVENT_SIZE_V1);
+ return 1;
+ }
+
+ return 0;
+}
+
+
+static int rfkill_event(void)
+{
+ struct rfkill_event event;
+ struct timeval tv;
+ char date_buf[ISO_BUFSIZ];
+ struct pollfd p;
+ int fd, n;
+
+ fd = rfkill_ro_open(0);
+ if (fd < 0)
+ return -errno;
+
+ memset(&p, 0, sizeof(p));
+ p.fd = fd;
+ p.events = POLLIN | POLLHUP;
+
+ /* interrupted by signal only */
+ while (1) {
+ int rc = 1; /* recover-able error */
+
+ n = poll(&p, 1, -1);
+ if (n < 0) {
+ warn(_("failed to poll %s"), _PATH_DEV_RFKILL);
+ goto failed;
+ }
+
+ if (n)
+ rc = rfkill_read_event(fd, &event);
+ if (rc < 0)
+ goto failed;
+ if (rc)
+ continue;
+
+ gettimeofday(&tv, NULL);
+ strtimeval_iso(&tv, ISO_TIMESTAMP_COMMA, date_buf,
+ sizeof(date_buf));
+ printf("%s: idx %u type %u op %u soft %u hard %u\n",
+ date_buf,
+ event.idx, event.type, event.op, event.soft, event.hard);
+ fflush(stdout);
+ }
+
+failed:
+ close(fd);
+ return -1;
+}
+
+static const char *get_sys_attr(uint32_t idx, const char *attr)
+{
+ static char name[128];
+ char path[PATH_MAX];
+ FILE *f;
+ char *p;
+
+ snprintf(path, sizeof(path), _PATH_SYS_RFKILL "/rfkill%u/%s", idx, attr);
+ f = fopen(path, "r");
+ if (!f)
+ goto done;
+ if (!fgets(name, sizeof(name), f))
+ goto done;
+ p = strchr(name, '\n');
+ if (p)
+ *p = '\0';
+done:
+ if (f)
+ fclose(f);
+ return name;
+}
+
+static struct rfkill_id rfkill_id_to_type(const char *s)
+{
+ const struct rfkill_type_str *p;
+ struct rfkill_id ret;
+
+ if (islower(*s)) {
+ for (p = rfkill_type_strings; p->name != NULL; p++) {
+ if (!strcmp(s, p->name)) {
+ ret.type = p->type;
+ if (!strcmp(s, "all"))
+ ret.result = RFKILL_IS_ALL;
+ else
+ ret.result = RFKILL_IS_TYPE;
+ return ret;
+ }
+ }
+ } else if (isdigit(*s)) {
+ /* assume a numeric character implies an index. */
+ char filename[64];
+
+ ret.index = strtou32_or_err(s, _("invalid identifier"));
+ snprintf(filename, sizeof(filename) - 1,
+ _PATH_SYS_RFKILL "/rfkill%" PRIu32 "/name", ret.index);
+ if (access(filename, F_OK) == 0)
+ ret.result = RFKILL_IS_INDEX;
+ else
+ ret.result = RFKILL_IS_INVALID;
+ return ret;
+ }
+
+ ret.result = RFKILL_IS_INVALID;
+ return ret;
+}
+
+static const char *rfkill_type_to_desc(enum rfkill_type type)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(rfkill_type_strings); i++) {
+ if (type == rfkill_type_strings[i].type)
+ return rfkill_type_strings[i].desc;
+ }
+
+ return NULL;
+}
+
+
+static int event_match(struct rfkill_event *event, struct rfkill_id *id)
+{
+ if (event->op != RFKILL_OP_ADD)
+ return 0;
+
+ /* filter out unwanted results */
+ switch (id->result) {
+ case RFKILL_IS_TYPE:
+ if (event->type != id->type)
+ return 0;
+ break;
+ case RFKILL_IS_INDEX:
+ if (event->idx != id->index)
+ return 0;
+ break;
+ case RFKILL_IS_ALL:
+ break;
+ default:
+ abort();
+ }
+
+ return 1;
+}
+
+static void fill_table_row(struct libscols_table *tb, struct rfkill_event *event)
+{
+ static struct libscols_line *ln;
+ size_t i;
+
+ assert(tb);
+
+ ln = scols_table_new_line(tb, NULL);
+ if (!ln) {
+ errno = ENOMEM;
+ errx(EXIT_FAILURE, _("failed to allocate output line"));
+ }
+
+ for (i = 0; i < (size_t)ncolumns; i++) {
+ char *str = NULL;
+ switch (get_column_id(i)) {
+ case COL_DEVICE:
+ str = xstrdup(get_sys_attr(event->idx, "name"));
+ break;
+ case COL_ID:
+ xasprintf(&str, "%" PRIu32, event->idx);
+ break;
+ case COL_TYPE:
+ str = xstrdup(get_sys_attr(event->idx, "type"));
+ break;
+ case COL_DESC:
+ str = xstrdup(rfkill_type_to_desc(event->type));
+ break;
+ case COL_SOFT:
+ str = xstrdup(event->soft ? _("blocked") : _("unblocked"));
+ break;
+ case COL_HARD:
+ str = xstrdup(event->hard ? _("blocked") : _("unblocked"));
+ break;
+ default:
+ abort();
+ }
+ if (str && scols_line_refer_data(ln, i, str))
+ errx(EXIT_FAILURE, _("failed to add output data"));
+ }
+}
+
+static int rfkill_list_old(const char *param)
+{
+ struct rfkill_id id = { .result = RFKILL_IS_ALL };
+ struct rfkill_event event;
+ int fd, rc = 0;
+
+ if (param) {
+ id = rfkill_id_to_type(param);
+ if (id.result == RFKILL_IS_INVALID) {
+ warnx(_("invalid identifier: %s"), param);
+ return -EINVAL;
+ }
+ }
+
+ fd = rfkill_ro_open(1);
+
+ while (1) {
+ rc = rfkill_read_event(fd, &event);
+ if (rc < 0)
+ break;
+ if (rc == 1 && errno == EAGAIN) {
+ rc = 0; /* done */
+ break;
+ }
+ if (rc == 0 && event_match(&event, &id)) {
+ char *name = xstrdup(get_sys_attr(event.idx, "name")),
+ *type = xstrdup(rfkill_type_to_desc(event.type));
+
+ if (!type)
+ type = xstrdup(get_sys_attr(event.idx, "type"));
+
+ printf("%u: %s: %s\n", event.idx, name, type);
+ printf("\tSoft blocked: %s\n", event.soft ? "yes" : "no");
+ printf("\tHard blocked: %s\n", event.hard ? "yes" : "no");
+
+ free(name);
+ free(type);
+ }
+ }
+ close(fd);
+ return rc;
+}
+
+static void rfkill_list_init(struct control *ctrl)
+{
+ size_t i;
+
+ scols_init_debug(0);
+
+ ctrl->tb = scols_new_table();
+ if (!ctrl->tb)
+ err(EXIT_FAILURE, _("failed to allocate output table"));
+
+ scols_table_enable_json(ctrl->tb, ctrl->json);
+ scols_table_enable_noheadings(ctrl->tb, ctrl->no_headings);
+ scols_table_enable_raw(ctrl->tb, ctrl->raw);
+
+ for (i = 0; i < (size_t) ncolumns; i++) {
+ const struct colinfo *col = get_column_info(i);
+ struct libscols_column *cl;
+
+ cl = scols_table_new_column(ctrl->tb, col->name, col->whint, col->flags);
+ if (!cl)
+ err(EXIT_FAILURE, _("failed to allocate output column"));
+ if (ctrl->json) {
+ int id = get_column_id(i);
+ if (id == COL_ID)
+ scols_column_set_json_type(cl, SCOLS_JSON_NUMBER);
+ }
+ }
+}
+
+static int rfkill_list_fill(struct control const *ctrl, const char *param)
+{
+ struct rfkill_id id = { .result = RFKILL_IS_ALL };
+ struct rfkill_event event;
+ int fd, rc = 0;
+
+ if (param) {
+ id = rfkill_id_to_type(param);
+ if (id.result == RFKILL_IS_INVALID) {
+ warnx(_("invalid identifier: %s"), param);
+ return -EINVAL;
+ }
+ }
+
+ fd = rfkill_ro_open(1);
+
+ while (1) {
+ rc = rfkill_read_event(fd, &event);
+ if (rc < 0)
+ break;
+ if (rc == 1 && errno == EAGAIN) {
+ rc = 0; /* done */
+ break;
+ }
+ if (rc == 0 && event_match(&event, &id))
+ fill_table_row(ctrl->tb, &event);
+ }
+ close(fd);
+ return rc;
+}
+
+static void rfkill_list_output(struct control const *ctrl)
+{
+ scols_print_table(ctrl->tb);
+ scols_unref_table(ctrl->tb);
+}
+
+static int rfkill_block(uint8_t block, const char *param)
+{
+ struct rfkill_id id;
+ struct rfkill_event event = {
+ .op = RFKILL_OP_CHANGE_ALL,
+ .soft = block,
+ 0
+ };
+ ssize_t len;
+ int fd;
+ char *message = NULL;
+
+ id = rfkill_id_to_type(param);
+
+ switch (id.result) {
+ case RFKILL_IS_INVALID:
+ warnx(_("invalid identifier: %s"), param);
+ return -1;
+ case RFKILL_IS_TYPE:
+ event.type = id.type;
+ xasprintf(&message, "type %s", param);
+ break;
+ case RFKILL_IS_INDEX:
+ event.op = RFKILL_OP_CHANGE;
+ event.idx = id.index;
+ xasprintf(&message, "id %d", id.index);
+ break;
+ case RFKILL_IS_ALL:
+ message = xstrdup("all");
+ break;
+ default:
+ abort();
+ }
+
+ fd = open(_PATH_DEV_RFKILL, O_RDWR);
+ if (fd < 0) {
+ warn(_("cannot open %s"), _PATH_DEV_RFKILL);
+ free(message);
+ return -errno;
+ }
+
+ len = write(fd, &event, sizeof(event));
+ if (len < 0)
+ warn(_("write failed: %s"), _PATH_DEV_RFKILL);
+ else {
+ openlog("rfkill", 0, LOG_USER);
+ syslog(LOG_NOTICE, "%s set for %s", block ? "block" : "unblock", message);
+ closelog();
+ }
+ free(message);
+ return close(fd);
+}
+
+static void __attribute__((__noreturn__)) usage(void)
+{
+ size_t i;
+
+ fputs(USAGE_HEADER, stdout);
+ fprintf(stdout, _(" %s [options] command [identifier ...]\n"), program_invocation_short_name);
+
+ fputs(USAGE_SEPARATOR, stdout);
+ fputs(_("Tool for enabling and disabling wireless devices.\n"), stdout);
+
+ fputs(USAGE_OPTIONS, stdout);
+ fputs(_(" -J, --json use JSON output format\n"), stdout);
+ fputs(_(" -n, --noheadings don't print headings\n"), stdout);
+ fputs(_(" -o, --output <list> define which output columns to use\n"), stdout);
+ fputs(_(" --output-all output all columns\n"), stdout);
+ fputs(_(" -r, --raw use the raw output format\n"), stdout);
+
+ fputs(USAGE_SEPARATOR, stdout);
+ printf(USAGE_HELP_OPTIONS(24));
+
+ fputs(USAGE_COLUMNS, stdout);
+ for (i = 0; i < ARRAY_SIZE(infos); i++)
+ fprintf(stdout, " %-10s %s\n", infos[i].name, _(infos[i].help));
+
+ fputs(USAGE_COMMANDS, stdout);
+
+ /*
+ * TRANSLATORS: command names should not be translated, explaining
+ * them as additional field after identifier is fine, for example
+ *
+ * list [identifier] (lista [tarkenne])
+ */
+ fputs(_(" help\n"), stdout);
+ fputs(_(" event\n"), stdout);
+ fputs(_(" list [identifier]\n"), stdout);
+ fputs(_(" block identifier\n"), stdout);
+ fputs(_(" unblock identifier\n"), stdout);
+
+ fprintf(stdout, USAGE_MAN_TAIL("rfkill(8)"));
+ exit(EXIT_SUCCESS);
+}
+
+int main(int argc, char **argv)
+{
+ struct control ctrl = { 0 };
+ int c, act = ACT_LIST, list_all = 0;
+ char *outarg = NULL;
+ enum {
+ OPT_LIST_TYPES = CHAR_MAX + 1
+ };
+ static const struct option longopts[] = {
+ { "json", no_argument, NULL, 'J' },
+ { "noheadings", no_argument, NULL, 'n' },
+ { "output", required_argument, NULL, 'o' },
+ { "output-all", no_argument, NULL, OPT_LIST_TYPES },
+ { "raw", no_argument, NULL, 'r' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 }
+ };
+ static const ul_excl_t excl[] = {
+ {'J', 'r'},
+ {0}
+ };
+ int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT;
+ int ret = 0;
+
+ setlocale(LC_ALL, "");
+ bindtextdomain(PACKAGE, LOCALEDIR);
+ textdomain(PACKAGE);
+ atexit(close_stdout);
+
+ while ((c = getopt_long(argc, argv, "Jno:rVh", longopts, NULL)) != -1) {
+ err_exclusive_options(c, longopts, excl, excl_st);
+ switch (c) {
+ case 'J':
+ ctrl.json = 1;
+ break;
+ case 'n':
+ ctrl.no_headings = 1;
+ break;
+ case 'o':
+ outarg = optarg;
+ break;
+ case OPT_LIST_TYPES:
+ list_all = 1;
+ break;
+ case 'r':
+ ctrl.raw = 1;
+ break;
+ case 'V':
+ printf(UTIL_LINUX_VERSION);
+ return EXIT_SUCCESS;
+ case 'h':
+ usage();
+ default:
+ errtryhelp(EXIT_FAILURE);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 0) {
+ act = string_to_action(*argv);
+ if (act < 0)
+ errtryhelp(EXIT_FAILURE);
+ argv++;
+ argc--;
+
+ /*
+ * For backward compatibility we use old output format if
+ * "list" explicitly specified and--output not defined.
+ */
+ if (!outarg && act == ACT_LIST)
+ act = ACT_LIST_OLD;
+ }
+
+ switch (act) {
+ case ACT_LIST_OLD:
+ /* Deprecated in favour of ACT_LIST */
+ if (!argc)
+ ret |= rfkill_list_old(NULL); /* ALL */
+ else while (argc) {
+ ret |= rfkill_list_old(*argv);
+ argc--;
+ argv++;
+ }
+ break;
+
+ case ACT_LIST:
+ columns[ncolumns++] = COL_ID;
+ columns[ncolumns++] = COL_TYPE;
+ columns[ncolumns++] = COL_DEVICE;
+ if (list_all)
+ columns[ncolumns++] = COL_DESC;
+ columns[ncolumns++] = COL_SOFT;
+ columns[ncolumns++] = COL_HARD;
+
+ if (outarg
+ && string_add_to_idarray(outarg, columns,
+ ARRAY_SIZE(columns), &ncolumns,
+ column_name_to_id) < 0)
+ return EXIT_FAILURE;
+
+ rfkill_list_init(&ctrl);
+ if (!argc)
+ ret |= rfkill_list_fill(&ctrl, NULL); /* ALL */
+ else while (argc) {
+ ret |= rfkill_list_fill(&ctrl, *argv);
+ argc--;
+ argv++;
+ }
+ rfkill_list_output(&ctrl);
+ break;
+
+ case ACT_EVENT:
+ ret = rfkill_event();
+ break;
+
+ case ACT_HELP:
+ usage();
+ break;
+
+ case ACT_BLOCK:
+ while (argc) {
+ ret |= rfkill_block(1, *argv);
+ argc--;
+ argv++;
+ }
+ break;
+
+ case ACT_UNBLOCK:
+ while (argc) {
+ ret |= rfkill_block(0, *argv);
+ argv++;
+ argc--;
+ }
+ break;
+ }
+
+ return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}