summaryrefslogtreecommitdiffstats
path: root/plugins/solidigm/solidigm-internal-logs.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 19:41:32 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 19:41:32 +0000
commitf26f66d866ba1a9f3204e6fdfe2b07e67b5492ad (patch)
treec953c007cbe4f60a147ab62f97937d58abb2e9ca /plugins/solidigm/solidigm-internal-logs.c
parentInitial commit. (diff)
downloadnvme-cli-f26f66d866ba1a9f3204e6fdfe2b07e67b5492ad.tar.xz
nvme-cli-f26f66d866ba1a9f3204e6fdfe2b07e67b5492ad.zip
Adding upstream version 2.8.upstream/2.8
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/solidigm/solidigm-internal-logs.c')
-rw-r--r--plugins/solidigm/solidigm-internal-logs.c657
1 files changed, 657 insertions, 0 deletions
diff --git a/plugins/solidigm/solidigm-internal-logs.c b/plugins/solidigm/solidigm-internal-logs.c
new file mode 100644
index 0000000..c604761
--- /dev/null
+++ b/plugins/solidigm/solidigm-internal-logs.c
@@ -0,0 +1,657 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2023 Solidigm.
+ *
+ * Authors: leonardo.da.cunha@solidigm.com
+ * shankaralingegowda.singonahalli@solidigm.com
+ */
+
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <inttypes.h>
+#include <linux/limits.h>
+#include <time.h>
+
+#include "common.h"
+#include "nvme.h"
+#include "libnvme.h"
+#include "plugin.h"
+#include "nvme-print.h"
+#include "solidigm-util.h"
+
+#define DWORD_SIZE 4
+
+enum log_type {
+ NLOG = 0,
+ EVENTLOG = 1,
+ ASSERTLOG = 2,
+};
+
+#pragma pack(push, internal_logs, 1)
+struct version {
+ __u16 major;
+ __u16 minor;
+};
+
+struct event_dump_instance {
+ __u32 numeventdumps;
+ __u32 coresize;
+ __u32 coreoffset;
+ __u32 eventidoffset[16];
+ __u8 eventIdValidity[16];
+};
+
+struct commom_header {
+ struct version ver;
+ __u32 header_size;
+ __u32 log_size;
+ __u32 numcores;
+};
+
+struct event_dump_header {
+ struct commom_header header;
+ __u32 eventidsize;
+ struct event_dump_instance edumps[0];
+};
+
+struct assert_dump_core {
+ __u32 coreoffset;
+ __u32 assertsize;
+ __u8 assertdumptype;
+ __u8 assertvalid;
+ __u8 reserved[2];
+};
+
+struct assert_dump_header {
+ struct commom_header header;
+ struct assert_dump_core core[];
+};
+
+struct nlog_dump_header_common {
+ struct version ver;
+ __u32 logselect;
+ __u32 totalnlogs;
+ __u32 nlognum;
+ char nlogname[4];
+ __u32 nlogbytesize;
+ __u32 nlogprimarybuffsize;
+ __u32 tickspersecond;
+ __u32 corecount;
+};
+
+struct nlog_dump_header3_0 {
+ struct nlog_dump_header_common common;
+ __u32 nlogpausestatus;
+ __u32 selectoffsetref;
+ __u32 selectnlogpause;
+ __u32 selectaddedoffset;
+ __u32 nlogbufnum;
+ __u32 nlogbufnummax;
+};
+
+struct nlog_dump_header4_0 {
+ struct nlog_dump_header_common common;
+ __u64 nlogpausestatus;
+ __u32 selectoffsetref;
+ __u32 selectnlogpause;
+ __u32 selectaddedoffset;
+ __u32 nlogbufnum;
+ __u32 nlogbufnummax;
+ __u32 coreselected;
+ __u32 reserved[2];
+};
+
+struct nlog_dump_header4_1 {
+ struct nlog_dump_header_common common;
+ __u64 nlogpausestatus;
+ __u32 selectoffsetref;
+ __u32 selectnlogpause;
+ __u32 selectaddedoffset;
+ __u32 nlogbufnum;
+ __u32 nlogbufnummax;
+ __u32 coreselected;
+ __u32 lpaPointer1High;
+ __u32 lpaPointer1Low;
+ __u32 lpaPointer2High;
+ __u32 lpaPointer2Low;
+};
+
+#pragma pack(pop, internal_logs)
+
+struct config {
+ __u32 namespace_id;
+ char *dir_prefix;
+ char *type;
+ bool verbose;
+};
+
+static void print_nlog_header(__u8 *buffer)
+{
+ struct nlog_dump_header_common *nlog_header = (struct nlog_dump_header_common *) buffer;
+
+ if (nlog_header->ver.major >= 3) {
+ printf("Version Major %u\n", nlog_header->ver.major);
+ printf("Version Minor %u\n", nlog_header->ver.minor);
+ printf("Log_select %u\n", nlog_header->logselect);
+ printf("totalnlogs %u\n", nlog_header->totalnlogs);
+ printf("nlognum %u\n", nlog_header->nlognum);
+ printf("nlogname %c%c%c%c\n", nlog_header->nlogname[3], nlog_header->nlogname[2],
+ nlog_header->nlogname[1], nlog_header->nlogname[0]);
+ printf("nlogbytesize %u\n", nlog_header->nlogbytesize);
+ printf("nlogprimarybuffsize %u\n", nlog_header->nlogprimarybuffsize);
+ printf("tickspersecond %u\n", nlog_header->tickspersecond);
+ printf("corecount %u\n", nlog_header->corecount);
+ }
+ if (nlog_header->ver.major >= 4) {
+ struct nlog_dump_header4_0 *nlog_header = (struct nlog_dump_header4_0 *) buffer;
+
+ printf("nlogpausestatus %"PRIu64"\n", (uint64_t)nlog_header->nlogpausestatus);
+ printf("selectoffsetref %u\n", nlog_header->selectoffsetref);
+ printf("selectnlogpause %u\n", nlog_header->selectnlogpause);
+ printf("selectaddedoffset %u\n", nlog_header->selectaddedoffset);
+ printf("nlogbufnum %u\n", nlog_header->nlogbufnum);
+ printf("nlogbufnummax %u\n", nlog_header->nlogbufnummax);
+ printf("coreselected %u\n\n", nlog_header->coreselected);
+ }
+}
+
+#define INTERNAL_LOG_MAX_BYTE_TRANSFER 4096
+#define INTERNAL_LOG_MAX_DWORD_TRANSFER (INTERNAL_LOG_MAX_BYTE_TRANSFER / 4)
+
+static int cmd_dump_repeat(struct nvme_passthru_cmd *cmd, __u32 total_dw_size,
+ int out_fd, int ioctl_fd, bool force_max_transfer)
+{
+ int err = 0;
+
+ while (total_dw_size > 0) {
+ size_t dword_tfer = min(INTERNAL_LOG_MAX_DWORD_TRANSFER, total_dw_size);
+
+ cmd->cdw10 = force_max_transfer ? INTERNAL_LOG_MAX_DWORD_TRANSFER : dword_tfer;
+ cmd->data_len = dword_tfer * 4;
+ err = nvme_submit_admin_passthru(ioctl_fd, cmd, NULL);
+ if (err)
+ return err;
+
+ if (out_fd > 0) {
+ err = write(out_fd, (const void *)(uintptr_t)cmd->addr, cmd->data_len);
+ if (err < 0) {
+ perror("write failure");
+ return err;
+ }
+ err = 0;
+ }
+ total_dw_size -= dword_tfer;
+ cmd->cdw13 += dword_tfer;
+ }
+ return err;
+}
+
+static int write_header(__u8 *buf, int fd, size_t amnt)
+{
+ if (write(fd, buf, amnt) < 0)
+ return 1;
+ return 0;
+}
+
+static int read_header(struct nvme_passthru_cmd *cmd, int ioctl_fd)
+{
+ memset((void *)(uintptr_t)cmd->addr, 0, INTERNAL_LOG_MAX_BYTE_TRANSFER);
+ return cmd_dump_repeat(cmd, INTERNAL_LOG_MAX_DWORD_TRANSFER, -1, ioctl_fd, false);
+}
+
+static int get_serial_number(char *str, int fd)
+{
+ struct nvme_id_ctrl ctrl = {0};
+ int err;
+
+ err = nvme_identify_ctrl(fd, &ctrl);
+ if (err)
+ return err;
+
+ /* Remove trailing spaces */
+ for (int i = sizeof(ctrl.sn) - 1; i && ctrl.sn[i] == ' '; i--)
+ ctrl.sn[i] = '\0';
+ sprintf(str, "%-.*s", (int)sizeof(ctrl.sn), ctrl.sn);
+ return err;
+}
+
+static int dump_assert_logs(struct nvme_dev *dev, struct config cfg)
+{
+ __u8 buf[INTERNAL_LOG_MAX_BYTE_TRANSFER];
+ __u8 head_buf[INTERNAL_LOG_MAX_BYTE_TRANSFER];
+ char file_path[PATH_MAX];
+ char file_name[] = "AssertLog.bin";
+ struct assert_dump_header *ad = (struct assert_dump_header *) head_buf;
+ struct nvme_passthru_cmd cmd = {
+ .opcode = 0xd2,
+ .nsid = cfg.namespace_id,
+ .addr = (unsigned long)(void *)head_buf,
+ .cdw12 = ASSERTLOG,
+ .cdw13 = 0,
+ };
+ int output, err;
+
+ err = read_header(&cmd, dev_fd(dev));
+ if (err)
+ return err;
+
+ snprintf(file_path, sizeof(file_path), "%.*s/%s",
+ (int) (sizeof(file_path) - sizeof(file_name) - 1), cfg.dir_prefix, file_name);
+ output = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (output < 0)
+ return -errno;
+ err = write_header((__u8 *)ad, output, ad->header.header_size * DWORD_SIZE);
+ if (err) {
+ perror("write failure");
+ close(output);
+ return err;
+ }
+ cmd.addr = (unsigned long)(void *)buf;
+
+ if (cfg.verbose) {
+ printf("Assert Log, cores: %d log size: %d header size: %d\n", ad->header.numcores,
+ ad->header.log_size * DWORD_SIZE, ad->header.header_size * DWORD_SIZE);
+ for (__u32 i = 0; i < ad->header.numcores; i++)
+ printf("core %d assert size: %d\n", i, ad->core[i].assertsize * DWORD_SIZE);
+ }
+
+ for (__u32 i = 0; i < ad->header.numcores; i++) {
+ if (!ad->core[i].assertvalid)
+ continue;
+ cmd.cdw13 = ad->core[i].coreoffset;
+ err = cmd_dump_repeat(&cmd, ad->core[i].assertsize,
+ output,
+ dev_fd(dev), false);
+ if (err) {
+ close(output);
+ return err;
+ }
+ }
+ close(output);
+ printf("Successfully wrote log to %s\n", file_path);
+ return err;
+}
+
+static int dump_event_logs(struct nvme_dev *dev, struct config cfg)
+{
+ __u8 buf[INTERNAL_LOG_MAX_BYTE_TRANSFER];
+ __u8 head_buf[INTERNAL_LOG_MAX_BYTE_TRANSFER];
+ char file_path[PATH_MAX];
+ struct event_dump_header *ehdr = (struct event_dump_header *) head_buf;
+ struct nvme_passthru_cmd cmd = {
+ .opcode = 0xd2,
+ .nsid = cfg.namespace_id,
+ .addr = (unsigned long)(void *)head_buf,
+ .cdw12 = EVENTLOG,
+ .cdw13 = 0,
+ };
+ int output;
+ int core_num, err;
+
+ err = read_header(&cmd, dev_fd(dev));
+ if (err)
+ return err;
+ snprintf(file_path, sizeof(file_path), "%s/EventLog.bin", cfg.dir_prefix);
+ output = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (output < 0)
+ return -errno;
+ err = write_header(head_buf, output, INTERNAL_LOG_MAX_BYTE_TRANSFER);
+
+ core_num = ehdr->header.numcores;
+
+ if (err) {
+ close(output);
+ return err;
+ }
+ cmd.addr = (unsigned long)(void *)buf;
+
+ if (cfg.verbose)
+ printf("Event Log, cores: %d log size: %d\n", core_num, ehdr->header.log_size * 4);
+
+ for (__u32 j = 0; j < core_num; j++) {
+ if (cfg.verbose) {
+ for (int k = 0 ; k < 16; k++) {
+ printf("core: %d event: %d ", j, k);
+ printf("validity: %d ", ehdr->edumps[j].eventIdValidity[k]);
+ printf("offset: %d\n", ehdr->edumps[j].eventidoffset[k]);
+ }
+ }
+ cmd.cdw13 = ehdr->edumps[j].coreoffset;
+ err = cmd_dump_repeat(&cmd, ehdr->edumps[j].coresize,
+ output, dev_fd(dev), false);
+ if (err) {
+ close(output);
+ return err;
+ }
+ }
+ close(output);
+ printf("Successfully wrote log to %s\n", file_path);
+ return err;
+}
+
+static size_t get_nlog_header_size(struct nlog_dump_header_common *nlog_header)
+{
+ switch (nlog_header->ver.major) {
+ case 3:
+ return sizeof(struct nlog_dump_header3_0);
+ case 4:
+ if (nlog_header->ver.minor == 0)
+ return sizeof(struct nlog_dump_header4_0);
+ return sizeof(struct nlog_dump_header4_1);
+ default:
+ return INTERNAL_LOG_MAX_BYTE_TRANSFER;
+ }
+
+}
+
+/* dumps nlogs from specified core or all cores when core = -1 */
+static int dump_nlogs(struct nvme_dev *dev, struct config cfg, int core)
+{
+ int err = 0;
+ __u32 count, core_num;
+ __u8 buf[INTERNAL_LOG_MAX_BYTE_TRANSFER];
+ char file_path[PATH_MAX];
+ struct nlog_dump_header_common *nlog_header = (struct nlog_dump_header_common *)buf;
+ struct nvme_passthru_cmd cmd = {
+ .opcode = 0xd2,
+ .nsid = cfg.namespace_id,
+ .addr = (unsigned long)(void *)buf
+ };
+
+ struct dump_select {
+ union {
+ struct {
+ __u32 selectLog : 3;
+ __u32 selectCore : 2;
+ __u32 selectNlog : 8;
+ };
+ __u32 raw;
+ };
+ } log_select;
+ int output;
+ bool is_open = false;
+ size_t header_size = 0;
+
+ log_select.selectCore = core < 0 ? 0 : core;
+ do {
+ log_select.selectNlog = 0;
+ do {
+ cmd.cdw13 = 0;
+ cmd.cdw12 = log_select.raw;
+ err = read_header(&cmd, dev_fd(dev));
+ if (err) {
+ if (is_open)
+ close(output);
+ return err;
+ }
+ count = nlog_header->totalnlogs;
+ core_num = core < 0 ? nlog_header->corecount : 0;
+ if (!header_size) {
+ snprintf(file_path, sizeof(file_path), "%s/NLog.bin",
+ cfg.dir_prefix);
+ output = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (output < 0)
+ return -errno;
+ header_size = get_nlog_header_size(nlog_header);
+ is_open = true;
+ }
+ err = write_header(buf, output, header_size);
+ if (err)
+ break;
+ if (cfg.verbose)
+ print_nlog_header(buf);
+ cmd.cdw13 = 0x400;
+ err = cmd_dump_repeat(&cmd, nlog_header->nlogbytesize / 4,
+ output, dev_fd(dev), true);
+ if (err)
+ break;
+ } while (++log_select.selectNlog < count);
+ if (err)
+ break;
+ } while (++log_select.selectCore < core_num);
+ if (is_open) {
+ close(output);
+ printf("Successfully wrote log to %s\n", file_path);
+ }
+ return err;
+}
+
+enum telemetry_type {
+ HOSTGENOLD,
+ HOSTGENNEW,
+ CONTROLLER
+};
+
+static int dump_telemetry(struct nvme_dev *dev, struct config cfg, enum telemetry_type ttype)
+{
+ _cleanup_free_ struct nvme_telemetry_log *log = NULL;
+ size_t log_size = 0;
+ int err = 0;
+ __u8 *buffer = NULL;
+ size_t bytes_remaining = 0;
+ enum nvme_telemetry_da da;
+ size_t max_data_tx;
+ char file_path[PATH_MAX];
+ char *file_name;
+ char *log_descr;
+ struct stat sb;
+
+ _cleanup_file_ int output = -1;
+
+ switch (ttype) {
+ case HOSTGENNEW:
+ file_name = "lid_0x07_lsp_0x01_lsi_0x0000.bin";
+ log_descr = "Generated Host Initiated";
+ break;
+ case HOSTGENOLD:
+ file_name = "lid_0x07_lsp_0x00_lsi_0x0000.bin";
+ log_descr = "Existing Host Initiated";
+ break;
+ case CONTROLLER:
+ file_name = "lid_0x08_lsp_0x00_lsi_0x0000.bin";
+ log_descr = "Controller Initiated";
+ break;
+ default:
+ return -EINVAL;
+ }
+ err = nvme_get_telemetry_max(dev_fd(dev), &da, &max_data_tx);
+ if (err)
+ return err;
+
+ if (max_data_tx > DRIVER_MAX_TX_256K)
+ max_data_tx = DRIVER_MAX_TX_256K;
+
+ switch (ttype) {
+ case HOSTGENNEW:
+ err = nvme_get_telemetry_log(dev_fd(dev), true, false, false, max_data_tx, da,
+ &log, &log_size);
+ break;
+ case HOSTGENOLD:
+ err = nvme_get_telemetry_log(dev_fd(dev), false, false, false, max_data_tx, da,
+ &log, &log_size);
+ break;
+ case CONTROLLER:
+ err = nvme_get_telemetry_log(dev_fd(dev), false, true, true, max_data_tx, da, &log,
+ &log_size);
+ break;
+ }
+
+ if (err)
+ return err;
+
+ snprintf(file_path, sizeof(file_path), "%s/log_pages", cfg.dir_prefix);
+ if (!(stat(file_path, &sb) == 0 && S_ISDIR(sb.st_mode))) {
+ if (mkdir(file_path, 777) != 0) {
+ perror(file_path);
+ return -errno;
+ }
+ }
+
+ snprintf(file_path, sizeof(file_path), "%s/log_pages/%s", cfg.dir_prefix, file_name);
+ output = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (output < 0)
+ return -errno;
+
+ bytes_remaining = log_size;
+ buffer = (__u8 *)log;
+
+ while (bytes_remaining) {
+ ssize_t bytes_written = write(output, buffer, bytes_remaining);
+
+ if (bytes_written < 0) {
+ err = -errno;
+ goto tele_close_output;
+ }
+
+ bytes_remaining -= bytes_written;
+ buffer += bytes_written;
+ }
+ printf("Successfully wrote %s Telemetry log to %s\n", log_descr, file_path);
+
+tele_close_output:
+ close(output);
+ return err;
+}
+
+int solidigm_get_internal_log(int argc, char **argv, struct command *command,
+ struct plugin *plugin)
+{
+ char folder[PATH_MAX];
+ char zip_name[PATH_MAX];
+ char *output_path;
+ char sn_prefix[sizeof(((struct nvme_id_ctrl *)0)->sn)+1];
+ int log_count = 0;
+ int err;
+ _cleanup_nvme_dev_ struct nvme_dev *dev = NULL;
+ bool all = false;
+ time_t t;
+ struct tm tm;
+
+ const char *desc = "Get Debug Firmware Logs and save them.";
+ const char *type =
+ "Log type: ALL, CONTROLLERINITTELEMETRY, HOSTINITTELEMETRY, HOSTINITTELEMETRYNOGEN, NLOG, ASSERT, EVENT. Defaults to ALL.";
+ const char *prefix = "Output dir prefix; defaults to device serial number.";
+ const char *verbose = "To print out verbose info.";
+ const char *namespace_id = "Namespace to get logs from.";
+
+
+ struct config cfg = {
+ .namespace_id = NVME_NSID_ALL,
+ .dir_prefix = NULL,
+ .type = NULL,
+ };
+
+ OPT_ARGS(opts) = {
+ OPT_STR("type", 't', &cfg.type, type),
+ OPT_UINT("namespace-id", 'n', &cfg.namespace_id, namespace_id),
+ OPT_FILE("dir-prefix", 'p', &cfg.dir_prefix, prefix),
+ OPT_FLAG("verbose", 'v', &cfg.verbose, verbose),
+ OPT_END()
+ };
+
+ err = parse_and_open(&dev, argc, argv, desc, opts);
+ if (err)
+ return err;
+
+ if (!cfg.dir_prefix) {
+ err = get_serial_number(sn_prefix, dev_fd(dev));
+ if (err)
+ return err;
+ cfg.dir_prefix = sn_prefix;
+ }
+ t = time(NULL);
+ tm = *localtime(&t);
+ snprintf(folder, sizeof(folder), "%s-%d%02d%02d%02d%02d%02d", cfg.dir_prefix,
+ tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+ if (mkdir(folder, 0777) != 0) {
+ perror("mkdir");
+ return -errno;
+ }
+ cfg.dir_prefix = folder;
+ output_path = folder;
+
+ if (!cfg.type)
+ cfg.type = "ALL";
+ else {
+ for (char *p = cfg.type; *p; ++p)
+ *p = toupper(*p);
+ }
+
+ if (!strcmp(cfg.type, "ALL")) {
+ all = true;
+ }
+ if (all || !strcmp(cfg.type, "ASSERT")) {
+ err = dump_assert_logs(dev, cfg);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving Assert log");
+ }
+ if (all || !strcmp(cfg.type, "EVENT")) {
+ err = dump_event_logs(dev, cfg);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving Event log");
+ }
+ if (all || !strcmp(cfg.type, "NLOG")) {
+ err = dump_nlogs(dev, cfg, -1);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving Nlog");
+ }
+ if (all || !strcmp(cfg.type, "CONTROLLERINITTELEMETRY")) {
+ err = dump_telemetry(dev, cfg, CONTROLLER);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving Telemetry Controller Initiated");
+ }
+ if (all || !strcmp(cfg.type, "HOSTINITTELEMETRYNOGEN")) {
+ err = dump_telemetry(dev, cfg, HOSTGENOLD);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving previously existing Telemetry Host Initiated");
+ }
+ if (all || !strcmp(cfg.type, "HOSTINITTELEMETRY")) {
+ err = dump_telemetry(dev, cfg, HOSTGENNEW);
+ if (err == 0)
+ log_count++;
+ else if (err < 0)
+ perror("Error retrieving Telemetry Host Initiated");
+ }
+
+ if (log_count > 0) {
+ int ret_cmd;
+ char cmd[ARG_MAX];
+ char *where_err = cfg.verbose ? "" : ">/dev/null 2>&1";
+
+ snprintf(zip_name, sizeof(zip_name), "%s.zip", cfg.dir_prefix);
+ snprintf(cmd, sizeof(cmd), "cd \"%s\" && zip -r \"../%s\" ./* %s", cfg.dir_prefix,
+ zip_name, where_err);
+ printf("Compressing logs to %s\n", zip_name);
+ ret_cmd = system(cmd);
+ if (ret_cmd == -1)
+ perror(cmd);
+ else {
+ output_path = zip_name;
+ snprintf(cmd, sizeof(cmd), "rm -rf %s", cfg.dir_prefix);
+ printf("Removing %s\n", cfg.dir_prefix);
+ if (system(cmd) != 0)
+ perror("Failed removing logs folder");
+ }
+ }
+
+ if (log_count == 0) {
+ if (err > 0)
+ nvme_show_status(err);
+ } else if ((log_count > 1) || cfg.verbose)
+ printf("Total: %d log files in %s\n", log_count, output_path);
+
+ return err;
+}