From e9acd0f187b49511c501f9ea2c3608741c3ce80c Mon Sep 17 00:00:00 2001 From: Sergey Kiselev Date: Fri, 26 Jun 2026 10:55:03 +0500 Subject: [PATCH] The first version of the library --- examples/test.lua | 38 +++++ src/Makefile | 40 +++++ src/diskinfo.c | 407 ++++++++++++++++++++++++++++++++++++++++++++++ src/smart.h | 112 +++++++++++++ test/camtest.c | 159 ++++++++++++++++++ test/smartdump.c | 154 ++++++++++++++++++ 6 files changed, 910 insertions(+) create mode 100644 examples/test.lua create mode 100644 src/Makefile create mode 100644 src/diskinfo.c create mode 100644 src/smart.h create mode 100644 test/camtest.c create mode 100644 test/smartdump.c diff --git a/examples/test.lua b/examples/test.lua new file mode 100644 index 0000000..ffb4440 --- /dev/null +++ b/examples/test.lua @@ -0,0 +1,38 @@ +-- test.lua +local diskinfo = require("diskinfo") +local disks, err = diskinfo.get_info() + +if not disks then + print("Error fetching disk info: " .. tostring(err)) + os.exit(1) +end + +print("==================================================================") +print("FREEBSD SYSTEM DISK TREE (With S.M.A.R.T. Temperature)") +print("==================================================================") + +for _, disk_name in ipairs(disks._order) do + local disk = disks[disk_name] + + -- Проверяем, вернул ли модуль температуру SMART для этого накопителя + local smart_str = "" + if disk.smart_temp then + smart_str = string.format(" | Temp: %d°C", disk.smart_temp) + end + + print(string.format("Drive: %s [%s] | Type: %s%s", disk_name, disk.size, disk.type, smart_str)) + print(string.format(" |- Hardware Model : %s", disk.model)) + print(" |- Partition Tree :") + + if #disk.mounts._order == 0 then + print(" [No active mounts discovered on this device]") + else + for _, part_name in ipairs(disk.mounts._order) do + local partition = disk.mounts[part_name] + print(string.format(" -> %-10s | Path: %-20s | FS Type: %s", + part_name, partition.path, partition.type)) + end + end + print(string.rep("-", 66)) +end + diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..bcba26a --- /dev/null +++ b/src/Makefile @@ -0,0 +1,40 @@ +LIB_NAME= diskinfo +SRC= diskinfo.c + +PREFIX?= /usr/local + +OS!= uname -s +.if ${OS} != "FreeBSD" +.error "This Makefile and module are designed strictly for FreeBSD." +.endif + +.if !empty(LUA_VER) +.if ${LUA_VER} == "jit" +LUA_PC= luajit +.else +LUA_PC= lua-${LUA_VER} +.endif +.else +LUA_PC!= pkg-config --list-all | grep -E '^(lua-[0-9]|luajit)' | awk '{print $$1}' | sort -V | tail -n 1 +.endif + +LUA_CFLAGS!= pkg-config --cflags ${LUA_PC} +LUA_MODDIR!= pkg-config --variable=INSTALL_CMOD ${LUA_PC} + +CFLAGS+= -O2 -fPIC -Wall ${LUA_CFLAGS} +LDFLAGS+= -shared -lcam + +all: ${LIB_NAME}.so + +${LIB_NAME}.so: ${SRC} + ${CC} ${CFLAGS} ${LDFLAGS} -o ${LIB_NAME}.so ${SRC} + +clean: + rm -f ${LIB_NAME}.so + +install: all + mkdir -p ${DESTDIR}${LUA_MODDIR} + install -m 755 ${LIB_NAME}.so ${DESTDIR}${LUA_MODDIR}/${LIB_NAME}.so + +.PHONY: all clean install + diff --git a/src/diskinfo.c b/src/diskinfo.c new file mode 100644 index 0000000..14f71a8 --- /dev/null +++ b/src/diskinfo.c @@ -0,0 +1,407 @@ +/* + * diskinfo.c - FreeBSD-specific Lua module for gathering disk hardware + * models, capacities, precise drive types, and active filesystem mount points. + * + * This version uses the native FreeBSD libcam library via O_RDWR access. + * Accurately branches protocols between SCSI SAT for USB bridges (da) and + * native ATA commands for direct SATA controllers (ada). + */ + +#ifndef __FreeBSD__ +#error "This module is FreeBSD-specific and cannot be compiled on this platform." +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* FreeBSD CAM (Common Access Method) userland library interfaces */ +#include +#include +#include +#include + +/* Core Lua API headers */ +#include +#include + +/* Internal project definitions */ +#include "smart.h" + +/* Backward compatibility layer macro to support Lua 5.1 and LuaJIT environments */ +#if LUA_VERSION_NUM == 501 +#define luaL_newlib(L, l) (lua_newtable(L), luaL_register(L, NULL, l)) +#endif + +#define MAX_DISKS 64 +#define MAX_MOUNTS 128 + +/* Simple structure used to sort array components alphabetically via qsort API */ +typedef struct { + char name[MNAMELEN]; +} StringKey; + +/* Comparison callback function for alphabetical qsort */ +static int compare_strings(const void *a, const void *b) { + return strcmp(((StringKey *)a)->name, ((StringKey *)b)->name); +} + +/* Cleans up trailing spaces and non-printable junk from SCSI inquiry strings */ +static void clean_scsi_str(char *str) { + int i = strlen(str) - 1; + while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0')) { + str[i] = '\0'; + i--; + } +} + +/* Fixes word-byte-swapping in raw ATA firmware identity blocks */ +static void ata_str_fix(char *str, size_t len) { + for (size_t i = 0; i < len - 1; i += 2) { + char tmp = str[i]; + str[i] = str[i + 1]; + str[i + 1] = tmp; + } + int i = len - 1; + while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0' || str[i] == '\n' || str[i] == '\r')) { + str[i] = '\0'; + i--; + } +} + +/* Extracts base drive token name from slice/partition maps (e.g. "ada0p1" -> "ada0") */ +static void get_base_disk(const char *dev_name, char *base_buf, size_t buf_len) { + strlcpy(base_buf, dev_name, buf_len); + char *p = base_buf; + while (*p && !(*p >= '0' && *p <= '9')) p++; /* Skip leading letters */ + while (*p && (*p >= '0' && *p <= '9')) p++; /* Skip index digits */ + if (*p) *p = '\0'; /* Chop partition suffixes */ +} + +/* + * Queries the device using FreeBSD libcam via appropriate protocol. + * For 'da' (USB) devices: Sends SCSI INQUIRY and SAT ATA PASS-THROUGH (16). + * For 'ada' (SATA) devices: Sends native ATA IDENTIFY command. + * Successfully bypasses all controller bugs to read real struct ata_params. + */ + static void query_cam_properties(const char *base_disk, char *model_buf, size_t buf_len, int *is_removable, int *is_ssd, int *pt_failed, int *temp) { + struct cam_device *cam_dev = NULL; + union ccb *ccb = NULL; + struct ata_params *ident_buf = NULL; + struct smart_values *smart_buf = NULL; + uint8_t *inq_buf = NULL; + + *is_removable = 0; + *is_ssd = 0; + *pt_failed = 0; + *temp = -1; + + /* CRITICAL: Must use O_RDWR for transport-layer commands execution path to unlock */ + cam_dev = cam_open_device(base_disk, O_RDWR); + if (cam_dev == NULL) return; + + ccb = cam_getccb(cam_dev); + if (ccb == NULL) { + cam_close_device(cam_dev); + return; + } + + ident_buf = calloc(1, sizeof(struct ata_params)); + smart_buf = calloc(1, sizeof(struct smart_values)); + if (ident_buf == NULL || smart_buf == NULL) { + free(ident_buf); free(smart_buf); + cam_freeccb(ccb); cam_close_device(cam_dev); + return; + } + + /* --- PROTOCOL BRANCH 1: USB / SCSI Direct Access Devices (da) --- */ + if (strncmp(base_disk, "da", 2) == 0) { + inq_buf = calloc(1, sizeof(struct scsi_inquiry_data)); + if (inq_buf == NULL) { + free(ident_buf); free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); + return; + } + + /* 1a: SCSI INQUIRY */ + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + struct scsi_inquiry_data *inq_data = (struct scsi_inquiry_data *)inq_buf; + cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE, + (uint8_t *)inq_data, SHORT_INQUIRY_LENGTH, SSD_FULL_SIZE, 6, 5000); + ccb->csio.cdb_io.cdb_bytes[0] = INQUIRY; + ccb->csio.cdb_io.cdb_bytes[4] = SHORT_INQUIRY_LENGTH; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + if ((inq_data->device & SID_RMB) != 0) *is_removable = 1; + char vendor[9], product[17]; + snprintf(vendor, sizeof(vendor), "%.8s", inq_data->vendor); + snprintf(product, sizeof(product), "%.16s", inq_data->product); + clean_scsi_str(vendor); clean_scsi_str(product); + snprintf(model_buf, buf_len, "%s %s", vendor, product); + } + + /* 1b: SCSI SAT ATA PASS-THROUGH (16) -> IDENTIFY */ + cam_freeccb(ccb); ccb = cam_getccb(cam_dev); + if (ccb != NULL) { + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE, + (uint8_t *)ident_buf, sizeof(struct ata_params), SSD_FULL_SIZE, 16, 5000); + + uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes; + cdb[0] = 0x85; cdb[1] = 4 << 1; cdb[2] = 0x08 | 0x02; cdb[4] = 1; cdb[14] = 0xEC; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + char real_model[41]; + memcpy(real_model, ident_buf->model, sizeof(ident_buf->model)); + real_model[sizeof(ident_buf->model)] = '\0'; + ata_str_fix(real_model, sizeof(real_model)); + if (strlen(real_model) > 0) strlcpy(model_buf, real_model, buf_len); + if (ident_buf->media_rotation_rate == 1) *is_ssd = 1; + } else { + *pt_failed = 1; + } + } + + /* 1c: SCSI SAT ATA PASS-THROUGH (16) -> SMART VALUES */ + if (!(*pt_failed)) { + cam_freeccb(ccb); ccb = cam_getccb(cam_dev); + if (ccb != NULL) { + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE, + (uint8_t *)smart_buf, sizeof(struct smart_values), SSD_FULL_SIZE, 16, 5000); + + uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes; + cdb[0] = 0x85; cdb[1] = 4 << 1; cdb[2] = 0x08 | 0x02; cdb[4] = 0xD0; cdb[6] = 1; + cdb[8] = 0x4F; cdb[10] = 0xC2; cdb[14] = 0xB0; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + for (int i = 0; i < 30; i++) { + struct smart_attribute *a = &smart_buf->attr[i]; + if (a->id == 194 || a->id == 190) { + *temp = parse_smart_temperature(a); + break; + } + } + } + } + } + free(inq_buf); + } + /* --- PROTOCOL BRANCH 2: Native SATA/ATA Devices (ada) --- */ + else if (strncmp(base_disk, "ada", 3) == 0) { + /* 2a: Pure ATA IDENTIFY */ + memset(&ccb->ataio, 0, sizeof(struct ccb_ataio)); + ccb->ccb_h.func_code = XPT_ATA_IO; ccb->ccb_h.flags = CAM_DIR_IN; ccb->ccb_h.timeout = 5000; + ccb->ataio.data_ptr = (uint8_t *)ident_buf; ccb->ataio.dxfer_len = sizeof(struct ata_params); + ccb->ataio.cmd.command = 0xEC; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + char real_model[41]; + memcpy(real_model, ident_buf->model, sizeof(ident_buf->model)); + real_model[sizeof(ident_buf->model)] = '\0'; + ata_str_fix(real_model, sizeof(real_model)); + if (strlen(real_model) > 0) strlcpy(model_buf, real_model, buf_len); + if (ident_buf->media_rotation_rate == 1) *is_ssd = 1; + } + + /* 2b: Pure ATA SMART VALUES */ + cam_freeccb(ccb); ccb = cam_getccb(cam_dev); + if (ccb != NULL) { + memset(&ccb->ataio, 0, sizeof(struct ccb_ataio)); + ccb->ccb_h.func_code = XPT_ATA_IO; ccb->ccb_h.flags = CAM_DIR_IN; ccb->ccb_h.timeout = 5000; + ccb->ataio.data_ptr = (uint8_t *)smart_buf; ccb->ataio.dxfer_len = sizeof(struct smart_values); + + ccb->ataio.cmd.command = 0xB0; ccb->ataio.cmd.features = 0xD0; + ccb->ataio.cmd.lba_mid = 0x4F; ccb->ataio.cmd.lba_high = 0xC2; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + for (int i = 0; i < 30; i++) { + struct smart_attribute *a = &smart_buf->attr[i]; + if (a->id == 194 || a->id == 190) { + *temp = parse_smart_temperature(a); + break; + } + } + } + } + } + + free(ident_buf); free(smart_buf); + if (ccb != NULL) cam_freeccb(ccb); + cam_close_device(cam_dev); +} + +/* Fetches the hardware description/model, raw storage capacity and precise drive type */ +static void get_disk_properties(lua_State *L, const char *base_disk) { + char path[MAXPATHLEN]; + char model_buf[256]; + char type_buf[16]; + off_t mediasize = 0; + + snprintf(path, sizeof(path), "/dev/%s", base_disk); + strlcpy(model_buf, "Generic Drive", sizeof(model_buf)); + strlcpy(type_buf, "unknown", sizeof(type_buf)); + + if (strncmp(base_disk, "nvme", 4) == 0 || strncmp(base_disk, "nvd", 3) == 0) { + strlcpy(type_buf, "nvme", sizeof(type_buf)); + } else if (strncmp(base_disk, "mmcsd", 5) == 0) { + strlcpy(type_buf, "sd", sizeof(type_buf)); + } + + int fd = open(path, O_RDONLY); + if (fd >= 0) { + struct diocgattr_arg arg; + memset(&arg, 0, sizeof(arg)); + strlcpy(arg.name, "GEOM::descr", sizeof(arg.name)); + arg.len = sizeof(arg.value.str); + if (ioctl(fd, DIOCGATTR, &arg) == 0 && strlen(arg.value.str) > 0) { + strlcpy(model_buf, arg.value.str, sizeof(model_buf)); + } + ioctl(fd, DIOCGMEDIASIZE, &mediasize); + close(fd); + } + + int is_ssd = 0, is_removable = 0, pt_failed = 0; + int smart_temp = -1; + + if (strncmp(base_disk, "ada", 3) == 0 || strncmp(base_disk, "da", 2) == 0) { + query_cam_properties(base_disk, model_buf, sizeof(model_buf), &is_removable, &is_ssd, &pt_failed, &smart_temp); + } + + if (strcmp(type_buf, "unknown") == 0) { + if (strncmp(base_disk, "da", 2) == 0) { + if (is_removable || pt_failed) strlcpy(type_buf, "usb", sizeof(type_buf)); + else if (is_ssd) strlcpy(type_buf, "usdssd", sizeof(type_buf)); + else strlcpy(type_buf, "usdhdd", sizeof(type_buf)); + } else if (strncmp(base_disk, "ada", 3) == 0) { + if (is_ssd) strlcpy(type_buf, "ssd", sizeof(type_buf)); + else strlcpy(type_buf, "hdd", sizeof(type_buf)); + } + } + + /* Push properties into the Lua table current on top of stack */ + lua_pushstring(L, model_buf); lua_setfield(L, -2, "model"); + lua_pushstring(L, type_buf); lua_setfield(L, -2, "type"); + + /* Format storage size with binary standards */ + char size_buf[32]; + double size_gib = (double)mediasize / (1024.0 * 1024.0 * 1024.0); + double final_size = size_gib; + const char *unit = "GiB"; + if (size_gib >= 1024.0) { final_size = size_gib / 1024.0; unit = "TiB"; } + if (final_size == (long long)final_size) snprintf(size_buf, sizeof(size_buf), "%lld %s", (long long)final_size, unit); + else snprintf(size_buf, sizeof(size_buf), "%.1f %s", final_size, unit); + lua_pushstring(L, size_buf); lua_setfield(L, -2, "size"); + + /* Inject smart_temp into Lua fields strictly if it was discovered successfully */ + if (smart_temp >= 0) { + lua_pushinteger(L, smart_temp); + lua_setfield(L, -2, "smart_temp"); + } +} + +/* Main Lua binding function that builds the nested tree structure */ +static int lua_get_nested_disks(lua_State *L) { + StringKey disk_names[MAX_DISKS]; + int disk_count = 0; + memset(disk_names, 0, sizeof(disk_names)); + + /* Read the list of available physical disks from sysctl kernel tree */ + char *disks_str = NULL; size_t len = 0; + if (sysctlbyname("kern.disks", NULL, &len, NULL, 0) == 0) { + disks_str = malloc(len); + if (disks_str && sysctlbyname("kern.disks", disks_str, &len, NULL, 0) == 0) { + char *token = strtok(disks_str, " "); + while (token != NULL && disk_count < MAX_DISKS) { + strlcpy(disk_names[disk_count].name, token, sizeof(disk_names[disk_count].name)); + disk_count++; token = strtok(NULL, " "); + } + } + free(disks_str); + } + qsort(disk_names, disk_count, sizeof(StringKey), compare_strings); + + /* Fetch the active kernel mount points table using getfsstat */ + struct statfs *mntbuf = NULL; + int num_mounts = getfsstat(NULL, 0, MNT_NOWAIT); + if (num_mounts > 0) { + size_t bufsize = (num_mounts + 4) * sizeof(struct statfs); + mntbuf = malloc(bufsize); + if (mntbuf) num_mounts = getfsstat(mntbuf, bufsize, MNT_NOWAIT); + } + + /* Create the main root Lua table and its accompanying order tracker */ + lua_newtable(L); lua_newtable(L); + for (int i = 0; i < disk_count; i++) { + lua_pushstring(L, disk_names[i].name); lua_rawseti(L, -2, i + 1); + } + lua_setfield(L, -2, "_order"); + + /* Populate metrics, sub-tables and partitions for each physical disk discovered */ + for (int i = 0; i < disk_count; i++) { + char *current_disk = disk_names[i].name; + + lua_newtable(L); + get_disk_properties(L, current_disk); + + lua_newtable(L); + StringKey part_names[MAX_MOUNTS]; int part_count = 0; + if (mntbuf && num_mounts > 0) { + for (int m = 0; m < num_mounts; m++) { + char *dev_name = mntbuf[m].f_mntfromname; + if (strncmp(dev_name, "/dev/", 5) == 0) dev_name += 5; + char base_disk[MNAMELEN]; get_base_disk(dev_name, base_disk, sizeof(base_disk)); + if (strcmp(base_disk, current_disk) == 0 && part_count < MAX_MOUNTS) { + strlcpy(part_names[part_count].name, dev_name, sizeof(part_names[part_count].name)); + part_count++; + } + } + } + qsort(part_names, part_count, sizeof(StringKey), compare_strings); + + /* Create inner partition index arrays */ + lua_newtable(L); + for (int p = 0; p < part_count; p++) { + lua_pushstring(L, part_names[p].name); lua_rawseti(L, -2, p + 1); + } + lua_setfield(L, -2, "_order"); + + /* Intersect active mount parameters into final sub-objects */ + for (int p = 0; p < part_count; p++) { + for (int m = 0; m < num_mounts; m++) { + char *dev_name = mntbuf[m].f_mntfromname; + if (strncmp(dev_name, "/dev/", 5) == 0) dev_name += 5; + if (strcmp(dev_name, part_names[p].name) == 0) { + lua_newtable(L); + lua_pushstring(L, mntbuf[m].f_mntonname); lua_setfield(L, -2, "path"); + lua_pushstring(L, mntbuf[m].f_fstypename); lua_setfield(L, -2, "type"); + lua_setfield(L, -2, part_names[p].name); break; + } + } + } + lua_setfield(L, -2, "mounts"); lua_setfield(L, -2, current_disk); + } + if (mntbuf) free(mntbuf); + return 1; +} + +/* Official Lua Shared Library registration hooks definition */ +int luaopen_diskinfo(lua_State *L) { + static const struct luaL_Reg mylib[] = { + { "get_info", lua_get_nested_disks }, + { NULL, NULL } + }; + luaL_newlib(L, mylib); + return 1; +} + diff --git a/src/smart.h b/src/smart.h new file mode 100644 index 0000000..9550f17 --- /dev/null +++ b/src/smart.h @@ -0,0 +1,112 @@ +/* + * smart.h - Shared ATA S.M.A.R.T. structures and attribute definitions + * for diskinfo module and smartdump utility. + * + * Supports cross-vendor parsing for HDD (including Helium drives), + * SATA SSD, and Enterprise storage controllers. + */ + +#ifndef _DISKINFO_SMART_H_ +#define _DISKINFO_SMART_H_ + +#include + +#pragma pack(push, 1) +/* 12-byte layout of an individual ATA S.M.A.R.T. attribute */ +struct smart_attribute { + uint8_t id; /* Attribute ID */ + uint16_t status; /* Status flags */ + uint8_t current; /* Normalized Value */ + uint8_t worst; /* Worst Value */ + uint8_t raw[6]; /* 48-bit Raw Value register matrix */ + uint8_t reserv; +}; + +/* 512-byte payload sector returned by SMART READ DATA (0xD0) */ +struct smart_values { + uint16_t rev; /* Structure revision */ + struct smart_attribute attr[30]; /* 30 sequential attributes */ + uint8_t reserved[149];/* Vendor specific padding bytes */ + uint8_t chksum; /* Data integrity checksum */ +}; +#pragma pack(pop) + +/* + * Universal and safe cross-vendor temperature parsing rule. + * Grabs the first raw byte for Seagate's noisy multi-byte registers + * and reads the full 48-bit value for Toshiba/WD/Samsung/Intel, + * safely filtering out vendor-specific debug telemetry. + */ +static inline int parse_smart_temperature(struct smart_attribute *a) { + uint64_t raw_val = 0; + for (int b = 0; b < 6; b++) { + raw_val |= ((uint64_t)a->raw[b]) << (b * 8); + } + + /* If the full 48-bit value is reasonable, return it directly */ + if (raw_val > 0 && raw_val < 150) { + return (int)raw_val; + } + /* Fallback for Seagate: current real temperature is always in the first byte */ + return (int)a->raw[0]; +} + +/* + * Universal ATA/ATAPI SMART Attribute dictionary compliant with T13 standard. + * Declared as static inline to safely include across multiple compilation units. + */ +static inline const char *get_attr_name(uint8_t id) { + switch (id) { + case 1: return "Raw_Read_Error_Rate"; + case 2: return "Throughput_Performance"; + case 3: return "Spin_Up_Time"; + case 4: return "Start_Stop_Count"; + case 5: return "Reallocated_Sector_Ct"; + case 7: return "Seek_Error_Rate"; + case 8: return "Seek_Time_Performance"; + case 9: return "Power_On_Hours"; + case 10: return "Spin_Retry_Count"; + case 12: return "Power_Cycle_Count"; + case 16: return "Helium_Level_WD"; + case 22: return "Current_Helium_Level"; /* HGST / Hitachi Helium Drives */ + case 168: return "SATA_Phy_Error_Count"; + case 170: return "Bad_Block_Count_SSD"; + case 171: return "Program_Fail_Count_SSD"; + case 172: return "Erase_Fail_Count_SSD"; + case 173: return "Wear_Leveling_Count"; /* Universal SSD Wear Counter */ + case 174: return "Unexpected_Power_Loss"; + case 175: return "Power_Loss_Protect_Fail"; /* Intel SSD Capacitor Monitor */ + case 177: return "Wear_Range_Delta"; + case 179: return "Used_Rsvd_Blk_Cnt_Tot"; + case 181: return "Program_Fail_Count_SSD"; + case 182: return "Erase_Fail_Count_SSD"; + case 183: return "SATA_Downshift_Error_Ct"; + case 184: return "End-to-End_Error"; + case 187: return "Reported_Uncorrect"; + case 188: return "Command_Timeout"; + case 189: return "High_Fly_Writes"; + case 190: return "Airflow_Temperature_Cel"; + case 192: return "Power-Off_Retract_Count"; + case 193: return "Load_Cycle_Count"; + case 194: return "Temperature_Celsius"; + case 195: return "Hardware_ECC_Recovered"; + case 196: return "Reallocation_Event_Ct"; + case 197: return "Current_Pending_Sector"; /* Crucial HDD Health Marker */ + case 198: return "Offline_Uncorrectable"; + case 199: return "UDMA_CRC_Error_Count"; /* Interface/Cable Error Counter */ + case 202: return "Percentage_Used_Life"; /* SSD Remaining Life % */ + case 218: return "SATA_CRC_Error_Count"; + case 231: return "SSD_Life_Left"; + case 232: return "Available_Reserv_Space"; /* SSD Available Reserved Flash % */ + case 233: return "Media_Wearout_Indicator"; + case 235: return "POR_Recovery_Count"; /* Samsung SSD Panic Recovery Count */ + case 240: return "Head_Flying_Hours"; + case 241: return "Total_LBAs_Written"; /* NAND TBW Metric */ + case 242: return "Total_LBAs_Read"; /* Host Reads Metric */ + case 244: return "Thermal_Throttle_Count"; /* SSD Overheating Throttling Events */ + default: return "Vendor_Specific"; + } +} + +#endif /* _DISKINFO_SMART_H_ */ + diff --git a/test/camtest.c b/test/camtest.c new file mode 100644 index 0000000..ee5b803 --- /dev/null +++ b/test/camtest.c @@ -0,0 +1,159 @@ +/* + * cc -O2 -Wall camtest.c -lcam -o camtest + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Official FreeBSD CAM Library Interface Headers */ +#include +#include + +static void trim_str(char *str) { + int i = strlen(str) - 1; + while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0')) { + str[i] = '\0'; + i--; + } +} + +static void ata_str_fix(char *str, size_t len) { + for (size_t i = 0; i < len - 1; i += 2) { + char tmp = str[i]; + str[i] = str[i + 1]; + str[i + 1] = tmp; + } + trim_str(str); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s (e.g. ada0 or da0)\n", argv[0]); + return 1; + } + + const char *disk = argv[1]; + printf("=== Testing device via libcam (O_RDWR): %s ===\n\n", disk); + + /* ------------------------------------------------------------- + * TEST 1: Open device via libcam using O_RDWR + * ------------------------------------------------------------- */ + struct cam_device *cam_dev = cam_open_device(disk, O_RDWR); + if (cam_dev == NULL) { + fprintf(stderr, "Error: cam_open_device failed for '%s'.\n", disk); + fprintf(stderr, "Check permissions on /dev/xpt0 and /dev/pass* (group operator/devfs.rules).\n"); + return 1; + } + + union ccb *ccb = cam_getccb(cam_dev); + if (ccb == NULL) { + fprintf(stderr, "Error: Failed to allocate CCB block\n"); + cam_close_device(cam_dev); + return 1; + } + + /* Allocate 512 bytes alignment buffer for responses */ + uint8_t *data_buf = calloc(1, 512); + if (data_buf == NULL) { + fprintf(stderr, "Error: Memory allocation failed\n"); + cam_freeccb(ccb); + cam_close_device(cam_dev); + return 1; + } + + /* ------------------------------------------------------------- + * TEST 2: Pure SCSI INQUIRY + * ------------------------------------------------------------- */ + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + struct scsi_inquiry_data *inq = (struct scsi_inquiry_data *)data_buf; + + cam_fill_csio(&ccb->csio, + /*retries*/ 2, + /*cbfcnp*/ NULL, + /*flags*/ CAM_DIR_IN, + /*tag_action*/ CAM_TAG_ACTION_NONE, + /*data_ptr*/ (uint8_t *)inq, + /*dxfer_len*/ SHORT_INQUIRY_LENGTH, + /*sense_len*/ SSD_FULL_SIZE, + /*cdb_len*/ 6, + /*timeout*/ 5000); + + /* Safely fill raw SCSI INQUIRY command bytes */ + ccb->csio.cdb_io.cdb_bytes[0] = INQUIRY; + ccb->csio.cdb_io.cdb_bytes[4] = SHORT_INQUIRY_LENGTH; + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + char vendor[9], product[17]; + snprintf(vendor, sizeof(vendor), "%.8s", inq->vendor); + snprintf(product, sizeof(product), "%.16s", inq->product); + trim_str(vendor); trim_str(product); + + printf("[SCSI INQUIRY]:\n"); + printf(" Vendor : %s\n", vendor); + printf(" Product : %s\n", product); + printf(" Removable : %s (RMB Bit)\n", (inq->device & SID_RMB) ? "Yes (Flash/SD)" : "No (Fixed/HDD/SSD)"); + } else { + printf("[SCSI INQUIRY]: Failed (CAM Status: 0x%X)\n", ccb->ccb_h.status); + } + + /* ------------------------------------------------------------- + * TEST 3: SCSI SAT ATA PASS-THROUGH (16) -> ATA IDENTIFY + * ------------------------------------------------------------- */ + cam_freeccb(ccb); + ccb = cam_getccb(cam_dev); + if (ccb == NULL) { + free(data_buf); + cam_close_device(cam_dev); + return 1; + } + + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + memset(data_buf, 0, 512); + struct ata_params *ident_buf = (struct ata_params *)data_buf; + + cam_fill_csio(&ccb->csio, + /*retries*/ 2, + /*cbfcnp*/ NULL, + /*flags*/ CAM_DIR_IN, + /*tag_action*/ CAM_TAG_ACTION_NONE, + /*data_ptr*/ (uint8_t *)ident_buf, + /*dxfer_len*/ sizeof(struct ata_params), + /*sense_len*/ SSD_FULL_SIZE, + /*cdb_len*/ 16, + /*timeout*/ 5000); + + /* Construct manual SAT ATA PASS-THROUGH (16) command block array */ + uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes; + cdb[0] = 0x85; /* ATA PASS-THROUGH (16) Opcode */ + cdb[1] = 4 << 1; /* Protocol: PIO Data-In */ + cdb[2] = 0x08 | 0x02; /* T_DIR = 1 (Data In), ByteBlock = 1 (Block mode) */ + cdb[4] = 1; /* Sector Count = 1 sector (512 bytes) */ + cdb[14] = 0xEC; /* ATA Command: IDENTIFY DEVICE */ + + if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { + char ata_model[41]; + memcpy(ata_model, ident_buf->model, sizeof(ident_buf->model)); + ata_model[40] = '\0'; + ata_str_fix(ata_model, sizeof(ata_model)); + + printf("\n[SCSI ATA PASS-THROUGH (16)]:\n"); + printf(" ATA Model : %s\n", ata_model); + printf(" Media RPM : %d (0=SSD, 1=Unknown)\n", ident_buf->media_rotation_rate); + } else { + printf("\n[SCSI ATA PASS-THROUGH (16)]: Failed / rejected by bridge (CAM Status: 0x%X)\n", ccb->ccb_h.status); + } + + free(data_buf); + cam_freeccb(ccb); + cam_close_device(cam_dev); + return 0; +} + diff --git a/test/smartdump.c b/test/smartdump.c new file mode 100644 index 0000000..3ffa743 --- /dev/null +++ b/test/smartdump.c @@ -0,0 +1,154 @@ +/* + * smartdump.c - FreeBSD CLI utility for querying and decoding + * disk hardware S.M.A.R.T. records directly via the CAM subsystem. + * + * Can output raw binary stream for piping or human-readable decoded matrix. + * + * cc -O2 -Wall smartdump.c -lcam -o smartdump + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* FreeBSD CAM Library Interface Headers */ +#include +#include + +/* Import local shared SMART layout matrix definitions */ +#include "../src/smart.h" + +/* Cross-vendor SMART buffer decoder and normalizer */ +static void decode_smart(struct smart_values *smart_buf) { + printf("ID# ATTRIBUTE_NAME VALUE WORST RAW_VALUE\n"); + printf("==================================================================\n"); + + for (int i = 0; i < 30; i++) { + struct smart_attribute *a = &smart_buf->attr[i]; + if (a->id == 0) continue; + + uint64_t raw_val = 0; + + /* Enforce custom cross-vendor temperature extraction rule */ + if (a->id == 194 || a->id == 190) { + raw_val = parse_smart_temperature(a); + } else { + /* Assemble clean 48-bit metric integer for standard counters */ + for (int b = 0; b < 6; b++) { + raw_val |= ((uint64_t)a->raw[b]) << (b * 8); + } + } + + printf("%3d %-23s %3d %3d %llu\n", + a->id, get_attr_name(a->id), a->current, a->worst, (unsigned long long)raw_val); + } +} + +int main(int argc, char **argv) { + int ch; + int mode_decode = 0; + + /* Parse configuration operational runtime flag args */ + while ((ch = getopt(argc, argv, "dr")) != -1) { + switch (ch) { + case 'd': mode_decode = 1; break; + case 'r': mode_decode = 0; break; + default: + fprintf(stderr, "Usage: %s [-d|-r] \n", argv[0]); + return 1; + } + } + argc -= optind; + argv += optind; + + if (argc < 1) { + fprintf(stderr, "Usage: %s [-d|-r] (e.g. ada1 or da0)\n", argv[-optind]); + return 1; + } + + const char *disk = argv[0]; + struct cam_device *cam_dev = NULL; + union ccb *ccb = NULL; + struct smart_values *smart_buf = NULL; + + /* Open device via O_RDWR to unlock transport-layer CCB execution paths */ + cam_dev = cam_open_device(disk, O_RDWR); + if (cam_dev == NULL) { + fprintf(stderr, "Error: cam_open_device failed for '%s'. Check permissions.\n", disk); + return 1; + } + + ccb = cam_getccb(cam_dev); + if (ccb == NULL) { + fprintf(stderr, "Error: Failed to allocate CCB block\n"); + cam_close_device(cam_dev); + return 1; + } + + smart_buf = calloc(1, sizeof(struct smart_values)); + if (smart_buf == NULL) { + fprintf(stderr, "Error: Memory allocation failed\n"); + cam_freeccb(ccb); + cam_close_device(cam_dev); + return 1; + } + + /* --- PROTOCOL BRANCH 1: Native SATA/ATA Devices (ada) --- */ + if (strncmp(disk, "ada", 3) == 0) { + cam_fill_ataio(&ccb->ataio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE, + (uint8_t *)smart_buf, sizeof(struct smart_values), 5000); + + ccb->ataio.cmd.command = 0xB0; /* ATA SMART Command Opcode */ + ccb->ataio.cmd.features = 0xD0; /* SMART READ DATA Command Feature Code */ + ccb->ataio.cmd.lba_mid = 0x4F; /* Subsystem Spec Signature Marker */ + ccb->ataio.cmd.lba_high = 0xC2; /* Subsystem Spec Signature Marker */ + + if (cam_send_ccb(cam_dev, ccb) != 0 || (ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) { + fprintf(stderr, "Error: ATA SMART command failed (CAM Status: 0x%X)\n", ccb->ccb_h.status); + free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1; + } + } + /* --- PROTOCOL BRANCH 2: USB / SCSI Bridges (da) --- */ + else if (strncmp(disk, "da", 2) == 0) { + memset(&ccb->csio, 0, sizeof(struct ccb_scsiio)); + cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE, + (uint8_t *)smart_buf, sizeof(struct smart_values), SSD_FULL_SIZE, 16, 5000); + + uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes; + cdb[0] = 0x85; /* SCSI Opcode: ATA PASS-THROUGH (16) */ + cdb[1] = 4 << 1; /* Protocol: PIO Data-In */ + cdb[2] = 0x08 | 0x02; /* T_DIR = 1 (Data-In), ByteBlock = 1 (Sectors mode) */ + cdb[4] = 0xD0; /* Features (lower) -> SMART READ DATA */ + cdb[6] = 1; /* Sector Count = 1 sector (512 bytes) */ + cdb[8] = 0x4F; /* LBA Mid -> SMART Signature Mid */ + cdb[10] = 0xC2; /* LBA High -> SMART Signature High */ + cdb[14] = 0xB0; /* ATA Command -> SMART Command Opcode */ + + if (cam_send_ccb(cam_dev, ccb) != 0 || (ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) { + fprintf(stderr, "Error: SCSI SAT SMART command failed (CAM Status: 0x%X)\n", ccb->ccb_h.status); + free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1; + } + } else { + fprintf(stderr, "Error: Unsupported device driver type target routing path.\n"); + free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1; + } + + /* Route output generation maps according to active selection flag switches */ + if (mode_decode) { + decode_smart(smart_buf); + } else { + fwrite(smart_buf, 1, 512, stdout); + } + + free(smart_buf); + cam_freeccb(ccb); + cam_close_device(cam_dev); + return 0; +} +