snapraid/cmdline/unixapp.c

3994 lines
94 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2013 Andrea Mazzoleni
#include "os/portable.h"
#ifndef __MINGW32__ /* Only for Unix */
#include "app.h"
#include "support.h"
/****************************************************************************/
/* global */
/**
* Exit codes.
*/
int exit_success = 0;
int exit_failure = 1;
int exit_sync_needed = 2;
/****************************************************************************/
/* signal */
volatile sig_atomic_t global_interrupt = 0;
int os_signal_interrupt(void)
{
return global_interrupt;
}
/* LCOV_EXCL_START */
void app_signal_handler(int signum)
{
/* report the request of interruption with the signal received */
global_interrupt = signum;
}
/* LCOV_EXCL_STOP */
/****************************************************************************/
/* find */
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
#if HAVE_LINUX_DEVICE
static const char* const bcachefs_paths[] = {
#ifdef BCACHEFS_PATH
/* Path configured at build time (e.g. on NixOS). */
BCACHEFS_PATH,
#else
"/usr/sbin/bcachefs",
"/sbin/bcachefs",
"/usr/local/sbin/bcachefs",
"/usr/bin/bcachefs",
"/bin/bcachefs",
"/usr/local/bin/bcachefs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/bcachefs",
#endif
#endif
0
};
static const char* find_bcachefs(void)
{
for (int i = 0; bcachefs_paths[i]; ++i) {
if (eaccess(bcachefs_paths[i], X_OK) == 0) {
return bcachefs_paths[i];
}
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static const char* const zfs_paths[] = {
#ifdef ZFS_PATH
/* Path configured at build time (e.g. on NixOS). */
ZFS_PATH,
#else
"/usr/sbin/zfs",
"/sbin/zfs",
"/usr/local/sbin/zfs",
"/usr/bin/zfs",
"/bin/zfs",
"/usr/local/bin/zfs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zfs",
#endif
#endif
0
};
static const char* find_zfs(void)
{
for (int i = 0; zfs_paths[i]; ++i) {
if (eaccess(zfs_paths[i], X_OK) == 0) {
return zfs_paths[i];
}
}
return 0;
}
static const char* const zpool_paths[] = {
#ifdef ZPOOL_PATH
/* Path configured at build time (e.g. on NixOS). */
ZPOOL_PATH,
#else
"/usr/sbin/zpool",
"/sbin/zpool",
"/usr/local/sbin/zpool",
"/usr/bin/zpool",
"/bin/zpool",
"/usr/local/bin/zpool",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zpool",
#endif
#endif
0
};
static const char* find_zpool(void)
{
for (int i = 0; zpool_paths[i]; ++i) {
if (eaccess(zpool_paths[i], X_OK) == 0) {
return zpool_paths[i];
}
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static const char* smartctl_paths[] = {
#ifdef SMARTCTL_PATH
/* Path configured at build time (e.g. on NixOS). */
SMARTCTL_PATH,
#else
/* Linux & BSD */
"/usr/sbin/smartctl",
"/sbin/smartctl",
"/usr/local/sbin/smartctl",
"/usr/bin/smartctl",
"/usr/local/bin/smartctl",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/smartctl",
#endif
#endif
0
};
static const char* find_smartctl(void)
{
for (int i = 0; smartctl_paths[i]; ++i) {
if (eaccess(smartctl_paths[i], X_OK) == 0) {
return smartctl_paths[i];
}
}
return 0;
}
#endif
/****************************************************************************/
/* sys */
/**
* Read a file from sys
*
* Return -1 on error, otherwise the size of data read
*/
#if HAVE_LINUX_DEVICE
static ssize_t sysread(const char* path, char* buf, size_t buf_size)
{
int f;
int ret;
ssize_t len;
f = open(path, O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
len = read(f, buf, buf_size);
if (len < 0) {
/* LCOV_EXCL_START */
close(f);
return -1;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return len;
}
#endif
#if HAVE_LINUX_DEVICE
/*
* sysread_vpd_pg83 — parse raw binary VPD page 0x83 (Device Identification)
* from a sysfs vpd_pg83 file and extract the first NAA (Network Address
* Authority) designator for the Logical Unit (ASSOCIATION == 0x00).
*/
static int sysattr_vpd_pg80(const char* path, char* dst, size_t dst_size)
{
unsigned char buf[512];
ssize_t ret = sysread(path, (char*)buf, sizeof(buf));
/* need at least the header */
if (ret < 4) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* validate page code */
if (buf[1] != 0x80) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* clamp to what was actually read */
size_t page_len = buf[3];
size_t available = (size_t)ret - 4;
if (available < page_len)
page_len = available;
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* if too bit to store the 0 termination */
if (4 + page_len + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
char* attr = (char*)buf + 4;
attr[page_len] = 0;
strtrim(attr);
/* if empty */
if (!*attr) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(dst, dst_size, attr);
return 0;
}
#endif
/*
* sysread_vpd_pg83 — parse raw binary VPD page 0x83 (Device Identification)
* from a sysfs vpd_pg83 file and extract the first NAA (Network Address
* Authority) designator for the Logical Unit (ASSOCIATION == 0x00).
*
* The result is written into dst as a lowercase hex string,
* e.g.: "5000c500abcdef01" (8-byte / NAA type 1,2,3,5)
* "6000c500abcdef010000000000000000" (16-byte / NAA type 6)
*
* VPD page 0x83 wire format (SPC-4 §7.7.2)
*
* Page header — 4 bytes:
* [0] peripheral qualifier (7:5) | device type (4:0)
* [1] page code = 0x83
* [2] PAGE LENGTH MSB } 16-bit big-endian: number of bytes that follow
* [3] PAGE LENGTH LSB } the header (i.e. total descriptor bytes)
*
* Then a sequence of Designation Descriptors:
* [+0] PROTOCOL ID (7:4) | CODE SET (3:0)
* 0x1 = binary, 0x2 = ASCII, 0x3 = UTF-8
* [+1] PIV (7) | reserved (6) | ASSOCIATION (5:4) | DESIGNATOR TYPE (3:0)
* ASSOCIATION: 0x0 = logical unit ← we want only this
* 0x1 = target port
* 0x2 = target device
* DESIGNATOR TYPE: 0x3 = NAA ← we want only this
* [+2] reserved
* [+3] DESIGNATOR LENGTH = N (bytes that follow in this descriptor)
* [+4 … +4+N-1] DESIGNATOR (binary for NAA)
*
* NAA subtypes (top nibble of designator byte 0)
*
* 0x1 NAA IEEE Extended → 8 bytes
* 0x2 NAA Locally Assigned → 8 bytes
* 0x3 NAA IEEE Registered → 8 bytes (most common on SATA/SAS)
* 0x5 NAA IEEE Registered → 8 bytes
* 0x6 NAA IEEE Registered Ext. → 16 bytes
* others: reserved
*
* The DESIGNATOR LENGTH from the descriptor header must agree with the
* expected length for the NAA subtype. If they disagree, the descriptor
* is malformed and must be skipped.
*
* Returns 0 and fills dst on success, -1 if no valid NAA LU descriptor found.
*/
#if HAVE_LINUX_DEVICE
static int sysattr_vpd_pg83(const char* path, char* dst, size_t dst_size)
{
unsigned char buf[4096];
ssize_t ret = sysread(path, (char*)buf, sizeof(buf));
if (ret < 4) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* validate page code */
if (buf[1] != 0x83) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t page_len = ((size_t)buf[2] << 8) | buf[3];
/* clamp to what was actually read */
size_t available = (size_t)ret - 4;
if (page_len > available)
page_len = available;
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* Walk every Designation Descriptor looking for the first NAA descriptor
* that is associated with the Logical Unit (ASSOCIATION == 0x00).
*
* We take the first one found rather than scoring: all NAA subtypes are
* globally unique by construction; the first LU-associated NAA is the
* canonical device WWN.
*/
size_t offset = 0;
while (offset + 4 <= page_len) {
const unsigned char* desc = buf + 4 + offset;
unsigned code_set = desc[0] & 0x0f;
unsigned assoc = (desc[1] >> 4) & 0x03;
unsigned dtype = desc[1] & 0x0f;
size_t id_len = desc[3];
/* bounds check: the full descriptor must lie within page_len */
if (offset + 4 + id_len > page_len)
break; /* malformed page — stop iterating */
/* skip anything that is not an LU-associated NAA descriptor */
if (assoc != 0x00 || dtype != 0x03)
goto next;
/*
* NAA is always binary (CODE SET == 0x01).
* Reject if the device mis-advertises it as ASCII (defensive).
*/
if (code_set != 0x01)
goto next;
/* if empty */
if (id_len == 0)
goto next;
/*
* Validate NAA subtype vs. expected byte length.
* The NAA value is the top nibble of the first byte of the
* designator (NOT the descriptor header byte).
*/
unsigned naa = (desc[4] >> 4) & 0x0f;
size_t expected = 0;
switch (naa) {
case 0x1 : /* IEEE Extended */
case 0x2 : /* Locally Assigned */
case 0x3 : /* IEEE Registered */
case 0x5 : /* IEEE Registered */
expected = 8;
break;
case 0x6 : /* IEEE Registered Extended */
expected = 16;
break;
default :
goto next;
}
/* DESIGNATOR LENGTH must match the NAA subtype exactly */
if (id_len != expected)
goto next;
if (dst_size < id_len * 2 + 1)
return -1;
for (size_t i = 0; i < id_len; i++)
snprintf(dst + i * 2, 3, "%02x", desc[4 + i]);
dst[id_len * 2] = 0;
return 0;
next:
offset += 4 + id_len;
}
return -1;
}
#endif
/**
* Read a file from sys.
* Trim spaces.
* Always put an ending 0.
* Do not log error on reading.
* Return an error if truncated
*
* Return -1 on error, 0 on success
*/
#if HAVE_LINUX_DEVICE
static int sysattr(const char* path, char* buf, size_t buf_size)
{
ssize_t len;
len = sysread(path, buf, buf_size);
if (len < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)len + 1 > buf_size) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
buf[len] = 0;
strtrim(buf);
return 0;
}
#endif
/******************************************************************************/
/* cpu */
#if HAVE_LINUX_DEVICE
static long long syslong(const char* path)
{
char buf[32];
if (sysattr(path, buf, sizeof(buf)) != 0)
return -1;
char* end;
long long v = strtoll(buf, &end, 10);
if (end == buf)
return -1;
return v;
}
/*
* Parse a sysfs hexadecimal bitmask string (e.g. thread_siblings).
*
* Linux sysfs outputs bitmasks in big-endian 32-bit hex word groups
* (highest CPUs on the left, lowest CPUs on the right).
*
* Left-shifting digits into a 64-bit unsigned long long operates modulo 2^64,
* discarding any high-order hex words (CPUs >= 64) and leaving bits 0..63
* containing the exact mask for CPUs 0..63. Since CPUS_MAX is 64, this mask
* is always accurate for all evaluated CPUs even on systems with > 64 CPUs.
*/
static unsigned long long syshex(const char* path)
{
char buf[1152]; /* enough for 1024 CPUs */
if (sysattr(path, buf, sizeof(buf)) != 0)
return 0;
unsigned long long v = 0;
for (char* p = buf; *p != 0 && *p != '\n'; ++p) {
if (*p == ',')
continue;
v <<= 4;
if (*p >= '0' && *p <= '9')
v |= *p - '0';
else if (*p >= 'a' && *p <= 'f')
v |= *p - 'a' + 10;
else if (*p >= 'A' && *p <= 'F')
v |= *p - 'A' + 10;
else
break; /* hit unexpected character */
}
return v;
}
static int cpu_is_online(int i)
{
char path[256];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/online", i);
long long v = syslong(path);
if (v < 0)
return 1; /* cpu0 has no "online" knob — always on */
return v;
}
/*
* Returns 1 if the specified cpu is the lowest-numbered logical CPU in its
* physical core's SMT sibling set.
*
* thread_siblings is a hex bitmask where bit N = logical cpu N.
* The lowest set bit identifies the primary sibling.
* Safe for up to 64 logical CPUs (fits in unsigned long).
*/
static int cpu_is_primary_sibling(int i)
{
char path[256];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/topology/thread_siblings", i);
unsigned long long mask = syshex(path);
if (mask == 0) /* assume primary */
return 1;
return __builtin_ctzll(mask) == i;
}
#define CPUS_MAX 64
/**
* os_get_optimal_cpu() – returns the logical CPU index of the "fastest"
* core using a three-tier strategy:
*
* Tier 1 – ACPI CPPC preferred-core ranking (hardware-binned quality)
* Tier 2 – cpufreq max-frequency (P-core vs E-core discrimination)
* Tier 3 – SMT topology isolation (primary sibling only)
*/
int os_get_optimal_cpu(void)
{
char path[256];
int num_cpus = sysconf(_SC_NPROCESSORS_CONF);
if (num_cpus <= 0)
return -1;
if (num_cpus > CPUS_MAX)
num_cpus = CPUS_MAX;
/* ACPI CPPC: highest_perf with nominal_perf tiebreaker */
long long max_highest = -1;
long long max_nominal = -1;
int best_cppc = -1;
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/acpi_cppc/highest_perf", i);
long long hp = syslong(path);
if (hp < 0)
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/acpi_cppc/nominal_perf", i);
long long np = syslong(path);
/* np may be missing */
/* tiebreaker priority: highest_perf > nominal_perf > non-CPU0 */
int better = (hp > max_highest)
|| (hp == max_highest && np > max_nominal)
|| (hp == max_highest && np == max_nominal && best_cppc == 0);
if (better) {
max_highest = hp;
max_nominal = np;
best_cppc = i;
}
}
if (best_cppc >= 0)
return best_cppc;
/* max frequency, then SMT isolation */
/* find the global maximum frequency among online CPUs */
long long global_max_freq = -1;
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
long long freq = syslong(path);
if (freq < 0)
continue;
if (freq > global_max_freq)
global_max_freq = freq;
}
/* first online CPU at max freq that is a primary sibling, preferring non-CPU0 */
int best_isolated = -1; /* primary sibling, not CPU 0 */
int best_any = -1; /* any matching CPU, not CPU 0 */
int best_c0 = -1; /* CPU 0 fallback (always a primary sibling) */
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
if (global_max_freq != -1) {
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
if (syslong(path) != global_max_freq)
continue;
}
if (i == 0) {
best_c0 = 0;
} else {
if (best_isolated == -1 && cpu_is_primary_sibling(i))
best_isolated = i;
if (best_any == -1)
best_any = i;
}
}
/* prefer non-CPU0, fall back to CPU0 only if no alternative exists */
if (best_isolated != -1)
return best_isolated;
if (best_any != -1)
return best_any;
if (best_c0 != -1)
return best_c0;
return -1;
}
#endif
/******************************************************************************/
/* resolve */
struct dev_struct {
uint64_t device; /**< Device ID. */
tommy_node node;
};
#if HAVE_LINUX_DEVICE
static int devdereference_btrfs(uint64_t device, const char* dir, int fd, tommy_list* devlist)
{
struct btrfs_ioctl_fs_info_args fs_info;
memset(&fs_info, 0, sizeof(fs_info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &fs_info) < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fs_info.max_id == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
for (__u64 i = 1; i <= fs_info.max_id; ++i) {
struct btrfs_ioctl_dev_info_args dev_info;
memset(&dev_info, 0, sizeof(dev_info));
dev_info.devid = i;
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &dev_info) != 0) {
/* LCOV_EXCL_START */
/* btrfs device IDs (devid) are not guaranteed to be contiguous */
if (errno == ENODEV || errno == ENOENT)
continue; /* this is the expected error on missing id */
log_error(errno, "Ioctl BTRFS_IOC_DEV_INFO failed. %s.", strerror(errno));
goto bail;
/* LCOV_EXCL_STOP */
}
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat((char*)dev_info.path, &st) != 0) {
/* LCOV_EXCL_START */
goto bail;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:btrfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
/* something has to be found */
if (tommy_list_empty(devlist))
goto bail;
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
static int devdereference_bcachefs(uint64_t device, const char* dir, tommy_list* devlist)
{
char resolved[PATH_MAX];
char device_list[PATH_MAX * 8];
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
FILE* fd = fopen("/proc/self/mountinfo", "r");
if (!fd) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* mountinfo format
* 0 - mount ID
* 1 - parent ID
* 2 - major:minor
* 3 - root
* 4 - mount point
* 5 - options
* 6 - "-" (separator)
* 7 - fs
* 8 - mount source - /dev/device
*/
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* id_map[8];
unsigned id_mac;
char* fs_map[8];
unsigned fs_mac;
char* s = fgets(buf, sizeof(buf), fd);
if (s == 0)
break;
/* find the separator position */
char* separator = strstr(s, " - ");
if (!separator)
continue;
/* skip the separator */
*separator = 0;
separator += 3;
/* split the line */
id_mac = strsplit(id_map, 8, s, " \t\n");
fs_mac = strsplit(fs_map, 8, separator, " \t\n");
/* if too short, it's the wrong line */
if (id_mac < 5)
continue;
if (fs_mac < 2)
continue;
/* mount point must contain the directory */
const char* mp = id_map[4];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* it should be bcachefs */
const char* fs = fs_map[0];
if (strcmp(fs, "bcachefs") != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
pathcpy(device_list, sizeof(device_list), fs_map[1]);
}
}
fclose(fd);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* split the device list */
char* dev_map[64];
unsigned dev_mac = strsplit(dev_map, 64, device_list, ":");
for (unsigned i = 0; i < dev_mac; ++i) {
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat(dev_map[i], &st) != 0) {
/* LCOV_EXCL_START */
goto bail;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:bcachefs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
/* something has to be found */
if (tommy_list_empty(devlist))
goto bail;
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
static int extract_zfs(const char* dir, char* dataset, size_t dataset_size, char* uuid, size_t uuid_size, char* mount_point, size_t mount_point_size)
{
char resolved[PATH_MAX];
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* list all ZFS filesystems in one shot */
const char* argv[] = {
zfs,
"list",
"-H",
"-o",
"name,guid,mountpoint",
"-t",
"filesystem",
0
};
OS_FILE* fp = os_popen(argv);
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* map[3];
unsigned mac;
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
/* split the line */
mac = strsplit(map, 3, s, " \t\n");
if (mac < 3)
continue;
const char* mp = map[2];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
if (dataset)
pathcpy(dataset, dataset_size, map[0]);
if (uuid)
pathcpy(uuid, uuid_size, map[1]);
if (mount_point)
pathcpy(mount_point, mount_point_size, map[2]);
}
}
os_pclose(fp);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
static int devdereference_zfs(uint64_t device, const char* dir, tommy_list* devlist)
{
char pool[PATH_MAX];
const char* zpool = find_zpool();
if (!zpool) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (extract_zfs(dir, pool, sizeof(pool), 0, 0, 0, 0) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* extract pool name */
char* slash = strchr(pool, '/');
if (slash)
*slash = 0;
const char* argv[] = {
zpool,
"status",
"-P",
pool,
0
};
OS_FILE* fp = os_popen(argv);
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
while (1) {
char buf[PATH_MAX * 2 + 64];
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
char* file = strstr(s, "/dev/");
if (!file)
continue;
size_t i = 0;
while (file[i] != 0 && file[i] != ' ' && file[i] != '\t' && file[i] != '\n')
++i;
file[i] = 0;
struct stat st;
if (stat(file, &st) != 0) {
/* LCOV_EXCL_START */
os_pclose(fp);
goto bail;
/* LCOV_EXCL_STOP */
}
if (!S_ISBLK(st.st_mode)) {
/* LCOV_EXCL_START */
os_pclose(fp);
goto bail;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:zfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
os_pclose(fp);
if (tommy_list_empty(devlist))
goto bail;
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
/**
* Get the devices of a virtual device.
*
* This is intended to resolve the case of Btrfs/ZFS filesystems that
* create a virtual superblock (major==0) not backed by any low
* level device.
*
* See:
* Bug 711881 - too funny btrfs st_dev numbers
* https://bugzilla.redhat.com/show_bug.cgi?id=711881
*/
static int devdereference(uint64_t device, const char* dir, tommy_list* devlist)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
int ret;
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
ret = devdereference_btrfs(device, dir, fd, devlist);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
ret = devdereference_bcachefs(device, dir, devlist);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
ret = devdereference_zfs(device, dir, devlist);
} else {
/* insert the device itself in the list */
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = device;
tommy_list_insert_tail(devlist, &dev->node, dev);
ret = 0;
}
close(fd);
return ret;
}
#endif
/**
* Read a file extracting the specified tag TAG=VALUE format.
* Return !=0 on error.
*/
#if HAVE_LINUX_DEVICE
static int tagread(const char* path, const char* tag, char* value, size_t value_size)
{
ssize_t ret;
char buf[512];
size_t tag_len;
char* i;
char* e;
ret = sysread(path, buf, sizeof(buf));
if (ret < 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
/* ending 0 */
buf[ret] = 0;
tag_len = strlen(tag);
for (i = buf; *i; ++i) {
char* p = i;
/* start with a space */
if (p != buf) {
if (!isspace((unsigned char)*p))
continue;
++p;
}
if (strncmp(p, tag, tag_len) != 0)
continue;
p += tag_len;
/* end with a = */
if (*p != '=')
continue;
++p;
/* found */
i = p;
break;
}
if (!*i) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Missing tag '%s' for '%s'.\n", tag, path);
return -1;
/* LCOV_EXCL_STOP */
}
/* terminate at the first space */
e = i;
while (*e != 0 && !isspace((unsigned char)*e))
++e;
*e = 0;
if (!*i) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Empty tag '%s' for '%s'.\n", tag, path);
return -1;
/* LCOV_EXCL_STOP */
}
pathprint(value, value_size, "%s", i);
return 0;
}
#endif
/**
* Get the device file from the device number.
*
* It uses /sys/dev/block/.../uevent.
*
* For null device (major==0) it fails.
*/
#if HAVE_LINUX_DEVICE
static int devresolve_sys(dev_t device, char* path, size_t path_size)
{
struct stat st;
char buf[PATH_MAX];
/* default device path from device number */
pathprint(path, path_size, "/sys/dev/block/%u:%u/uevent", major(device), minor(device));
if (tagread(path, "DEVNAME", buf, sizeof(buf)) != 0) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: failed to read DEVNAME tag '%s'\n", major(device), minor(device), path);
return -1;
/* LCOV_EXCL_STOP */
}
/* set the real device path */
pathprint(path, path_size, "/dev/%s", buf);
/* check the device */
if (stat(path, &st) != 0) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: failed to stat '%s'\n", major(device), minor(device), path);
return -1;
/* LCOV_EXCL_STOP */
}
if (st.st_rdev != device) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: unexpected device '%u:%u' for '%s'.\n", major(device), minor(device), major(st.st_rdev), minor(st.st_rdev), path);
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("resolve:sys:%u:%u:%s: found\n", major(device), minor(device), path);
return 0;
}
#endif
/**
* Get the device file from the device number.
*/
#if HAVE_LINUX_DEVICE
static int devresolve(uint64_t device, char* path, size_t path_size)
{
if (devresolve_sys(device, path, path_size) == 0)
return 0;
return -1;
}
#endif
/**
* Cache used by blkid.
*/
#if HAVE_BLKID
static blkid_cache cache = 0;
#endif
/****************************************************************************/
/* uuid */
/**
* Get the filesystem UUID using the /dev/disk/by-uuid/ links.
* It doesn't require root permission, and the UUIDs are always updated.
*
* NOTE: This method reads the filesystem-level UUID (unlike /dev/disk/by-partuuid/
* which reads the GPT partition table slot).
*
* This method WILL reliably work for standard single-device filesystems:
* - ext2 / ext3 / ext4
* - XFS
* - F2FS (Flash-Friendly File System)
* - NTFS
* - FAT32 / exFAT (via their volume serial number formatted as a UUID string)
*
* This method will NOT find an entry for:
* - Network filesystems (NFS, SMB/CIFS, CephFS)
* - Virtual/In-memory filesystems (tmpfs, proc, sysfs)
* - ZFS (bypasses the Linux block layer and uses internal GUIDs)
* - Raw flash filesystems (JFFS2, UBIFS)
*
* For multi-device filesystems like Btrfs and Bcachefs, all member devices
* share the same filesystem UUID. Because /dev/disk/by-uuid/ cannot have duplicate
* filename entries, only ONE arbitrary device from the pool will be present here.
*/
#if HAVE_LINUX_DEVICE
static int devuuid_dev(uint64_t device, char* uuid, size_t uuid_size)
{
ssize_t ret;
DIR* d;
struct dirent* dd;
struct stat st;
/* scan the UUID directory searching for the device */
d = opendir("/dev/disk/by-uuid");
if (!d) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: opendir(/dev/disk/by-uuid) failed, %s\n", major(device), minor(device), strerror(errno));
/* directory missing?, likely we are not in Linux */
return -1;
/* LCOV_EXCL_STOP */
}
int dir_fd = dirfd(d);
if (dir_fd == -1) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: dirfd(/dev/disk/by-uuid) failed, %s\n", major(device), minor(device), strerror(errno));
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
while ((dd = readdir(d)) != 0) {
/* skip "." and ".." files, UUIDs never start with '.' */
if (dd->d_name[0] == '.')
continue;
ret = fstatat(dir_fd, dd->d_name, &st, 0);
if (ret != 0) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: fstatat(%s) failed, %s\n", major(device), minor(device), dd->d_name, strerror(errno));
/* generic error, ignore and continue the search */
continue;
/* LCOV_EXCL_STOP */
}
/* if it matches, we have the uuid */
if (S_ISBLK(st.st_mode) && st.st_rdev == (dev_t)device) {
char buf[PATH_MAX];
char path[PATH_MAX];
/* resolve the link */
pathprint(path, sizeof(path), "/dev/disk/by-uuid/%s", dd->d_name);
ret = readlink(path, buf, sizeof(buf));
if (ret < 0 || ret >= PATH_MAX) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: readlink(/dev/disk/by-uuid/%s) failed, %s\n", major(device), minor(device), dd->d_name, strerror(errno));
/* generic error, ignore and continue the search */
continue;
/* LCOV_EXCL_STOP */
}
buf[ret] = 0;
/* found */
pathcpy(uuid, uuid_size, dd->d_name);
log_tag("uuid:by-uuid:%u:%u:%s: found %s\n", major(device), minor(device), uuid, buf);
closedir(d);
return 0;
}
}
log_tag("uuid:by-uuid:%u:%u: /dev/disk/by-uuid doesn't contain a matching block device\n", major(device), minor(device));
/* not found */
closedir(d);
return -1;
}
#endif
/**
* Get the UUID using libblkid.
* It uses a cache to work without root permission, resulting in UUID
* not necessarily recent.
* We could call blkid_probe_all() to refresh the UUID, but it would
* require root permission to read the superblocks, and resulting in
* all the disks spinning.
*/
#if HAVE_BLKID
static int devuuid_blkid(uint64_t device, char* uuid, size_t uuid_size)
{
char* devname;
char* uuidname;
devname = blkid_devno_to_devname(device);
if (!devname) {
/* LCOV_EXCL_START */
log_tag("uuid:blkid:%u:%u: blkid_devno_to_devname() failed, %s\n", major(device), minor(device), strerror(errno));
/* device mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
uuidname = blkid_get_tag_value(cache, "UUID", devname);
if (!uuidname) {
/* LCOV_EXCL_START */
log_tag("uuid:blkid:%u:%u: blkid_get_tag_value(UUID,%s) failed, %s\n", major(device), minor(device), devname, strerror(errno));
/* uuid mapping failed */
free(devname);
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(uuid, uuid_size, uuidname);
log_tag("uuid:blkid:%u:%u:%s: found %s\n", major(device), minor(device), uuid, devname);
free(devname);
free(uuidname);
return 0;
}
#endif
/**
* Get the LABEL using libblkid.
* It uses a cache to work without root permission, resulting in LABEL
* not necessarily recent.
*/
#if HAVE_BLKID
static int devuuid_label(uint64_t device, char* label, size_t label_size)
{
char* devname;
char* labelname;
devname = blkid_devno_to_devname(device);
if (!devname) {
/* LCOV_EXCL_START */
log_tag("label:blkid:%u:%u: blkid_devno_to_devname() failed, %s\n", major(device), minor(device), strerror(errno));
/* device mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
labelname = blkid_get_tag_value(cache, "LABEL", devname);
if (!labelname) {
/* LCOV_EXCL_START */
log_tag("label:blkid:%u:%u: blkid_get_tag_value(LABEL,%s) failed, %s\n", major(device), minor(device), devname, strerror(errno));
free(devname);
/* label mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(label, label_size, labelname);
log_tag("label:blkid:%u:%u:%s: found %s\n", major(device), minor(device), label, devname);
free(devname);
free(labelname);
return 0;
}
#endif
/**
* Get the LABEL using the generic Linux FS_IOC_GETFSLABEL ioctl.
*/
#if HAVE_LINUX_DEVICE
#ifndef FSLABEL_MAX
#define FSLABEL_MAX 256
#endif
#ifndef FS_IOC_GETFSLABEL
#define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
#endif
static int devlabel_ioctl(const char* path, char* label, size_t label_size)
{
char label_buf[FSLABEL_MAX];
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0 && (errno == ENOENT || errno == ENOTDIR)) {
char dir[PATH_MAX];
char* slash;
if (strlen(path) + 1 > sizeof(dir)) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
strcpy(dir, path);
slash = strrchr(dir, '/');
if (slash) {
*slash = 0;
fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
}
}
if (fd < 0) {
/* LCOV_EXCL_START */
log_tag("label:ioctl: open(%s) failed, %s\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
memset(label_buf, 0, sizeof(label_buf));
if (ioctl(fd, FS_IOC_GETFSLABEL, label_buf) < 0) {
log_tag("label:ioctl: FS_IOC_GETFSLABEL(%s) failed, %s\n", path, strerror(errno));
close(fd);
return -1;
}
close(fd);
if (label_buf[0] == 0) {
log_tag("label:ioctl:%s: empty label\n", path);
return -1;
}
if (strlen(label_buf) + 1 > label_size) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
strcpy(label, label_buf);
log_tag("label:ioctl:%s: found %s\n", path, label);
return 0;
}
#endif
#ifdef __APPLE__
static int devuuid_darwin(const char* path, char* uuid, size_t uuid_size)
{
CFStringRef path_apple = 0;
DASessionRef session = 0;
CFURLRef path_appler = 0;
int result = -1;
*uuid = 0;
path_apple = CFStringCreateWithCString(kCFAllocatorDefault, path, kCFStringEncodingUTF8);
if (!path_apple)
goto bail;
session = DASessionCreate(kCFAllocatorDefault);
if (!session)
goto bail;
path_appler = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path_apple, kCFURLPOSIXPathStyle, false);
if (!path_appler)
goto bail;
/* find the mount point */
DADiskRef disk = NULL;
while (path_appler) {
disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, path_appler);
if (disk)
break;
CFURLRef parent = CFURLCreateCopyDeletingLastPathComponent(kCFAllocatorDefault, path_appler);
if (!parent)
break;
/* check if we hit the root (parent == child) */
if (CFEqual(parent, path_appler)) {
CFRelease(parent);
break;
}
CFRelease(path_appler);
path_appler = parent;
}
/* safely extract the UUID */
if (disk) {
CFDictionaryRef description = DADiskCopyDescription(disk);
if (description) {
/* key might not exist for NTFS/ExFAT on some drivers */
const void* value = (CFUUIDRef)CFDictionaryGetValue(description, kDADiskDescriptionVolumeUUIDKey);
CFStringRef uuid_string = NULL;
if (value) {
if (CFGetTypeID(value) == CFUUIDGetTypeID()) {
uuid_string = CFUUIDCreateString(kCFAllocatorDefault, (CFUUIDRef)value);
} else if (CFGetTypeID(value) == CFStringGetTypeID()) {
/* if it's already a string, retain it so we can release it later consistently */
uuid_string = (CFStringRef)CFRetain(value);
}
}
if (uuid_string) {
if (CFStringGetCString(uuid_string, uuid, uuid_size, kCFStringEncodingUTF8)) {
result = 0; /* success */
}
CFRelease(uuid_string);
}
CFRelease(description);
}
CFRelease(disk);
}
bail:
/* clean up */
if (path_appler) CFRelease(path_appler);
if (session) CFRelease(session);
if (path_apple) CFRelease(path_apple);
return result;
}
#endif
#if HAVE_LINUX_DEVICE
static int devuuid_btrfs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP*/
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type != BTRFS_SUPER_MAGIC) {
close(fd);
return -1;
}
struct btrfs_ioctl_fs_info_args info;
memset(&info, 0, sizeof(info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &info) < 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(uuid, uuid_size,
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
info.fsid[0], info.fsid[1], info.fsid[2], info.fsid[3],
info.fsid[4], info.fsid[5],
info.fsid[6], info.fsid[7],
info.fsid[8], info.fsid[9],
info.fsid[10], info.fsid[11], info.fsid[12], info.fsid[13], info.fsid[14], info.fsid[15]
);
log_tag("uuid:by-btrfs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
close(fd);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
struct bch_ioctl_query_uuid {
__u8 uuid[16];
};
#define BCH_IOCTL_QUERY_UUID _IOR(0xbc, 1, struct bch_ioctl_query_uuid)
static int devuuid_bcachefs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP*/
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type != BCACHEFS_SUPER_MAGIC) {
close(fd);
return -1;
}
struct bch_ioctl_query_uuid info;
memset(&info, 0, sizeof(info));
if (ioctl(fd, BCH_IOCTL_QUERY_UUID, &info) < 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd);
snprintf(uuid, uuid_size,
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
info.uuid[0], info.uuid[1], info.uuid[2], info.uuid[3],
info.uuid[4], info.uuid[5],
info.uuid[6], info.uuid[7],
info.uuid[8], info.uuid[9],
info.uuid[10], info.uuid[11], info.uuid[12], info.uuid[13], info.uuid[14], info.uuid[15]
);
log_tag("uuid:by-bcachefs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
int devuuid_zfs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
if (extract_zfs(dir, 0, 0, uuid, uuid_size, 0, 0) != 0)
return -1;
log_tag("uuid:by-zfs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
return 0;
}
#endif
int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid_size)
{
(void)device_path;
(void)device_id;
#ifdef __APPLE__
if (devuuid_darwin(device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_btrfs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_bcachefs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_zfs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_dev(device_id, uuid, uuid_size) == 0)
return 0;
#else
log_tag("uuid:by-uuid:%u:%u: by-uuid not supported\n", major(device_id), minor(device_id));
#endif
#if HAVE_BLKID
if (devuuid_blkid(device_id, uuid, uuid_size) == 0)
return 0;
#else
log_tag("uuid:blkid:%u:%u: blkid support not compiled in\n", major(device_id), minor(device_id));
#endif
log_tag("uuid:notfound:%u:%u:\n", major(device_id), minor(device_id));
/* not supported */
(void)uuid;
(void)uuid_size;
return -1;
}
/****************************************************************************/
/* fs */
/* from man statfs */
#define ADFS_SUPER_MAGIC 0xadf5
#define AFFS_SUPER_MAGIC 0xADFF
#define BDEVFS_MAGIC 0x62646576
#define BEFS_SUPER_MAGIC 0x42465331
#define BFS_MAGIC 0x1BADFACE
#define BINFMTFS_MAGIC 0x42494e4d
#define CGROUP_SUPER_MAGIC 0x27e0eb
#define CIFS_MAGIC_NUMBER 0xFF534D42
#define CODA_SUPER_MAGIC 0x73757245
#define COH_SUPER_MAGIC 0x012FF7B7
#define CRAMFS_MAGIC 0x28cd3d45
#define DEBUGFS_MAGIC 0x64626720
#define DEVFS_SUPER_MAGIC 0x1373
#define DEVPTS_SUPER_MAGIC 0x1cd1
#define EFIVARFS_MAGIC 0xde5e81e4
#define EFS_SUPER_MAGIC 0x00414A53
#define EXT_SUPER_MAGIC 0x137D
#define EXT2_OLD_SUPER_MAGIC 0xEF51
#define EXT4_SUPER_MAGIC 0xEF53 /* also ext2/ext3 */
#define FUSE_SUPER_MAGIC 0x65735546
#define FUTEXFS_SUPER_MAGIC 0xBAD1DEA
#define HFS_SUPER_MAGIC 0x4244
#define HFSPLUS_SUPER_MAGIC 0x482b
#define HOSTFS_SUPER_MAGIC 0x00c0ffee
#define HPFS_SUPER_MAGIC 0xF995E849
#define HUGETLBFS_MAGIC 0x958458f6
#define ISOFS_SUPER_MAGIC 0x9660
#define JFFS2_SUPER_MAGIC 0x72b6
#define JFS_SUPER_MAGIC 0x3153464a
#define MINIX_SUPER_MAGIC 0x137F
#define MINIX_SUPER_MAGIC2 0x138F
#define MINIX2_SUPER_MAGIC 0x2468
#define MINIX2_SUPER_MAGIC2 0x2478
#define MINIX3_SUPER_MAGIC 0x4d5a
#define MQUEUE_MAGIC 0x19800202
#define MSDOS_SUPER_MAGIC 0x4d44
#define NCP_SUPER_MAGIC 0x564c
#define NFS_SUPER_MAGIC 0x6969
#define NILFS_SUPER_MAGIC 0x3434
#define NTFS_SB_MAGIC 0x5346544e
#define OCFS2_SUPER_MAGIC 0x7461636f
#define OPENPROM_SUPER_MAGIC 0x9fa1
#define PIPEFS_MAGIC 0x50495045
#define PROC_SUPER_MAGIC 0x9fa0
#define PSTOREFS_MAGIC 0x6165676C
#define QNX4_SUPER_MAGIC 0x002f
#define QNX6_SUPER_MAGIC 0x68191122
#define RAMFS_MAGIC 0x858458f6
#define REISERFS_SUPER_MAGIC 0x52654973
#define ROMFS_MAGIC 0x7275
#define SELINUX_MAGIC 0xf97cff8c
#define SMACK_MAGIC 0x43415d53
#define SMB_SUPER_MAGIC 0x517B
#define SOCKFS_MAGIC 0x534F434B
#define SQUASHFS_MAGIC 0x73717368
#define SYSFS_MAGIC 0x62656572
#define SYSV2_SUPER_MAGIC 0x012FF7B6
#define SYSV4_SUPER_MAGIC 0x012FF7B5
#define TMPFS_MAGIC 0x01021994
#define UDF_SUPER_MAGIC 0x15013346
#define UFS_MAGIC 0x00011954
#define USBDEVICE_SUPER_MAGIC 0x9fa2
#define V9FS_MAGIC 0x01021997
#define VXFS_SUPER_MAGIC 0xa501FCF5
#define XENFS_SUPER_MAGIC 0xabba1974
#define XENIX_SUPER_MAGIC 0x012FF7B4
#define XFS_SUPER_MAGIC 0x58465342
#define _XIAFS_SUPER_MAGIC 0x012FD16D
#define AFS_SUPER_MAGIC 0x5346414F
#define AUFS_SUPER_MAGIC 0x61756673
#define ANON_INODE_FS_SUPER_MAGIC 0x09041934
#define CEPH_SUPER_MAGIC 0x00C36400
#define ECRYPTFS_SUPER_MAGIC 0xF15F
#define FAT_SUPER_MAGIC 0x4006
#define FHGFS_SUPER_MAGIC 0x19830326
#define FUSEBLK_SUPER_MAGIC 0x65735546
#define FUSECTL_SUPER_MAGIC 0x65735543
#define GFS_SUPER_MAGIC 0x1161970
#define GPFS_SUPER_MAGIC 0x47504653
#define MTD_INODE_FS_SUPER_MAGIC 0x11307854
#define INOTIFYFS_SUPER_MAGIC 0x2BAD1DEA
#define ISOFS_R_WIN_SUPER_MAGIC 0x4004
#define ISOFS_WIN_SUPER_MAGIC 0x4000
#define JFFS_SUPER_MAGIC 0x07C0
#define KAFS_SUPER_MAGIC 0x6B414653
#define LUSTRE_SUPER_MAGIC 0x0BD00BD0
#define NFSD_SUPER_MAGIC 0x6E667364
#define PANFS_SUPER_MAGIC 0xAAD7AAEA
#define RPC_PIPEFS_SUPER_MAGIC 0x67596969
#define SECURITYFS_SUPER_MAGIC 0x73636673
#define UFS_BYTESWAPPED_SUPER_MAGIC 0x54190100
#define VMHGFS_SUPER_MAGIC 0xBACBACBC
#define VZFS_SUPER_MAGIC 0x565A4653
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
struct filesystem_entry {
unsigned id;
const char* name;
int remote;
} FILESYSTEMS[] = {
{ ADFS_SUPER_MAGIC, "adfs", 0 },
{ AFFS_SUPER_MAGIC, "affs", 0 },
{ AFS_SUPER_MAGIC, "afs", 1 },
{ AUFS_SUPER_MAGIC, "aufs", 1 },
{ BEFS_SUPER_MAGIC, "befs", 0 },
{ BDEVFS_MAGIC, "bdevfs", 0 },
{ BFS_MAGIC, "bfs", 0 },
{ BINFMTFS_MAGIC, "binfmt_misc", 0 },
{ BTRFS_SUPER_MAGIC, "btrfs", 0 },
{ BCACHEFS_SUPER_MAGIC, "bcachefs", 0 },
{ CEPH_SUPER_MAGIC, "ceph", 1 },
{ CGROUP_SUPER_MAGIC, "cgroupfs", 0 },
{ CIFS_MAGIC_NUMBER, "cifs", 1 },
{ CODA_SUPER_MAGIC, "coda", 1 },
{ COH_SUPER_MAGIC, "coh", 0 },
{ CRAMFS_MAGIC, "cramfs", 0 },
{ DEBUGFS_MAGIC, "debugfs", 0 },
{ DEVFS_SUPER_MAGIC, "devfs", 0 },
{ DEVPTS_SUPER_MAGIC, "devpts", 0 },
{ ECRYPTFS_SUPER_MAGIC, "ecryptfs", 0 },
{ EFS_SUPER_MAGIC, "efs", 0 },
{ EXT_SUPER_MAGIC, "ext", 0 },
{ EXT4_SUPER_MAGIC, "ext4", 0 },
{ EXT2_OLD_SUPER_MAGIC, "ext2", 0 },
{ FAT_SUPER_MAGIC, "fat", 0 },
{ FHGFS_SUPER_MAGIC, "fhgfs", 1 },
{ FUSEBLK_SUPER_MAGIC, "fuseblk", 1 },
{ FUSECTL_SUPER_MAGIC, "fusectl", 1 },
{ FUTEXFS_SUPER_MAGIC, "futexfs", 0 },
{ GFS_SUPER_MAGIC, "gfs/gfs2", 1 },
{ GPFS_SUPER_MAGIC, "gpfs", 1 },
{ HFS_SUPER_MAGIC, "hfs", 0 },
{ HFSPLUS_SUPER_MAGIC, "hfsplus", 0 },
{ HPFS_SUPER_MAGIC, "hpfs", 0 },
{ HUGETLBFS_MAGIC, "hugetlbfs", 0 },
{ MTD_INODE_FS_SUPER_MAGIC, "inodefs", 0 },
{ INOTIFYFS_SUPER_MAGIC, "inotifyfs", 0 },
{ ISOFS_SUPER_MAGIC, "isofs", 0 },
{ ISOFS_R_WIN_SUPER_MAGIC, "isofs", 0 },
{ ISOFS_WIN_SUPER_MAGIC, "isofs", 0 },
{ JFFS_SUPER_MAGIC, "jffs", 0 },
{ JFFS2_SUPER_MAGIC, "jffs2", 0 },
{ JFS_SUPER_MAGIC, "jfs", 0 },
{ KAFS_SUPER_MAGIC, "k-afs", 1 },
{ LUSTRE_SUPER_MAGIC, "lustre", 1 },
{ MINIX_SUPER_MAGIC, "minix", 0 },
{ MINIX_SUPER_MAGIC2, "minix", 0 },
{ MINIX2_SUPER_MAGIC, "minix2", 0 },
{ MINIX2_SUPER_MAGIC2, "minix2", 0 },
{ MINIX3_SUPER_MAGIC, "minix3", 0 },
{ MQUEUE_MAGIC, "mqueue", 0 },
{ MSDOS_SUPER_MAGIC, "msdos", 0 },
{ NCP_SUPER_MAGIC, "novell", 1 },
{ NFS_SUPER_MAGIC, "nfs", 1 },
{ NFSD_SUPER_MAGIC, "nfsd", 1 },
{ NILFS_SUPER_MAGIC, "nilfs", 0 },
{ NTFS_SB_MAGIC, "ntfs", 0 },
{ OPENPROM_SUPER_MAGIC, "openprom", 0 },
{ OCFS2_SUPER_MAGIC, "ocfs2", 1 },
{ PANFS_SUPER_MAGIC, "panfs", 1 },
{ PIPEFS_MAGIC, "pipefs", 1 },
{ PROC_SUPER_MAGIC, "proc", 0 },
{ PSTOREFS_MAGIC, "pstorefs", 0 },
{ QNX4_SUPER_MAGIC, "qnx4", 0 },
{ QNX6_SUPER_MAGIC, "qnx6", 0 },
{ RAMFS_MAGIC, "ramfs", 0 },
{ REISERFS_SUPER_MAGIC, "reiserfs", 0 },
{ ROMFS_MAGIC, "romfs", 0 },
{ RPC_PIPEFS_SUPER_MAGIC, "rpc_pipefs", 0 },
{ SECURITYFS_SUPER_MAGIC, "securityfs", 0 },
{ SELINUX_MAGIC, "selinux", 0 },
{ SMB_SUPER_MAGIC, "smb", 1 },
{ SOCKFS_MAGIC, "sockfs", 0 },
{ SQUASHFS_MAGIC, "squashfs", 0 },
{ SYSFS_MAGIC, "sysfs", 0 },
{ SYSV2_SUPER_MAGIC, "sysv2", 0 },
{ SYSV4_SUPER_MAGIC, "sysv4", 0 },
{ TMPFS_MAGIC, "tmpfs", 0 },
{ UDF_SUPER_MAGIC, "udf", 0 },
{ UFS_MAGIC, "ufs", 0 },
{ UFS_BYTESWAPPED_SUPER_MAGIC, "ufs", 0 },
{ USBDEVICE_SUPER_MAGIC, "usbdevfs", 0 },
{ V9FS_MAGIC, "v9fs", 0 },
{ VMHGFS_SUPER_MAGIC, "vmhgfs", 1 },
{ VXFS_SUPER_MAGIC, "vxfs", 0 },
{ VZFS_SUPER_MAGIC, "vzfs", 0 },
{ XENFS_SUPER_MAGIC, "xenfs", 0 },
{ XENIX_SUPER_MAGIC, "xenix", 0 },
{ XFS_SUPER_MAGIC, "xfs", 0 },
{ _XIAFS_SUPER_MAGIC, "xia", 0 },
{ ZFS_SUPER_MAGIC, "zfs", 0 },
{ 0 }
};
int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space, char* fstype, size_t fstype_size, char* fslabel, size_t fslabel_size)
{
#if HAVE_STATFS
struct statfs st;
if (statfs(path, &st) != 0) {
char dir[PATH_MAX];
char* slash;
if (errno != ENOENT) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* If it doesn't exist, we assume a file
* and we check for the containing dir
*/
if (strlen(path) + 1 > sizeof(dir)) {
/* LCOV_EXCL_START */
errno = ENAMETOOLONG;
return -1;
/* LCOV_EXCL_STOP */
}
strcpy(dir, path);
slash = strrchr(dir, '/');
if (!slash) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
*slash = 0;
if (statfs(dir, &st) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
}
#endif
/* to get the fs type check "man stat" or "stat -f -t FILE" */
if (has_persistent_inode) {
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
switch (st.f_type) {
case FUSEBLK_SUPER_MAGIC : /* FUSE, "fuseblk" in the stat command */
case MSDOS_SUPER_MAGIC : /* VFAT, "msdos" in the stat command */
*has_persistent_inode = 0;
break;
default :
/* by default assume yes */
*has_persistent_inode = 1;
break;
}
#else
/* in Unix inodes are persistent by default */
*has_persistent_inode = 1;
#endif
}
if (has_syncronized_hardlinks) {
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
switch (st.f_type) {
case NTFS_SB_MAGIC : /* NTFS */
case MSDOS_SUPER_MAGIC : /* VFAT, "msdos" in the stat command */
*has_syncronized_hardlinks = 0;
break;
default :
/* by default assume yes */
*has_syncronized_hardlinks = 1;
break;
}
#else
/* in Unix hardlinks share the same metadata by default */
*has_syncronized_hardlinks = 1;
#endif
}
if (total_space) {
#if HAVE_STATFS
*total_space = st.f_bsize * (uint64_t)st.f_blocks;
#else
*total_space = 0;
#endif
}
if (free_space) {
#if HAVE_STATFS
*free_space = st.f_bsize * (uint64_t)st.f_bfree;
#else
*free_space = 0;
#endif
}
const char* ptype = 0;
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_FSTYPENAME
/* get the filesystem type directly from the struct (Mac OS X) */
ptype = st.f_fstypename;
#elif HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
/*
* Get the filesystem type from f_type (Linux)
* from: https://github.com/influxdata/gopsutil/blob/master/disk/disk_linux.go
*/
for (int i = 0; FILESYSTEMS[i].id != 0; ++i) {
if (st.f_type == FILESYSTEMS[i].id) {
ptype = FILESYSTEMS[i].name;
break;
}
}
#endif
if (fstype) {
fstype[0] = 0;
if (ptype)
snprintf(fstype, fstype_size, "%s", ptype);
}
if (fslabel) {
fslabel[0] = 0;
#if HAVE_LINUX_DEVICE
if (devlabel_ioctl(path, fslabel, fslabel_size) != 0)
#endif
#if HAVE_BLKID
{
struct stat fst;
if (stat(path, &fst) == 0)
devuuid_label(fst.st_dev, fslabel, fslabel_size);
}
#else
(void)fslabel_size;
#endif
}
return 0;
}
/****************************************************************************/
/* snapshot */
#if HAVE_LINUX_DEVICE
static int fssnapshot_inode(const char* path, uint32_t magic, uint64_t root_inode, struct fssnapshot_struct* fss)
{
char current_path[PATH_MAX];
pathcpy(current_path, sizeof(current_path), path);
/* walk up the directory tree to find the subvolume root (Inode 256) */
while (1) {
struct stat st;
if (stat(current_path, &st) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating '%s'. %s.\n", current_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (st.st_ino == root_inode) {
/* copy the subvol root */
pathcpy(fss->root_dir, sizeof(fss->root_dir), current_path);
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), current_path);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/");
/* create the snapshot container directory if not existing */
int ret = mkdir(fss->snapshot_dir, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (ret != 0 && errno != EEXIST) {
log_error(errno, "Error creating '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
/* move to parent directory */
pathup(current_path);
if (current_path[0] == 0) {
/* LCOV_EXCL_START */
log_error(ENOENT, "No subvolume root found for '%s'. %s.\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
}
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs(const char* dir, uint32_t magic, struct fssnapshot_struct* fss)
{
if (extract_zfs(dir, fss->dataset, sizeof(fss->dataset), 0, 0, fss->root_dir, sizeof(fss->root_dir)) != 0)
return -1;
pathslash(fss->root_dir, sizeof(fss->root_dir));
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), ".zfs/snapshot/");
/* ensure ZFS snapshot directory is visible */
if (eaccess(fss->snapshot_dir, R_OK | X_OK) != 0) {
log_error(errno, "ZFS snapshot directory not accessible: '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
#endif
int fssnapshot_mount(const char* path, struct fssnapshot_struct* fss)
{
#if HAVE_LINUX_DEVICE
struct statfs sfs;
if (statfs(path, &sfs) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating filesystem '%s'. %s.\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
assert(fss != 0); /* silence the clang static analyzer (it's never called with a NULL fss) */
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
/* btrfs reserved inode 256 for subvolume roots */
return fssnapshot_inode(path, BTRFS_SUPER_MAGIC, 256, fss);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
/* Bcachefs reserves inode 4096 for each subvolume's root directory */
return fssnapshot_inode(path, BCACHEFS_SUPER_MAGIC, 4096, fss);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs(path, ZFS_SUPER_MAGIC, fss);
} else {
return -1;
}
#else
(void)path;
(void)fss;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_fs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_zfs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
/*
* Ensure to get into into the snapshot
*
* This is required by ZFS, otherwise we get a different st_dev
*/
pathcat(target_path, sizeof(target_path), "/.");
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
/**
* When verifying if a ZFS snapshot has been successfully deleted, a direct
* path lookup (stat, access, open) is unreliable. A directory listing
* (readdir) of the .zfs/snapshot directory must be used instead.
*
* On Linux, the Virtual File System (VFS) maintains a 'dentry cache'
* (dcache). If a snapshot path was accessed recently, the kernel caches
* the directory entry in RAM. When ZFS destroys the snapshot, the kernel
* dcache is not always immediately synchronized. Consequently, stat()
* may hit the stale cache and report the snapshot still exists.
*
* Unlike stat(), a directory listing (ls/readdir) forces the kernel to
* query the ZFS module for the current set of valid entries, bypassing
* the stale dcache for a "ground truth" check.
*
* This behavior can be confirmed by forcing the kernel to purge its
* dcache manually using the following command:
* sync; echo 2 > /proc/sys/vm/drop_caches
*
* After dropping caches, stat() will correctly return ENOENT. To avoid
* requiring root privileges or impacting system performance with
* drop_caches, always use readdir() for existence checks in this context.
*/
DIR* d = opendir(fss->snapshot_dir);
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
while (1) {
struct dirent* entry = readdir(d);
if (!entry)
break;
if (strcmp(entry->d_name, name) == 0) {
closedir(d);
return 0;
}
}
closedir(d);
return -1;
}
#endif
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_stat_zfs(fss, name, st);
} else {
return fssnapshot_stat_fs(fss, name, st);
}
#else
(void)fss;
(void)name;
(void)st;
return 0;
#endif
}
#if HAVE_LINUX_DEVICE
int fssnapshot_btrfs_create(const struct fssnapshot_struct* fss, const char* name)
{
struct btrfs_ioctl_vol_args_v2 args;
int fd_source;
int fd_dest_parent;
int ret;
/* open the source subvolume to get a file descriptor */
fd_source = open(fss->root_dir, O_RDONLY | O_DIRECTORY);
if (fd_source < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
fd_dest_parent = open(fss->snapshot_dir, O_RDONLY | O_DIRECTORY);
if (fd_dest_parent < 0) {
/* LCOV_EXCL_START */
close(fd_source);
return -1;
/* LCOV_EXCL_STOP */
}
memset(&args, 0, sizeof(args));
args.fd = fd_source;
args.flags = BTRFS_SUBVOL_RDONLY;
pathcpy(args.name, BTRFS_SUBVOL_NAME_MAX, name);
/* issue the snapshot command to the PARENT directory of the destination */
ret = ioctl(fd_dest_parent, BTRFS_IOC_SNAP_CREATE_V2, &args);
if (ret < 0) {
/* LCOV_EXCL_START */
close(fd_source);
close(fd_dest_parent);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd_source);
close(fd_dest_parent);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const char* name)
{
char target_path[PATH_MAX];
int ret;
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (os_validate_exec_input(fss->root_dir) != 0 || os_validate_exec_input(target_path) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
bcachefs,
"subvolume",
"snapshot",
"-r",
fss->root_dir,
target_path,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char* name)
{
char snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name);
if (os_validate_exec_input(snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"snapshot",
snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_create(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_create(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_create(fss, name);
} else {
return -1;
}
#else
(void)fss;
(void)name;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
int fssnapshot_btrfs_delete(const struct fssnapshot_struct* fss, const char* name)
{
struct btrfs_ioctl_vol_args args;
int fd_parent;
int ret;
fd_parent = open(fss->snapshot_dir, O_RDONLY | O_DIRECTORY);
if (fd_parent < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
memset(&args, 0, sizeof(args));
pathcpy(args.name, BTRFS_PATH_NAME_MAX, name);
ret = ioctl(fd_parent, BTRFS_IOC_SNAP_DESTROY, &args);
if (ret < 0) {
/* LCOV_EXCL_START */
close(fd_parent);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd_parent);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char target_path[PATH_MAX];
int ret;
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (os_validate_exec_input(target_path) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
bcachefs,
"subvolume",
"delete",
target_path,
0,
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name);
if (os_validate_exec_input(snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"destroy",
snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_delete(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_delete(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_delete(fss, name);
} else {
return -1;
}
#else
(void)fss;
(void)name;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_btrfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char old_path[PATH_MAX];
char new_path[PATH_MAX];
pathcpy(old_path, sizeof(old_path), fss->snapshot_dir);
pathcat(old_path, sizeof(old_path), old_name);
pathcpy(new_path, sizeof(new_path), fss->snapshot_dir);
pathcat(new_path, sizeof(new_path), new_name);
if (rename(old_path, new_path) < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
if (fssnapshot_bcachefs_create(fss, new_name) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fssnapshot_bcachefs_delete(fss, old_name) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char old_snapshot[PATH_MAX + 64];
char new_snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(old_snapshot, sizeof(old_snapshot), "%s@%s", fss->dataset, old_name);
snprintf(new_snapshot, sizeof(new_snapshot), "%s@%s", fss->dataset, new_name);
if (os_validate_exec_input(old_snapshot) != 0 || os_validate_exec_input(new_snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"rename",
old_snapshot,
new_snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_rename(fss, old_name, new_name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_rename(fss, old_name, new_name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_rename(fss, old_name, new_name);
} else {
return -1;
}
#else
(void)fss;
(void)old_name;
(void)new_name;
return -1;
#endif
}
void fssnapshot_unmount(const struct fssnapshot_struct* fss)
{
(void)fss;
}
/****************************************************************************/
/* dev */
/**
* Read a file extracting the contained device number in %u:%u format.
* Return 0 on error.
*/
#if HAVE_LINUX_DEVICE
static dev_t devread(const char* path)
{
int f;
int ret;
ssize_t len;
char buf[64];
char* e;
unsigned ma;
unsigned mi;
f = open(path, O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to open '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
len = read(f, buf, sizeof(buf));
if (len < 0) {
/* LCOV_EXCL_START */
close(f);
log_fatal(errno, "Failed to read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
if (len == sizeof(buf)) {
/* LCOV_EXCL_START */
close(f);
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to close '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
buf[len] = 0;
ma = strtou(buf, &e, 10);
if (*e != ':') {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
mi = strtou(e + 1, &e, 10);
if (*e != 0 && !isspace((unsigned char)*e)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
return makedev(ma, mi);
}
#endif
/**
* Read a device tree filling the specified list of disk_t entries.
*/
#if HAVE_LINUX_DEVICE
static int devtree(devinfo_t* parent, dev_t device, tommy_list* list)
{
char path[PATH_MAX];
DIR* d;
int slaves = 0;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/slaves", major(device), minor(device));
/* check if there is a slaves list */
d = opendir(path);
if (d != 0) {
struct dirent* dd;
while ((dd = readdir(d)) != 0) {
if (dd->d_name[0] != '.') {
dev_t subdev;
/* for each slave, expand the full potential tree */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/slaves/%s/dev", major(device), minor(device), dd->d_name);
subdev = devread(path);
if (!subdev) {
/* LCOV_EXCL_START */
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
if (devtree(parent, subdev, list) != 0) {
/* LCOV_EXCL_START */
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
++slaves;
}
}
closedir(d);
}
/* if no slaves found */
if (!slaves) {
/* this is a raw device */
devinfo_t* devinfo;
/* check if it's a real device */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device", major(device), minor(device));
if (eaccess(path, F_OK) != 0) {
/* get the parent device */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/../dev", major(device), minor(device));
device = devread(path);
if (!device) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
}
/* get the device file */
if (devresolve(device, path, sizeof(path)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
devinfo = calloc_nofail(1, sizeof(devinfo_t));
devinfo->device = device;
pathcpy(devinfo->name, sizeof(devinfo->name), parent->name);
pathcpy(devinfo->smartctl, sizeof(devinfo->smartctl), parent->smartctl);
pathcpy(devinfo->smartctl_info, sizeof(devinfo->smartctl_info), parent->smartctl_info);
memcpy(devinfo->smartignore, parent->smartignore, sizeof(devinfo->smartignore));
pathcpy(devinfo->file, sizeof(devinfo->file), path);
devinfo->parent = parent;
/* insert in the list */
tommy_list_insert_tail(list, &devinfo->node, devinfo);
}
return 0;
}
#endif
/**
* Compute disk usage by aggregating access statistics.
*/
#if HAVE_LINUX_DEVICE
static int devstat(dev_t device, uint64_t* count)
{
char path[PATH_MAX];
char buf[512];
int token;
ssize_t ret;
char* i;
*count = 0;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/stat", major(device), minor(device));
ret = sysread(path, buf, sizeof(buf));
if (ret < 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
/* ending 0 */
buf[ret] = 0;
i = buf;
token = 1; /* token number */
while (*i) {
char* n;
char* e;
unsigned long long v;
/* skip spaces */
while (*i && isspace((unsigned char)*i))
++i;
/* read digits */
n = i;
while (*i && isdigit((unsigned char)*i))
++i;
if (i == n) /* if no digit, abort */
break;
if (*i == 0 || !isspace((unsigned char)*i)) /* if no space, abort */
break;
*i++ = 0; /* put a terminator */
v = strtoull(n, &e, 10);
if (*e != 0) {
/* LCOV_EXCL_START */
break;
/* LCOV_EXCL_STOP */
}
/* sum reads and writes completed */
if (token == 1 || token == 5) {
*count += v;
if (token == 5)
break; /* stop here */
}
++token;
}
return 0;
}
#endif
/**
* Max number of arguments supported.
*/
#define ARGS_MAX 64
/**
* Get SMART attributes.
*/
#if HAVE_LINUX_DEVICE
static int devsmart(dev_t device, const char* name, const char* smartctl, const char* smartctl_info, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
char info_buf[SMART_MAX];
pathcpy(info_buf, sizeof(info_buf), info_opts);
argc += argsplit(argv + argc, ARGS_MAX - argc, info_buf);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s [info: %s] %s\n", file, name, x, info_opts, extra_args);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s [info: %s] %s\n", file, name, x, info_opts, file);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, interface) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, file);
return -1;
/* LCOV_EXCL_STOP */
}
/* store the return smartctl return value */
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
return 0;
}
#endif
/**
* Get device attributes.
*/
#if HAVE_LINUX_DEVICE
static void devattr(dev_t device, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char path[PATH_MAX];
char buf[512];
ssize_t ret;
(void)family; /* not available, smartctl uses an internal database to get it */
if (info[INFO_SIZE] == SMART_UNASSIGNED) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/size", major(device), minor(device));
long long v = syslong(path);
if (v >= 0)
info[INFO_SIZE] = v * 512;
}
if (info[INFO_ROTATION_RATE] == SMART_UNASSIGNED) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/queue/rotational", major(device), minor(device));
long long v = syslong(path);
if (v >= 0)
info[INFO_ROTATION_RATE] = v;
}
if (*model == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/model", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(model, SMART_MAX, buf);
}
}
if (*serial == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/serial", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(serial, SMART_MAX, buf);
}
}
if (*serial == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/vpd_pg80", major(device), minor(device));
if (sysattr_vpd_pg80(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(serial, SMART_MAX, buf);
}
}
/* always override interface if it's usb */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u", major(device), minor(device));
ret = readlink(path, buf, sizeof(buf));
if (ret > 0 && (unsigned)ret < sizeof(buf)) {
buf[ret] = 0;
if (strstr(buf, "usb") != 0)
strcpy(interface, "USB");
}
}
#endif
/**
* Get POWER state.
*/
#if HAVE_LINUX_DEVICE
static int devprobe(dev_t device, const char* name, const char* smartctl, const char* smartctl_info, int* power, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
argv[argc++] = "-n";
argv[argc++] = "standby,3";
char info_buf[SMART_MAX];
pathcpy(info_buf, sizeof(info_buf), info_opts);
argc += argsplit(argv + argc, ARGS_MAX - argc, info_buf);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] %s\n", file, name, x, info_opts, extra_args);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] %s\n", file, name, x, info_opts, file);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, interface) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) == 3) {
log_tag("attr:%s:%s:power:standby\n", file, name);
*power = POWER_STANDBY;
} else {
log_tag("attr:%s:%s:power:active\n", file, name);
*power = POWER_ACTIVE;
/* store the smartctl return value */
if (smart)
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
}
return 0;
}
#endif
/**
* Spin down a specific device.
*/
#if HAVE_LINUX_DEVICE
static int devdown(dev_t device, const char* name, const char* smartctl)
{
char extra_args[PATH_MAX];
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
argv[argc++] = "-s";
argv[argc++] = "standby,now";
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s -s standby,now %s\n", file, name, x, extra_args);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s -s standby,now %s\n", file, name, x, file);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_flush(f, file, name) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) != 0) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:exit:%d\n", file, name, WEXITSTATUS(ret));
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, extra_args, WEXITSTATUS(ret));
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, file, WEXITSTATUS(ret));
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("attr:%s:%s:power:down\n", file, name);
return 0;
}
#endif
/**
* Spin down a specific device if it's up.
*/
#if HAVE_LINUX_DEVICE
static int devdownifup(dev_t device, const char* name, const char* smartctl, const char* smartctl_info, int* power)
{
*power = POWER_UNKNOWN;
if (devprobe(device, name, smartctl, smartctl_info, power, 0, 0, 0, 0, 0, 0) != 0)
return -1;
if (*power == POWER_ACTIVE)
return devdown(device, name, smartctl);
return 0;
}
#endif
/**
* Check if the device needs power management (it's a rotational one)
*/
#if HAVE_LINUX_DEVICE
static int devpower(dev_t device)
{
char path[PATH_MAX];
long long rotational;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/queue/rotational", major(device), minor(device));
rotational = syslong(path);
if (rotational < 0)
return 1; /* assume rotational if unknown */
return rotational != 0;
}
#endif
/**
* Spin up a specific device.
*/
#if HAVE_LINUX_DEVICE
static int devup(dev_t device, const char* name)
{
char file[PATH_MAX];
ssize_t ret;
int f;
void* buf;
uint64_t size;
uint64_t offset;
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* O_DIRECT requires memory aligned to the block size */
if (posix_memalign(&buf, 4096, 4096) != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to allocate aligned memory for device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
f = open(file, O_RDONLY | O_DIRECT);
if (f < 0) {
/* LCOV_EXCL_START */
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to open device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
if (ioctl(f, BLKGETSIZE64, &size) < 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to get device size '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* select a random offset */
uint64_t blocks = size / 4096;
offset = blocks > 0 ? (random_u64() % blocks) * 4096 : 0;
#if HAVE_POSIX_FADVISE
/* clear cache */
ret = posix_fadvise_wrapper(f, offset, 4096, POSIX_FADV_DONTNEED);
if (ret == ENOSYS) {
/* call is not supported */
ret = 0;
}
if (ret != 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to advise device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
#endif
ret = pread(f, buf, 4096, offset);
if (ret < 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to read device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret < 0) {
/* LCOV_EXCL_START */
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to close device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("attr:%s:%s:power:up\n", file, name);
free(buf);
return 0;
}
#endif
/**
* Thread for spinning up.
*
* Note that filling up the devinfo object is done inside this thread,
* to avoid to block the main thread if the device need to be spin up
* to handle stat/resolve requests.
*/
static void* thread_spinup(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
/* skip not rotational devices */
if (devpower(devinfo->device) == 0)
return (void*)-1;
start = os_tick_ms();
if (devup(devinfo->device, devinfo->name) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
msg_status("Spunup device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
/* after the spin up, get SMART info */
if (devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for spinning down.
*/
static void* thread_spindown(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
/* skip not rotational devices */
if (devpower(devinfo->device) == 0)
return (void*)-1;
start = os_tick_ms();
if (devdown(devinfo->device, devinfo->name, devinfo->smartctl) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
msg_status("Spundown device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for spinning down.
*/
static void* thread_spindownifup(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
int power;
start = os_tick_ms();
if (devdownifup(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, &power) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
if (power == POWER_ACTIVE)
msg_status("Spundown device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for getting smart info.
*/
static void* thread_smart(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
if (devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for getting power info.
*/
static void* thread_probe(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
if (devprobe(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, &devinfo->power, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
static int device_thread(tommy_list* list, void* (*func)(void* arg))
{
int fail = 0;
tommy_node* i;
#if HAVE_THREAD
/* start all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
thread_create(&devinfo->thread, func, devinfo);
}
/* join all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
void* retval;
thread_join(devinfo->thread, &retval);
if (retval != 0)
++fail;
}
#else
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
if (func(devinfo) != 0)
++fail;
}
#endif
if (fail != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
int devquery(tommy_list* high, tommy_list* low, int operation)
{
void* (*func)(void* arg) = 0;
#if HAVE_LINUX_DEVICE
tommy_node* i;
struct stat st;
/* sysfs interface is required */
if (stat("/sys/dev/block", &st) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Missing interface /sys/dev/block.\n");
return -1;
/* LCOV_EXCL_STOP */
}
/* for each high device */
for (i = tommy_list_head(high); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
uint64_t device = devinfo->device;
#if HAVE_SYNCFS
if (operation == DEVICE_DOWN) {
/* flush the high level filesystem before spinning down */
int f = open(devinfo->mount, O_RDONLY);
if (f >= 0) {
syncfs(f);
close(f);
}
}
#endif
tommy_list devlist;
tommy_list_init(&devlist);
/* obtain the real devices */
if (devdereference(device, devinfo->mount, &devlist) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to dereference device '%u:%u' at '%s'.\n", major(device), minor(device), devinfo->mount);
return -1;
/* LCOV_EXCL_STOP */
}
devinfo->file[0] = 0;
for (tommy_node* j = tommy_list_head(&devlist); j != 0; j = j->next) {
struct dev_struct* dev = j->data;
uint64_t access_stat;
/* retrieve access stat for the high level device */
if (devstat(dev->device, &access_stat) == 0) {
/* cumulate access stat in the first split */
if (devinfo->split)
devinfo->split->access_stat += access_stat;
else
devinfo->access_stat += access_stat;
}
/* get the device file */
char file[PATH_MAX];
if (devresolve(dev->device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(dev->device), minor(dev->device));
return -1;
/* LCOV_EXCL_STOP */
}
/* add to the list of device files */
if (devinfo->file[0] != 0)
pathcat(devinfo->file, sizeof(devinfo->file), ",");
pathcat(devinfo->file, sizeof(devinfo->file), file);
/* expand the tree of devices */
if (devtree(devinfo, dev->device, low) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to expand device '%u:%u'.\n", major(dev->device), minor(dev->device));
return -1;
/* LCOV_EXCL_STOP */
}
}
tommy_list_foreach(&devlist, free);
}
#else
(void)high;
#endif
switch (operation) {
case DEVICE_UP : func = thread_spinup; break;
case DEVICE_DOWN : func = thread_spindown; break;
case DEVICE_SMART : func = thread_smart; break;
case DEVICE_PROBE : func = thread_probe; break;
case DEVICE_DOWNIFUP : func = thread_spindownifup; break;
}
if (!func)
return 0;
return device_thread(low, func);
}
int devmap(void)
{
#if HAVE_LINUX_DEVICE
char path[PATH_MAX];
DIR* d = opendir("/sys/block");
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
struct dirent* dd;
while (1) {
char buf[256];
dd = readdir(d);
if (dd == 0)
break;
if (dd->d_name[0] == '.')
continue;
/* verify it has a hardware device link (excludes virtual disks) */
pathprint(path, sizeof(path), "/sys/block/%s/device", dd->d_name);
if (eaccess(path, F_OK) != 0)
continue;
/* device/serial */
pathprint(path, sizeof(path), "/sys/block/%s/device/serial", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/vpd_pg80 */
pathprint(path, sizeof(path), "/sys/block/%s/device/vpd_pg80", dd->d_name);
if (sysattr_vpd_pg80(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/vpd_pg83 */
pathprint(path, sizeof(path), "/sys/block/%s/device/vpd_pg83", dd->d_name);
if (sysattr_vpd_pg83(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:naa.%s\n", dd->d_name, esc_tag(buf));
}
/* device/WWN */
pathprint(path, sizeof(path), "/sys/block/%s/device/wwn", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/WWID */
pathprint(path, sizeof(path), "/sys/block/%s/device/wwid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* WWN */
pathprint(path, sizeof(path), "/sys/block/%s/wwn", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* WWID */
pathprint(path, sizeof(path), "/sys/block/%s/wwid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* uuid */
pathprint(path, sizeof(path), "/sys/block/%s/uuid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
}
closedir(d);
#endif
return 0;
}
/****************************************************************************/
/* temperature */
#if HAVE_LINUX_DEVICE
/* List of possible ambient temperature labels */
const char* AMBIENT_LABEL[] = {
"systin",
"auxtin",
"mb",
"m/b",
"board",
"motherboard",
"system",
"chassis",
"case",
"room",
"ambient",
0
};
int ambient_temperature(void)
{
DIR* dir;
struct dirent* entry;
int lowest_temp = 0;
dir = opendir("/sys/class/hwmon");
if (!dir) {
/* LCOV_EXCL_START */
return 0;
/* LCOV_EXCL_STOP */
}
/* iterate through hwmon devices */
while ((entry = readdir(dir)) != NULL) {
char path[PATH_MAX];
DIR* hwmon_dir;
struct dirent* hwmon_entry;
if (strncmp(entry->d_name, "hwmon", 5) != 0)
continue;
pathprint(path, sizeof(path), "/sys/class/hwmon/%s", entry->d_name);
/* iterate through temp*_input files */
hwmon_dir = opendir(path);
if (!hwmon_dir) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
while ((hwmon_entry = readdir(hwmon_dir)) != NULL) {
char hwmon_name[PATH_MAX];
char value[SMART_MAX];
char name[SMART_MAX];
char label[SMART_MAX];
char* dash;
char* e;
int temp;
if (strncmp(hwmon_entry->d_name, "temp", 4) != 0)
continue;
/* make a copy to allow modifications in place */
pathcpy(hwmon_name, sizeof(hwmon_name), hwmon_entry->d_name);
dash = strrchr(hwmon_name, '_');
if (dash == 0) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
if (strcmp(dash, "_input") != 0)
continue;
/* read the temperature */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s", entry->d_name, hwmon_name);
if (sysattr(path, value, sizeof(value)) != 0) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
temp = strtoi(value, &e, 10) / 1000;
if (*e != 0 && !isspace((unsigned char)*e)) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
/* cut the file name at "_input" */
*dash = 0;
/* read the corresponding name */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/name", entry->d_name);
if (sysattr(path, name, sizeof(name)) != 0) {
/* LCOV_EXCL_START */
/* fallback to using the hwmon name */
pathcpy(name, sizeof(name), entry->d_name);
/* LCOV_EXCL_STOP */
}
/* read the corresponding label file */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s_label", entry->d_name, hwmon_name);
if (sysattr(path, label, sizeof(label)) != 0) {
/* LCOV_EXCL_START */
/* fallback to using the temp* name (e.g., temp1, temp2) */
pathcpy(label, sizeof(label), hwmon_name);
/* LCOV_EXCL_STOP */
}
log_tag("thermal:ambient:device:%s:%s:%s:%s:%d\n", entry->d_name, name, hwmon_name, label, temp);
/* check if temperature is in reasonable range */
if (temp < 15 || temp > 40)
continue;
/* lower case */
strlwr(label);
/* check if label matches possible ambient labels */
for (int i = 0; AMBIENT_LABEL[i]; ++i) {
if (worddigitstr(label, AMBIENT_LABEL[i]) != 0) {
log_tag("thermal:ambient:candidate:%d\n", temp);
if (lowest_temp == 0 || lowest_temp > temp)
lowest_temp = temp;
break;
}
}
/* accept also generic "temp1" */
if (strcmp(label, "temp1") == 0) {
log_tag("thermal:ambient:candidate:%d\n", temp);
if (lowest_temp == 0 || lowest_temp > temp)
lowest_temp = temp;
}
}
closedir(hwmon_dir);
}
closedir(dir);
return lowest_temp;
}
#else
int ambient_temperature(void)
{
return 0;
}
#endif
/****************************************************************************/
/* app */
void app_init(void)
{
#if HAVE_BLKID
int ret;
ret = blkid_get_cache(&cache, NULL);
if (ret != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "WARNING Failed to get blkid cache\n");
/* LCOV_EXCL_STOP */
}
#endif
/* set LC_ALL=C to make smartctl ignoring the locale when printing info */
setenv("LC_ALL", "C", 1);
}
void app_done(void)
{
#if HAVE_BLKID
if (cache != 0)
blkid_put_cache(cache);
#endif
}
void app_default_conf(char* conf, size_t conf_size, const char* argv0)
{
(void)argv0;
#ifdef SYSCONFDIR
/* if it exists, give precedence at sysconfdir, usually /usr/local/etc */
if (eaccess(SYSCONFDIR "/" PACKAGE ".conf", F_OK) == 0) {
pathcpy(conf, conf_size, SYSCONFDIR "/" PACKAGE ".conf");
} else /* otherwise fallback to plain /etc */
#endif
{
pathcpy(conf, conf_size, "/etc/" PACKAGE ".conf");
}
}
#endif