mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 10:51:54 -04:00
2418 lines
67 KiB
C
2418 lines
67 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2011 Andrea Mazzoleni
|
|
|
|
#include "os/portable.h"
|
|
|
|
#ifdef __MINGW32__ /* Only for MingW */
|
|
|
|
#include "support.h"
|
|
#include "raid/cpu.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 (int)global_interrupt;
|
|
}
|
|
|
|
void app_signal_handler(int signum)
|
|
{
|
|
/* report the request of interruption with the signal received */
|
|
global_interrupt = signum;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* fs */
|
|
|
|
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)
|
|
{
|
|
wchar_t conv_buf[CONV_MAX];
|
|
|
|
/* NTFS doesn't synchronize hardlinks metadata */
|
|
if (has_syncronized_hardlinks)
|
|
*has_syncronized_hardlinks = 0;
|
|
|
|
if (free_space || total_space) {
|
|
ULARGE_INTEGER total_bytes;
|
|
ULARGE_INTEGER total_free_bytes;
|
|
DWORD attr;
|
|
char dir[PATH_MAX];
|
|
|
|
if (strlen(path) + 1 > sizeof(dir)) {
|
|
windows_errno(ERROR_BUFFER_OVERFLOW);
|
|
return -1;
|
|
}
|
|
|
|
strcpy(dir, path);
|
|
|
|
/* get the file attributes */
|
|
attr = GetFileAttributesW(convert(conv_buf, dir));
|
|
if (attr == INVALID_FILE_ATTRIBUTES) {
|
|
DWORD error = GetLastError();
|
|
|
|
if (error != ERROR_FILE_NOT_FOUND) {
|
|
windows_errno(error);
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* If it doesn't exist, we assume a file
|
|
* and we check for the containing dir
|
|
*/
|
|
attr = 0;
|
|
}
|
|
|
|
/* if it's not a directory, truncate the file name */
|
|
if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0) {
|
|
char* slash = strrchr(dir, '/');
|
|
|
|
/**
|
|
* Cut the file name, but leave the last slash.
|
|
*
|
|
* This is done because a MSDN comment about using of UNC paths.
|
|
*
|
|
* MSDN 'GetDiskFreeSpaceEx function'
|
|
* http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937%28v=vs.85%29.aspx
|
|
* If this parameter is a UNC name, it must include a trailing backslash,
|
|
* for example, "\\MyServer\MyShare\".
|
|
*/
|
|
if (slash)
|
|
slash[1] = 0;
|
|
}
|
|
|
|
/*
|
|
* Get the free space of the directory
|
|
* note that it must be a directory
|
|
*/
|
|
if (!GetDiskFreeSpaceExW(convert(conv_buf, dir), 0, &total_bytes, &total_free_bytes)) {
|
|
windows_errno(GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
if (total_space)
|
|
*total_space = total_bytes.QuadPart;
|
|
if (free_space)
|
|
*free_space = total_free_bytes.QuadPart;
|
|
}
|
|
|
|
if (has_persistent_inode || fstype || fslabel) {
|
|
wchar_t volume_root[PATH_MAX];
|
|
wchar_t fs_name[PATH_MAX];
|
|
wchar_t vol_name[PATH_MAX];
|
|
int has_volume_info = 0;
|
|
|
|
if (fstype && fstype_size > 0)
|
|
fstype[0] = 0;
|
|
if (fslabel && fslabel_size > 0)
|
|
fslabel[0] = 0;
|
|
|
|
if (GetVolumePathNameW(convert(conv_buf, path), volume_root, PATH_MAX)) {
|
|
if (GetVolumeInformationW(volume_root, vol_name, PATH_MAX, 0, 0, 0, fs_name, PATH_MAX)) {
|
|
has_volume_info = 1;
|
|
}
|
|
}
|
|
|
|
if (has_persistent_inode) {
|
|
*has_persistent_inode = 0;
|
|
|
|
/**
|
|
* Reuse stored file IDs across scans only for filesystems known to
|
|
* provide stable and unique IDs. An unknown or unavailable filesystem
|
|
* must be treated as volatile: a false negative only loses inode-based
|
|
* move detection, while a false positive can match an unrelated file.
|
|
*/
|
|
if (has_volume_info
|
|
&& (_wcsicmp(fs_name, L"NTFS") == 0
|
|
|| _wcsicmp(fs_name, L"ReFS") == 0)) {
|
|
*has_persistent_inode = 1;
|
|
}
|
|
}
|
|
|
|
if (has_volume_info) {
|
|
if (fstype && fstype_size > 0) {
|
|
char u8[CONV_MAX];
|
|
size_t len;
|
|
char* ret;
|
|
|
|
ret = u16tou8_mayfail(u8, sizeof(u8), fs_name, wcslen(fs_name), &len);
|
|
if (ret != 0 && len + 1 <= fstype_size) {
|
|
memcpy(fstype, u8, len);
|
|
fstype[len] = 0;
|
|
}
|
|
}
|
|
|
|
if (fslabel && fslabel_size > 0) {
|
|
char u8[CONV_MAX];
|
|
size_t len;
|
|
char* ret;
|
|
|
|
ret = u16tou8_mayfail(u8, sizeof(u8), vol_name, wcslen(vol_name), &len);
|
|
if (ret != 0 && len + 1 <= fslabel_size) {
|
|
memcpy(fslabel, u8, len);
|
|
fslabel[len] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* identity */
|
|
|
|
int fsidentity(const char* path, const struct stat* st, struct fsidentity* identity)
|
|
{
|
|
(void)path;
|
|
|
|
identity->device = st->st_dev;
|
|
identity->mnt_id = 0;
|
|
identity->subvol = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* snapshot */
|
|
|
|
size_t windows_direct_size(void)
|
|
{
|
|
SYSTEM_INFO si;
|
|
|
|
GetSystemInfo(&si);
|
|
|
|
/*
|
|
* MSDN 'File Buffering'
|
|
* https://msdn.microsoft.com/en-us/library/windows/desktop/cc644950%28v=vs.85%29.aspx
|
|
*
|
|
* "Therefore, in most situations, page-aligned memory will also be sector-aligned,"
|
|
* "because the case where the sector size is larger than the page size is rare."
|
|
*/
|
|
return si.dwPageSize;
|
|
}
|
|
|
|
#define WINDOWS_NTFS_MAGIC 0x5346544E /* 'N','T','F','S' */
|
|
|
|
#define PS_CMD_MAX 16384 /* needs a x4 factor to handle -EncodedCommand */
|
|
|
|
#define SNAPSHOT_GUID ".guid"
|
|
|
|
static const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
static int base64_encode(const unsigned char* in, size_t in_len, char* out, size_t out_max)
|
|
{
|
|
if (out_max == 0)
|
|
return -1;
|
|
|
|
size_t i, j = 0;
|
|
for (i = 0; i < in_len; i += 3) {
|
|
uint32_t val = 0;
|
|
int count = 0;
|
|
int k;
|
|
|
|
for (k = 0; k < 3; ++k) {
|
|
val <<= 8;
|
|
if (i + k < in_len) {
|
|
val |= in[i + k];
|
|
++count;
|
|
}
|
|
}
|
|
|
|
if (j + 4 >= out_max)
|
|
return -1;
|
|
|
|
out[j++] = b64chars[(val >> 18) & 0x3F];
|
|
out[j++] = b64chars[(val >> 12) & 0x3F];
|
|
out[j++] = (count > 1) ? b64chars[(val >> 6) & 0x3F] : '=';
|
|
out[j++] = (count > 2) ? b64chars[val & 0x3F] : '=';
|
|
}
|
|
out[j] = 0;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* PowerShell helper
|
|
*
|
|
* Runs a PowerShell one-liner via os_spawn() and captures the first line
|
|
* of stdout into `out` as UTF-8.
|
|
* `out` may be 0 when output is not needed.
|
|
*
|
|
* Because os_spawn() captures stdout as a raw binary file descriptor without
|
|
* CRLF translation, callers must handle Windows \r\n explicitly. Here, \r\n
|
|
* is stripped using strpbrk() and strtrim().
|
|
*
|
|
* The command is executed using -EncodedCommand (Base64 UTF-16LE) to safely
|
|
* handle any shell metacharacters or quotes in paths and volume names.
|
|
*
|
|
* Returns 0 on success, -1 on failure.
|
|
*/
|
|
static int windows_ps(const char* ps_command, char* out, size_t out_size)
|
|
{
|
|
WCHAR wps_path[PATH_MAX];
|
|
char ps_path[PATH_MAX * 3];
|
|
WCHAR wcmd[PS_CMD_MAX / 4];
|
|
char b64cmd[PS_CMD_MAX];
|
|
DWORD plen;
|
|
int stdout_fd = -1;
|
|
pid_t pid;
|
|
int status;
|
|
int wlen;
|
|
|
|
/*
|
|
* Resolve powershell.exe to an absolute path.
|
|
* Safe search mode enabled in os_init() via SetSearchPathMode() ensures
|
|
* system directories and PATH are searched before the current working directory.
|
|
*/
|
|
plen = SearchPathW(NULL, L"powershell.exe", NULL, PATH_MAX, wps_path, NULL);
|
|
if (plen == 0 || plen >= PATH_MAX) {
|
|
windows_errno(GetLastError());
|
|
log_error(errno, "Failed to locate powershell.exe for command '%s'.\n", ps_command);
|
|
return -1;
|
|
}
|
|
|
|
if (WideCharToMultiByte(CP_UTF8, 0, wps_path, -1, ps_path, sizeof(ps_path), 0, 0) == 0) {
|
|
errno = E2BIG;
|
|
log_error(errno, "Failed to convert powershell.exe path for command '%s'.\n", ps_command);
|
|
return -1;
|
|
}
|
|
|
|
/* convert UTF-8 command to UTF-16LE WCHARs */
|
|
wlen = MultiByteToWideChar(CP_UTF8, 0, ps_command, -1, wcmd, PS_CMD_MAX / 4);
|
|
if (wlen <= 0) {
|
|
errno = E2BIG;
|
|
log_error(errno, "Failed to run PowerShell command '%s' (too big to convert).\n", ps_command);
|
|
return -1;
|
|
}
|
|
|
|
/* base64 encode the UTF-16LE command bytes (excluding the null terminator) */
|
|
if (base64_encode((const unsigned char*)wcmd, (wlen - 1) * sizeof(WCHAR), b64cmd, sizeof(b64cmd)) != 0) {
|
|
errno = E2BIG;
|
|
log_error(errno, "Failed to run PowerShell command '%s' (too big to encode).\n", ps_command);
|
|
return -1;
|
|
}
|
|
|
|
char* argv[] = {
|
|
ps_path,
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-EncodedCommand",
|
|
b64cmd,
|
|
0
|
|
};
|
|
|
|
pid = os_spawn(argv, &stdout_fd, 0, 0, 0);
|
|
if (pid < 0) {
|
|
log_error(errno, "Failed to run PowerShell command '%s' (from os_spawn).\n", ps_command);
|
|
return -1;
|
|
}
|
|
|
|
if (out && out_size > 0) {
|
|
size_t pos = 0;
|
|
while (pos + 1 < out_size) {
|
|
ssize_t ret = read(stdout_fd, out + pos, out_size - 1 - pos);
|
|
if (ret <= 0)
|
|
break;
|
|
pos += ret;
|
|
if (memchr(out, '\n', pos))
|
|
break;
|
|
}
|
|
out[pos] = 0;
|
|
|
|
/* truncate at first newline and trim */
|
|
char* eol = strpbrk(out, "\r\n");
|
|
if (eol)
|
|
*eol = 0;
|
|
strtrim(out);
|
|
}
|
|
|
|
close(stdout_fd);
|
|
|
|
status = 0;
|
|
pid_t pid_ret = os_wait(pid, &status, 0);
|
|
os_dispose(pid);
|
|
if (pid_ret < 0) {
|
|
log_error(errno, "Failed to wait for PowerShell command '%s' (from os_wait).\n", ps_command);
|
|
return -1;
|
|
}
|
|
if (status != 0) {
|
|
errno = EINVAL;
|
|
log_error(errno, "PowerShell command '%s' failed with '%d'.\n", ps_command, status);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Validates a GUID string of the form: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
|
*
|
|
* Returns 0 if valid, -1 otherwise.
|
|
*/
|
|
int windows_guid_is_valid(const char* guid, int len)
|
|
{
|
|
if (!guid || len != 38)
|
|
return -1;
|
|
|
|
if (guid[0] != '{' || guid[37] != '}')
|
|
return -1;
|
|
|
|
const char* inner = guid + 1;
|
|
|
|
for (int i = 0; i < 36; ++i) {
|
|
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
|
if (inner[i] != '-')
|
|
return -1;
|
|
} else {
|
|
char c = inner[i];
|
|
int is_digit = (c >= '0' && c <= '9');
|
|
int is_upper_hex = (c >= 'A' && c <= 'F');
|
|
int is_lower_hex = (c >= 'a' && c <= 'f');
|
|
|
|
if (!is_digit && !is_upper_hex && !is_lower_hex)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int windows_read_guid(const char* guid_link, char* guid, size_t guid_size)
|
|
{
|
|
int len = windows_readlink(guid_link, guid, guid_size);
|
|
if (len < 0 && errno == ENOENT) {
|
|
guid[0] = 0;
|
|
return 0;
|
|
}
|
|
if (len < 0) {
|
|
log_error(errno, "Error readlink '%s'. %s.\n", guid_link, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if ((size_t)len >= guid_size) {
|
|
errno = ENAMETOOLONG;
|
|
log_error(errno, "GUID too long in '%s'.\n", guid_link);
|
|
return -1;
|
|
}
|
|
|
|
guid[len] = 0;
|
|
|
|
/* validate that the GUID contains only safe characters: [A-Za-z0-9-{}] */
|
|
if (windows_guid_is_valid(guid, len) != 0)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int windows_rebuild_link(const struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
char cmd[PS_CMD_MAX];
|
|
char device_path[PATH_MAX];
|
|
char target_path[PATH_MAX];
|
|
char link_path[PATH_MAX];
|
|
char guid_link[PATH_MAX];
|
|
char guid[PATH_MAX];
|
|
|
|
pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir);
|
|
pathcat(guid_link, sizeof(guid_link), name);
|
|
pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID);
|
|
|
|
pathcpy(link_path, sizeof(link_path), fss->snapshot_dir);
|
|
pathcat(link_path, sizeof(link_path), name);
|
|
|
|
int ret = windows_read_guid(guid_link, guid, sizeof(guid));
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* if no GUID, assume it absent */
|
|
if (guid[0] == 0) {
|
|
/* remove stale links */
|
|
windows_rmdir(link_path);
|
|
return 0;
|
|
}
|
|
|
|
snprintf(cmd, sizeof(cmd),
|
|
"(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject",
|
|
guid);
|
|
|
|
if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) {
|
|
log_error(errno, "Error getting DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* if no device path, assume snapshot absent */
|
|
if (device_path[0] == 0) {
|
|
log_error(ENOENT, "WARNING! Snapshot '%s' DeviceObject lost or removed externally.\n", name);
|
|
/* remove stale links */
|
|
windows_rmdir(guid_link);
|
|
windows_rmdir(link_path);
|
|
return 0;
|
|
}
|
|
|
|
pathimport(target_path, sizeof(target_path), device_path);
|
|
pathslash(target_path, sizeof(target_path));
|
|
|
|
/* remove stale link if present */
|
|
windows_rmdir(link_path);
|
|
|
|
if (windows_symlink_directory(target_path, link_path) != 0) {
|
|
log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int windows_delete_link(const struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
char link_path[PATH_MAX];
|
|
|
|
pathcpy(link_path, sizeof(link_path), fss->snapshot_dir);
|
|
pathcat(link_path, sizeof(link_path), name);
|
|
|
|
/* remove link if present */
|
|
windows_rmdir(link_path);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void fssnapshot_retiring_name(const char* name, char* retiring_name, size_t retiring_name_size)
|
|
{
|
|
pathcpy(retiring_name, retiring_name_size, name);
|
|
pathcat(retiring_name, retiring_name_size, SNAPSHOT_RETIRING_SUFFIX);
|
|
}
|
|
|
|
static int fssnapshot_exists(struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
struct stat st;
|
|
|
|
if (fssnapshot_stat(fss, name, &st) == 0)
|
|
return 1;
|
|
|
|
if (errno == ENOENT)
|
|
return 0;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Recover an interrupted snapshot replacement.
|
|
*
|
|
* The .retiring snapshot is the previous destination and must remain available until the new destination is published. If the destination exists, publication completed and the retiring snapshot is deleted. Otherwise the previous destination is restored.
|
|
*
|
|
* Any failure is propagated to fssnapshot_mount(). Callers may then fall back to the live filesystem with an explicit warning when snapshots are used only as a read source, while snapshot-modifying operations fail.
|
|
*/
|
|
static int fssnapshot_recover_replace(struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
char retiring_name[PATH_MAX];
|
|
|
|
fssnapshot_retiring_name(name, retiring_name, sizeof(retiring_name));
|
|
|
|
int retiring = fssnapshot_exists(fss, retiring_name);
|
|
if (retiring <= 0)
|
|
return retiring;
|
|
|
|
int current = fssnapshot_exists(fss, name);
|
|
if (current < 0)
|
|
return -1;
|
|
|
|
if (current != 0) {
|
|
/* the new destination was published, so the previous one can now be retired */
|
|
return fssnapshot_delete(fss, retiring_name);
|
|
}
|
|
|
|
/* the new destination was not published, so restore the previous one */
|
|
return fssnapshot_rename(fss, retiring_name, name);
|
|
}
|
|
|
|
int fssnapshot_mount(const char* dir, struct fssnapshot_struct* fss)
|
|
{
|
|
wchar_t conv_buf_vol[CONV_MAX];
|
|
char conv_buf_root[CONV_MAX];
|
|
wchar_t full_path[PATH_MAX];
|
|
wchar_t volume_root[PATH_MAX];
|
|
wchar_t volume_name[PATH_MAX];
|
|
wchar_t fs_name[32];
|
|
char resolved_dir[PATH_MAX];
|
|
uint32_t magic;
|
|
char pending_retiring[PATH_MAX];
|
|
char stable_retiring[PATH_MAX];
|
|
|
|
/*
|
|
* Make the configured path absolute without resolving reparse points.
|
|
* Unlike realpath(), GetFullPathNameW() preserves volume mount points.
|
|
*/
|
|
DWORD full_len = GetFullPathNameW(convert_if_required(conv_buf_vol, dir), PATH_MAX, full_path, 0);
|
|
if (full_len == 0) {
|
|
windows_errno(GetLastError());
|
|
log_error(errno, "Error resolving full path '%s'. %s.\n", dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
if (full_len >= PATH_MAX) {
|
|
errno = ENAMETOOLONG;
|
|
log_error(errno, "Resolved path too long '%s'. %s.\n", dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* use pathimport to convert backslashes to slashes */
|
|
pathimport(resolved_dir, sizeof(resolved_dir), u16tou8(conv_buf_root, full_path));
|
|
pathslash(resolved_dir, sizeof(resolved_dir));
|
|
|
|
/*
|
|
* GetVolumePathNameW() accepts any path: file, directory, or deep
|
|
* subdirectory, and resolves it to the volume root (e.g. L"C:\").
|
|
* The result always carries a trailing backslash.
|
|
*
|
|
* Use convert_if_required() to avoid the automatic addition of \\?\
|
|
* made by convert() that is propagated in the resulting volume_root
|
|
*/
|
|
if (!GetVolumePathNameW(convert_if_required(conv_buf_vol, resolved_dir), volume_root, PATH_MAX)) {
|
|
windows_errno(GetLastError());
|
|
log_error(errno, "Error getting VolumeRoot from '%s'. %s.\n", resolved_dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if (!GetVolumeInformationW(volume_root, 0, 0, 0, 0, 0, fs_name, 32)) {
|
|
windows_errno(GetLastError());
|
|
log_error(errno, "Error getting information of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno));
|
|
return -1;
|
|
}
|
|
if (wcscmp(fs_name, L"NTFS") == 0)
|
|
magic = WINDOWS_NTFS_MAGIC;
|
|
else
|
|
return 1; /* support only NTFS */
|
|
|
|
/*
|
|
* Obtain the canonical volume name: \\?\Volume{GUID}\
|
|
*
|
|
* GetVolumePathNameW() may return a drive letter path ("C:\") or a
|
|
* directory mount point ("D:\Mount\"). Neither is guaranteed to be
|
|
* accepted by Win32_ShadowCopy.Create() on volumes without a drive
|
|
* letter. GetVolumeNameForVolumeMountPointW() always returns the
|
|
* stable kernel-form GUID path that VSS requires, regardless of how
|
|
* many (or how few) mount points the volume has.
|
|
*
|
|
* We store it in fss->dataset.
|
|
*/
|
|
if (!GetVolumeNameForVolumeMountPointW(volume_root, volume_name, PATH_MAX)) {
|
|
windows_errno(GetLastError());
|
|
log_error(errno, "Error getting VolumeName of VolumeRoot '%s'. %s.\n", u16tou8(conv_buf_root, volume_root), strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* don't use pathimport for dataset to keep backslashes */
|
|
pathcpy(fss->dataset, sizeof(fss->dataset), u16tou8(conv_buf_root, volume_name));
|
|
|
|
/* use pathimport to convert backslashes to slashes */
|
|
pathimport(fss->root_dir, sizeof(fss->root_dir), u16tou8(conv_buf_root, volume_root));
|
|
|
|
/* the returned root_dir should match the start of the resolved dir */
|
|
if (pathncmp(fss->root_dir, resolved_dir, strlen(fss->root_dir)) != 0) {
|
|
errno = EINVAL;
|
|
log_error(errno, "Not matching VolumeRoot '%s' for '%s'.\n", fss->root_dir, resolved_dir);
|
|
return -1;
|
|
}
|
|
|
|
size_t root_len = strlen(fss->root_dir);
|
|
pathcpy(fss->sub_dir, sizeof(fss->sub_dir), resolved_dir + root_len);
|
|
|
|
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir);
|
|
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/");
|
|
|
|
if (windows_mkdir(fss->snapshot_dir) != 0 && errno != EEXIST) {
|
|
log_error(errno, "Error creating directory '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
fssnapshot_retiring_name(SNAPSHOT_PENDING, pending_retiring, sizeof(pending_retiring));
|
|
fssnapshot_retiring_name(SNAPSHOT_STABLE, stable_retiring, sizeof(stable_retiring));
|
|
|
|
/* refresh the links, after reboots their DeviceObject paths are invalidated */
|
|
if (windows_rebuild_link(fss, SNAPSHOT_PENDING) != 0)
|
|
return -1;
|
|
|
|
if (windows_rebuild_link(fss, SNAPSHOT_STABLE) != 0)
|
|
return -1;
|
|
|
|
if (windows_rebuild_link(fss, SNAPSHOT_SCAN) != 0)
|
|
return -1;
|
|
|
|
if (windows_rebuild_link(fss, pending_retiring) != 0)
|
|
return -1;
|
|
|
|
if (windows_rebuild_link(fss, stable_retiring) != 0)
|
|
return -1;
|
|
|
|
/* never expose an interrupted replacement to the state layer */
|
|
if (fssnapshot_recover_replace(fss, SNAPSHOT_PENDING) != 0)
|
|
return -1;
|
|
|
|
if (fssnapshot_recover_replace(fss, SNAPSHOT_STABLE) != 0)
|
|
return -1;
|
|
|
|
fss->magic = magic;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st)
|
|
{
|
|
char link_path[PATH_MAX];
|
|
|
|
pathcpy(link_path, sizeof(link_path), fss->snapshot_dir);
|
|
pathcat(link_path, sizeof(link_path), name);
|
|
pathcat(link_path, sizeof(link_path), SNAPSHOT_GUID); /* we check the GUID as it's the one that matter */
|
|
|
|
/* use lstat because we want to check the link */
|
|
if (windows_lstat(link_path, st) != 0)
|
|
return -1;
|
|
|
|
/* it must be a link to a directory */
|
|
if (!S_ISLNKDIR(st->st_mode)) {
|
|
errno = ENOTDIR;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
char cmd[PS_CMD_MAX];
|
|
char device_path[PATH_MAX];
|
|
char target_path[PATH_MAX];
|
|
char link_path[PATH_MAX];
|
|
char guid_link[PATH_MAX];
|
|
char out[PATH_MAX];
|
|
|
|
/* create VSS shadow copy and capture the system-assigned GUID */
|
|
snprintf(cmd, sizeof(cmd),
|
|
"$r = (Get-WmiObject -List Win32_ShadowCopy).Create('%s', 'ClientAccessible'); "
|
|
"if ($r.ReturnValue -eq 0) { "
|
|
"Write-Output ('ID_' + $r.ShadowID); "
|
|
"} else { "
|
|
"Write-Output ('0x{0:X8}' -f $r.ReturnValue); "
|
|
"}",
|
|
fss->dataset);
|
|
if (windows_ps(cmd, out, sizeof(out)) != 0) {
|
|
log_error(errno, "Error creating snapshot of VolumeName '%s'. %s.\n", fss->dataset, strerror(errno));
|
|
return -1;
|
|
}
|
|
if (strncmp(out, "ID_", 3) != 0) {
|
|
errno = ENODATA;
|
|
log_error(errno, "VSS snapshot creation of '%s' failed with error %s.\n", fss->dataset, out);
|
|
return -1;
|
|
}
|
|
|
|
const char* guid = out + 3;
|
|
|
|
/* do not jump to bail_and_delete if GUID is invalid because an unvalidated GUID cannot be safely used in a WMI query */
|
|
if (windows_guid_is_valid(guid, strlen(guid)) != 0) {
|
|
errno = EINVAL;
|
|
log_error(errno, "Invalid GUID received from VSS shadow copy creation: '%s'\n", guid);
|
|
return -1;
|
|
}
|
|
|
|
/* resolve the shadow copy's device object path */
|
|
snprintf(cmd, sizeof(cmd), "(Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'}).DeviceObject", guid);
|
|
if (windows_ps(cmd, device_path, sizeof(device_path)) != 0) {
|
|
log_error(errno, "Error getting the snapshot DeviceObject from GUID '%s'. %s.\n", guid, strerror(errno));
|
|
goto bail_and_delete;
|
|
}
|
|
if (device_path[0] == 0) {
|
|
errno = ENODATA;
|
|
log_error(errno, "Empty snapshot DeviceObject of GUID '%s'. %s.\n", guid, strerror(errno));
|
|
goto bail_and_delete;
|
|
}
|
|
|
|
/* use pathimport to convert backslashes to slashes */
|
|
pathimport(target_path, sizeof(target_path), device_path);
|
|
|
|
/* the ending slash is required, otherwise the link won't work */
|
|
pathslash(target_path, sizeof(target_path));
|
|
|
|
pathcpy(link_path, sizeof(link_path), fss->snapshot_dir);
|
|
pathcat(link_path, sizeof(link_path), name);
|
|
|
|
/* remove potential stale link */
|
|
windows_rmdir(link_path);
|
|
|
|
int ret = windows_symlink_directory(target_path, link_path);
|
|
if (ret != 0) {
|
|
log_error(errno, "Error creating symlink '%s'. %s.\n", link_path, strerror(errno));
|
|
goto bail_and_delete;
|
|
}
|
|
|
|
pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir);
|
|
pathcat(guid_link, sizeof(guid_link), name);
|
|
pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID);
|
|
|
|
/* store GUID persistently as symlink target */
|
|
ret = windows_symlink_directory(guid, guid_link);
|
|
if (ret != 0) {
|
|
log_error(errno, "Error creating GUID symlink '%s'. %s.\n", guid_link, strerror(errno));
|
|
windows_rmdir(link_path);
|
|
goto bail_and_delete;
|
|
}
|
|
|
|
return 0;
|
|
|
|
bail_and_delete:
|
|
/* destroy the shadow copy we just created */
|
|
snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid);
|
|
windows_ps(cmd, 0, 0);
|
|
return -1;
|
|
}
|
|
|
|
int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name)
|
|
{
|
|
char guid[PATH_MAX];
|
|
char cmd[PS_CMD_MAX];
|
|
char guid_link[PATH_MAX];
|
|
|
|
pathcpy(guid_link, sizeof(guid_link), fss->snapshot_dir);
|
|
pathcat(guid_link, sizeof(guid_link), name);
|
|
pathcat(guid_link, sizeof(guid_link), SNAPSHOT_GUID);
|
|
|
|
if (windows_read_guid(guid_link, guid, sizeof(guid)) != 0)
|
|
return -1;
|
|
|
|
/* if the GUID link is gone, assume everything is gone */
|
|
if (guid[0] == 0) {
|
|
/* remove as not valid anymore */
|
|
windows_delete_link(fss, name);
|
|
return 0;
|
|
}
|
|
|
|
/* destroy the VSS shadow copy */
|
|
snprintf(cmd, sizeof(cmd), "Get-WmiObject Win32_ShadowCopy | Where-Object {$_.ID -eq '%s'} | Remove-WmiObject", guid);
|
|
int ret = windows_ps(cmd, 0, 0);
|
|
if (ret != 0) {
|
|
log_error(errno, "Error destroy the snapshot from GUID '%s'. %s.\n", guid, strerror(errno));
|
|
/* remove as not valid anymore */
|
|
windows_delete_link(fss, name);
|
|
return -1;
|
|
}
|
|
|
|
/* remove live symlink if present */
|
|
windows_delete_link(fss, name);
|
|
|
|
/* remove persistent GUID symlink */
|
|
if (windows_rmdir(guid_link) != 0) {
|
|
log_error(errno, "Error rmdir '%s'. %s.\n", guid_link, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
|
|
{
|
|
char old_guid[PATH_MAX];
|
|
char new_guid[PATH_MAX];
|
|
char old_link[PATH_MAX];
|
|
char new_link[PATH_MAX];
|
|
|
|
pathcpy(old_guid, sizeof(old_guid), fss->snapshot_dir);
|
|
pathcat(old_guid, sizeof(old_guid), old_name);
|
|
pathcat(old_guid, sizeof(old_guid), SNAPSHOT_GUID);
|
|
pathcpy(new_guid, sizeof(new_guid), fss->snapshot_dir);
|
|
pathcat(new_guid, sizeof(new_guid), new_name);
|
|
pathcat(new_guid, sizeof(new_guid), SNAPSHOT_GUID);
|
|
pathcpy(old_link, sizeof(old_link), fss->snapshot_dir);
|
|
pathcat(old_link, sizeof(old_link), old_name);
|
|
pathcpy(new_link, sizeof(new_link), fss->snapshot_dir);
|
|
pathcat(new_link, sizeof(new_link), new_name);
|
|
|
|
if (windows_rename(old_guid, new_guid) != 0) {
|
|
log_error(errno, "Error renaming '%s' to '%s'. %s.\n", old_guid, new_guid, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if (windows_rename(old_link, new_link) != 0) {
|
|
log_error(errno, "Error renaming '%s' to '%s'. %s.\n", old_link, new_link, strerror(errno));
|
|
/* remove as not valid anymore */
|
|
windows_delete_link(fss, old_name);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int fssnapshot_replace(struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
|
|
{
|
|
char retiring_name[PATH_MAX];
|
|
|
|
fssnapshot_retiring_name(new_name, retiring_name, sizeof(retiring_name));
|
|
|
|
/* resolve a replacement interrupted by an earlier invocation before starting another one */
|
|
if (fssnapshot_recover_replace(fss, new_name) != 0)
|
|
return -1;
|
|
|
|
int current = fssnapshot_exists(fss, new_name);
|
|
if (current < 0)
|
|
return -1;
|
|
|
|
if (current == 0)
|
|
return fssnapshot_rename(fss, old_name, new_name);
|
|
|
|
/* keep the previous destination recoverable until the source is published under the final name */
|
|
if (fssnapshot_rename(fss, new_name, retiring_name) != 0)
|
|
return -1;
|
|
|
|
/* on failure leave .retiring in place; fssnapshot_mount() will restore or complete the transition */
|
|
if (fssnapshot_rename(fss, old_name, new_name) != 0)
|
|
return -1;
|
|
|
|
/* the new destination is now published, so the previous destination is no longer required */
|
|
return fssnapshot_delete(fss, retiring_name);
|
|
}
|
|
|
|
int fssnapshot_path(const struct fssnapshot_struct* fss, const char* name, char* path, size_t path_size)
|
|
{
|
|
pathcpy(path, path_size, fss->snapshot_dir);
|
|
pathcat(path, path_size, name);
|
|
pathcatc(path, path_size, '/');
|
|
pathcat(path, path_size, fss->sub_dir);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void fssnapshot_unmount(const struct fssnapshot_struct* fss)
|
|
{
|
|
char pending_retiring[PATH_MAX];
|
|
char stable_retiring[PATH_MAX];
|
|
|
|
/*
|
|
* Remove symlinks on exit to prevent "Dangling Links."
|
|
* Since \Device\HarddiskVolumeShadowCopyN paths are reassigned by the kernel
|
|
* at boot, a stale link would either point to a non-existent device or,
|
|
* worse, a different disk's snapshot.
|
|
*
|
|
* Deleting the link ensures system hygiene and prevents users from seeing
|
|
* "Location not available" errors or incorrect data after a reboot.
|
|
*/
|
|
fssnapshot_retiring_name(SNAPSHOT_PENDING, pending_retiring, sizeof(pending_retiring));
|
|
fssnapshot_retiring_name(SNAPSHOT_STABLE, stable_retiring, sizeof(stable_retiring));
|
|
|
|
windows_delete_link(fss, SNAPSHOT_PENDING);
|
|
windows_delete_link(fss, SNAPSHOT_STABLE);
|
|
windows_delete_link(fss, SNAPSHOT_SCAN);
|
|
windows_delete_link(fss, pending_retiring);
|
|
windows_delete_link(fss, stable_retiring);
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* resolve */
|
|
|
|
/**
|
|
* Get the device file from a path inside the device.
|
|
*/
|
|
static int devresolve(const char* mount, char* file, size_t file_size, char* wfile, size_t wfile_size)
|
|
{
|
|
wchar_t conv_buf_mount[CONV_MAX];
|
|
char conv_buf_volume_guid[CONV_MAX];
|
|
WCHAR volume_mount[PATH_MAX];
|
|
WCHAR volume_guid[PATH_MAX];
|
|
DWORD i;
|
|
char* p;
|
|
|
|
/* get the volume mount point from the disk path */
|
|
if (!GetVolumePathNameW(convert(conv_buf_mount, mount), volume_mount, sizeof(volume_mount) / sizeof(WCHAR))) {
|
|
windows_errno(GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
/* get the volume GUID path from the mount point */
|
|
if (!GetVolumeNameForVolumeMountPointW(volume_mount, volume_guid, sizeof(volume_guid) / sizeof(WCHAR))) {
|
|
windows_errno(GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Remove the final slash, otherwise CreateFile() opens the file-system
|
|
* and not the volume
|
|
*/
|
|
i = 0;
|
|
while (volume_guid[i] != 0)
|
|
++i;
|
|
if (i != 0 && volume_guid[i - 1] == '\\')
|
|
volume_guid[i - 1] = 0;
|
|
|
|
pathcpy(wfile, wfile_size, u16tou8(conv_buf_volume_guid, volume_guid));
|
|
|
|
/* get the GUID start { */
|
|
p = strchr(wfile, '{');
|
|
if (!p)
|
|
p = wfile;
|
|
else
|
|
++p;
|
|
|
|
pathprint(file, file_size, "/dev/vol%s", p);
|
|
|
|
/* cut GUID end } */
|
|
p = strrchr(file, '}');
|
|
if (p)
|
|
*p = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* uuid */
|
|
|
|
int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid_size)
|
|
{
|
|
(void)device_path;
|
|
|
|
/* just use the volume serial number returned in the device parameter */
|
|
snprintf(uuid, uuid_size, "%08x", (unsigned)device_id);
|
|
|
|
log_tag("uuid:windows:%u:%s:\n", (unsigned)device_id, uuid);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* dev */
|
|
|
|
/**
|
|
* Read a device tree filling the specified list of disk_t entries.
|
|
*/
|
|
static int devtree(devinfo_t* parent, tommy_list* list)
|
|
{
|
|
wchar_t conv_buf[CONV_MAX];
|
|
HANDLE h;
|
|
unsigned char vde_buffer[sizeof(VOLUME_DISK_EXTENTS)];
|
|
VOLUME_DISK_EXTENTS* vde = (VOLUME_DISK_EXTENTS*)&vde_buffer;
|
|
unsigned vde_size = sizeof(vde_buffer);
|
|
void* vde_alloc = 0;
|
|
BOOL ret;
|
|
DWORD n;
|
|
DWORD i;
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, parent->wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
windows_errno(GetLastError());
|
|
return -1;
|
|
}
|
|
|
|
/* get the physical extents of the volume */
|
|
ret = DeviceIoControl(h, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, 0, 0, vde, vde_size, &n, 0);
|
|
if (!ret) {
|
|
DWORD error = GetLastError();
|
|
if (error != ERROR_MORE_DATA) {
|
|
CloseHandle(h);
|
|
windows_errno(error);
|
|
return -1;
|
|
}
|
|
|
|
/* more than one extends, allocate more space */
|
|
vde_size = sizeof(VOLUME_DISK_EXTENTS) + vde->NumberOfDiskExtents * sizeof(DISK_EXTENT);
|
|
vde_alloc = malloc_nofail(vde_size);
|
|
vde = vde_alloc;
|
|
|
|
/* retry with more space */
|
|
ret = DeviceIoControl(h, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, 0, 0, vde, vde_size, &n, 0);
|
|
}
|
|
if (!ret) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(h);
|
|
free(vde_alloc);
|
|
windows_errno(error);
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < vde->NumberOfDiskExtents; ++i) {
|
|
devinfo_t* devinfo = calloc_nofail(1, sizeof(devinfo_t));
|
|
|
|
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));
|
|
devinfo->device = vde->Extents[i].DiskNumber;
|
|
pathprint(devinfo->file, sizeof(devinfo->file), "/dev/pd%" PRIu64, devinfo->device);
|
|
pathprint(devinfo->wfile, sizeof(devinfo->wfile), "\\\\.\\PhysicalDrive%" PRIu64, devinfo->device);
|
|
devinfo->parent = parent;
|
|
|
|
/* insert in the list */
|
|
tommy_list_insert_tail(list, &devinfo->node, devinfo);
|
|
}
|
|
|
|
if (!CloseHandle(h)) {
|
|
windows_errno(GetLastError());
|
|
free(vde_alloc);
|
|
return -1;
|
|
}
|
|
|
|
free(vde_alloc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Compute disk usage by aggregating access statistics.
|
|
*
|
|
* On many Windows versions, these raw disk performance counters are disabled by default to save overhead.
|
|
*
|
|
* If DeviceIoControl call with IOCTL_DISK_PERFORMANCE returns zeros or fails, the counters are likely disabled.
|
|
*
|
|
* Run the following command from an elevated command prompt:
|
|
*
|
|
* diskperf -y
|
|
*
|
|
* This change usually requires a reboot to take effect at the driver level.
|
|
*/
|
|
static int devstat(uint64_t device, const char* name, const char* wfile, uint64_t* count)
|
|
{
|
|
wchar_t conv_buf[CONV_MAX];
|
|
HANDLE h;
|
|
BOOL ret;
|
|
DISK_PERFORMANCE ds;
|
|
DWORD bytes;
|
|
char file[128];
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
bytes = 0;
|
|
ret = DeviceIoControl(h, IOCTL_DISK_PERFORMANCE, NULL, 0, &ds, sizeof(ds), &bytes, NULL);
|
|
if (!ret) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(h);
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
if (!CloseHandle(h)) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
/* accumulate in 64-bit to avoid 32-bit overflow when adding the two DWORD counters */
|
|
*count = ds.ReadCount;
|
|
*count += ds.WriteCount;
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define ARGS_MAX 64
|
|
|
|
/**
|
|
* Build the absolute path to smartctl.exe located in the application directory.
|
|
*/
|
|
static int smartctl_executable(char* path, size_t size)
|
|
{
|
|
char conv_buf[CONV_MAX];
|
|
const wchar_t* dir = windows_exedir();
|
|
int ret;
|
|
|
|
if (!dir || dir[0] == 0) {
|
|
errno = ENOENT;
|
|
return -1;
|
|
}
|
|
|
|
ret = snprintf(path, size, "%ssmartctl.exe", u16tou8(conv_buf, dir));
|
|
if (ret < 0 || (size_t)ret >= size) {
|
|
errno = ENAMETOOLONG;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get SMART attributes.
|
|
*/
|
|
static int devsmart(uint64_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* inter)
|
|
{
|
|
char smartctl_path[CONV_MAX];
|
|
char info_buf[SMARTCTL_MAX];
|
|
char extra_args[PATH_MAX];
|
|
char extra_split[PATH_MAX];
|
|
const char* argv[ARGS_MAX];
|
|
char file[128];
|
|
unsigned argc;
|
|
OS_FILE* f;
|
|
int ret;
|
|
int count;
|
|
|
|
if (smartctl_executable(smartctl_path, sizeof(smartctl_path)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to build smartctl executable path.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
|
|
|
|
count = 0;
|
|
|
|
retry:
|
|
argc = 0;
|
|
argv[argc++] = smartctl_path;
|
|
|
|
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 */
|
|
}
|
|
|
|
if (count != 0) {
|
|
if (argc >= ARGS_MAX - 3) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
argv[argc++] = "-d";
|
|
argv[argc++] = "sat";
|
|
}
|
|
|
|
if (smartctl[0] && count == 0) {
|
|
pathprint(extra_args, sizeof(extra_args), smartctl, file);
|
|
pathcpy(extra_split, sizeof(extra_split), extra_args);
|
|
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
|
|
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, smartctl_path, 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;
|
|
if (count != 0)
|
|
log_tag("smartctl:%s:%s:run: %s [info: %s] -d sat %s\n", file, name, smartctl_path, info_opts, file);
|
|
else
|
|
log_tag("smartctl:%s:%s:run: %s [info: %s] %s\n", file, name, smartctl_path, 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] %s'.\n", smartctl_path, info_opts, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] -d sat %s'.\n", smartctl_path, info_opts, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] %s'.\n", smartctl_path, info_opts, file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, inter) != 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] %s' (not exited).\n", smartctl_path, info_opts, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] -d sat %s' (not exited).\n", smartctl_path, info_opts, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s [info: %s] %s' (not exited).\n", smartctl_path, info_opts, file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if first try without custom smartctl command */
|
|
if (count == 0 && smartctl[0] == 0) {
|
|
/*
|
|
* Handle some common cases in Windows.
|
|
*
|
|
* Sometimes the "type" autodetection is wrong, and the command fails at identification
|
|
* stage, returning with error 2, or even with error 0, and with no info at all.
|
|
* We detect this condition checking the PowerOnHours, Size and RotationRate attributes.
|
|
*
|
|
* In such conditions we retry using the "sat" type, that often allows to proceed.
|
|
*
|
|
* Note that getting error 4 is instead very common, even with full info gathering.
|
|
*/
|
|
if ((WEXITSTATUS(ret) == 0 || WEXITSTATUS(ret) == 2)
|
|
&& smart[SMART_POWER_ON_HOURS].raw == SMART_UNASSIGNED
|
|
&& info[INFO_SIZE] == SMART_UNASSIGNED
|
|
&& info[INFO_ROTATION_RATE] == SMART_UNASSIGNED
|
|
) {
|
|
++count;
|
|
goto retry;
|
|
}
|
|
}
|
|
|
|
/* store the smartctl return value */
|
|
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int serial_descriptor(PSTORAGE_DEVICE_DESCRIPTOR descriptor, size_t size, char* output, size_t output_size)
|
|
{
|
|
if (descriptor->SerialNumberOffset == 0
|
|
|| descriptor->SerialNumberOffset == (DWORD)-1
|
|
|| descriptor->SerialNumberOffset >= size)
|
|
return -1;
|
|
|
|
const char* raw = (const char*)descriptor + descriptor->SerialNumberOffset;
|
|
const char* end = memchr(raw, 0, size - descriptor->SerialNumberOffset);
|
|
if (!end)
|
|
return -1;
|
|
|
|
size_t len = end - raw;
|
|
if (len + 1 > output_size)
|
|
return -1;
|
|
|
|
/* a serial cannot be too short */
|
|
if (len < 6)
|
|
return -1;
|
|
|
|
/* all must be printable */
|
|
for (size_t i = 0; i < len; ++i) {
|
|
if (!isprint((unsigned char)raw[i]))
|
|
return -1;
|
|
}
|
|
|
|
memcpy(output, raw, len);
|
|
output[len] = 0;
|
|
|
|
strtrim(output);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int product_descriptor(PSTORAGE_DEVICE_DESCRIPTOR descriptor, size_t size, char* output, size_t output_size)
|
|
{
|
|
if (descriptor->ProductIdOffset == 0
|
|
|| descriptor->ProductIdOffset == (DWORD)-1
|
|
|| descriptor->ProductIdOffset >= size)
|
|
return -1;
|
|
|
|
const char* raw = (const char*)descriptor + descriptor->ProductIdOffset;
|
|
const char* end = memchr(raw, 0, size - descriptor->ProductIdOffset);
|
|
if (!end)
|
|
return -1;
|
|
|
|
size_t len = end - raw;
|
|
if (len + 1 > output_size)
|
|
return -1;
|
|
|
|
/* all must be printable */
|
|
for (size_t i = 0; i < len; ++i) {
|
|
if (!isprint((unsigned char)raw[i]))
|
|
return -1;
|
|
}
|
|
|
|
memcpy(output, raw, len);
|
|
output[len] = 0;
|
|
|
|
strtrim(output);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void devattr_property(HANDLE h, char* serial, char* model, char* inter)
|
|
{
|
|
STORAGE_PROPERTY_QUERY query = { 0 };
|
|
STORAGE_DESCRIPTOR_HEADER header = { 0 };
|
|
STORAGE_DEVICE_DESCRIPTOR* desc;
|
|
size_t descriptor_size;
|
|
DWORD bytes;
|
|
|
|
query.PropertyId = StorageDeviceProperty;
|
|
query.QueryType = PropertyStandardQuery;
|
|
if (!DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), &header, sizeof(header), &bytes, 0))
|
|
return;
|
|
if (header.Size < sizeof(STORAGE_DEVICE_DESCRIPTOR))
|
|
return;
|
|
|
|
/* allocate buffer and query full descriptor */
|
|
BYTE* buffer = malloc(header.Size);
|
|
if (!buffer)
|
|
return;
|
|
|
|
if (!DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), buffer, header.Size, &bytes, 0)) {
|
|
free(buffer);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Do not trust the descriptor size or offsets returned by a storage driver.
|
|
* DeviceIoControl() may return fewer bytes than requested, and strings in
|
|
* the descriptor are only offsets into the returned byte range.
|
|
*/
|
|
if (bytes < sizeof(STORAGE_DEVICE_DESCRIPTOR)) {
|
|
free(buffer);
|
|
return;
|
|
}
|
|
|
|
desc = (STORAGE_DEVICE_DESCRIPTOR*)buffer;
|
|
descriptor_size = bytes;
|
|
if (descriptor_size > header.Size)
|
|
descriptor_size = header.Size;
|
|
if (desc->Size < sizeof(STORAGE_DEVICE_DESCRIPTOR) || desc->Size > descriptor_size) {
|
|
free(buffer);
|
|
return;
|
|
}
|
|
descriptor_size = desc->Size;
|
|
|
|
/* extract strings (offsets are from start of descriptor) */
|
|
if (model && *model == 0) {
|
|
product_descriptor(desc, descriptor_size, model, SMART_MAX);
|
|
}
|
|
|
|
if (serial && *serial == 0) {
|
|
/*
|
|
* This is the RAW serial, maybe HEX encoded, maybe BYTES SWAPPED,
|
|
* but we don't care because we just need any identifier.
|
|
*
|
|
* If smartctl was not able to read it, we now accept anything.
|
|
*/
|
|
serial_descriptor(desc, descriptor_size, serial, SMART_MAX);
|
|
}
|
|
|
|
#define BusTypeVirtual 0x0E
|
|
#define BusTypeFileBackedVirtual 0x0F
|
|
#define BusTypeSpaces 0x10
|
|
#define BusTypeNvme 0x11
|
|
#define BusTypeSCM 0x12
|
|
#define BusTypeUfs 0x13
|
|
#define BusTypeNvmeof 0x14
|
|
|
|
const char* type = 0;
|
|
switch ((int)desc->BusType) { /* cast to int to avoid warnings about enum unlisted */
|
|
case BusTypeScsi : type = "SCSI"; break;
|
|
case BusTypeAtapi : type = "ATAPI"; break;
|
|
case BusTypeAta : type = "ATA"; break;
|
|
case BusType1394 : type = "FireWire"; break;
|
|
case BusTypeSsa : type = "SSA"; break;
|
|
case BusTypeFibre : type = "Fibre"; break;
|
|
case BusTypeUsb : type = "USB"; break;
|
|
case BusTypeRAID : type = "RAID"; break;
|
|
case BusTypeiScsi : type = "iSCSI"; break;
|
|
case BusTypeSas : type = "SAS"; break;
|
|
case BusTypeSata : type = "SATA"; break;
|
|
case BusTypeSd : type = "SD"; break; /* Secure Digital cards */
|
|
case BusTypeMmc : type = "MMC"; break; /* NAND Embedded MultiMediaCards (eMMC) */
|
|
case BusTypeVirtual : type = "Virtual"; break;
|
|
case BusTypeFileBackedVirtual : type = "Virtual"; break;
|
|
case BusTypeSpaces : type = "Storage Spaces"; break;
|
|
case BusTypeNvme : type = "NVMe"; break; /* NAND */
|
|
case BusTypeSCM : type = "SCM"; break; /* NAND Storage Class Memory */
|
|
case BusTypeUfs : type = "UFS"; break; /* NAND Universal Flash Storage */
|
|
case BusTypeNvmeof : type = "NVMe"; break; /* NAND NVMe over Fabrics */
|
|
default :
|
|
}
|
|
if (inter && type) /* always override interface as more detailed than smartctl */
|
|
snprintf(inter, SMART_MAX, "%s", type);
|
|
|
|
free(buffer);
|
|
}
|
|
|
|
static void devattr_size(HANDLE h, uint64_t* size)
|
|
{
|
|
DISK_GEOMETRY_EX geom = { 0 };
|
|
DWORD bytes;
|
|
if (!DeviceIoControl(h, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, 0, 0, &geom, sizeof(geom), &bytes, 0))
|
|
return;
|
|
|
|
*size = geom.DiskSize.QuadPart;
|
|
}
|
|
|
|
static void devattr_rotational(HANDLE h, uint64_t* rotational)
|
|
{
|
|
STORAGE_PROPERTY_QUERY query = { 0 };
|
|
DEVICE_SEEK_PENALTY_DESCRIPTOR desc = { 0 };
|
|
DWORD bytes;
|
|
|
|
query.PropertyId = StorageDeviceSeekPenaltyProperty;
|
|
query.QueryType = PropertyStandardQuery;
|
|
if (!DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), &desc, sizeof(desc), &bytes, 0))
|
|
return;
|
|
if (bytes < sizeof(desc))
|
|
return;
|
|
if (desc.Version != sizeof(DEVICE_SEEK_PENALTY_DESCRIPTOR) || desc.Size != sizeof(DEVICE_SEEK_PENALTY_DESCRIPTOR))
|
|
return;
|
|
|
|
*rotational = desc.IncursSeekPenalty ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Get device attributes.
|
|
*/
|
|
static void devattr(uint64_t device, const char* name, const char* wfile, uint64_t* info, char* serial, char* family, char* model, char* interf)
|
|
{
|
|
HANDLE h;
|
|
wchar_t conv_buf[CONV_MAX];
|
|
char file[128];
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
(void)family; /* not available, smartctl uses an internal database to get it */
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return;
|
|
}
|
|
|
|
if (info[INFO_SIZE] == SMART_UNASSIGNED)
|
|
devattr_size(h, &info[INFO_SIZE]);
|
|
|
|
if (*model == 0 || *serial == 0 || *interf == 0)
|
|
devattr_property(h, serial, model, interf);
|
|
|
|
/* set NVMe as not rotational */
|
|
if (info[INFO_ROTATION_RATE] == SMART_UNASSIGNED && strcasecmp(interf, "nvme") == 0)
|
|
info[INFO_ROTATION_RATE] = 0;
|
|
|
|
/* set SSD as not rotational */
|
|
if (info[INFO_ROTATION_RATE] == SMART_UNASSIGNED)
|
|
devattr_rotational(h, &info[INFO_ROTATION_RATE]);
|
|
|
|
if (!CloseHandle(h)) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the device needs power management (it's a rotational one)
|
|
*/
|
|
static int devpower(uint64_t device, const char* name, const char* wfile)
|
|
{
|
|
HANDLE h;
|
|
wchar_t conv_buf[CONV_MAX];
|
|
char file[128];
|
|
uint64_t rotational;
|
|
char interf[SMART_MAX];
|
|
|
|
rotational = SMART_UNASSIGNED;
|
|
interf[0] = 0;
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return 1; /* assume rotational if unknown */
|
|
}
|
|
|
|
/* get the interface */
|
|
devattr_property(h, 0, 0, interf);
|
|
|
|
/* set NVMe as not rotational */
|
|
if (strcasecmp(interf, "nvme") == 0)
|
|
rotational = 0;
|
|
|
|
/* set SSD as not rotational */
|
|
if (rotational == SMART_UNASSIGNED)
|
|
devattr_rotational(h, &rotational);
|
|
|
|
if (!CloseHandle(h)) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return 1; /* assume rotational if unknown */
|
|
}
|
|
|
|
if (rotational == SMART_UNASSIGNED)
|
|
return 1; /* assume rotational if unknown */
|
|
|
|
return rotational != 0;
|
|
}
|
|
|
|
/**
|
|
* Get POWER state
|
|
*/
|
|
static int devprobe(uint64_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* interf)
|
|
{
|
|
char smartctl_path[CONV_MAX];
|
|
char info_buf[SMARTCTL_MAX];
|
|
char extra_args[PATH_MAX];
|
|
char extra_split[PATH_MAX];
|
|
const char* argv[ARGS_MAX];
|
|
char file[128];
|
|
unsigned argc;
|
|
OS_FILE* f;
|
|
int ret;
|
|
int count;
|
|
|
|
if (smartctl_executable(smartctl_path, sizeof(smartctl_path)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to build smartctl executable path.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
|
|
|
|
count = 0;
|
|
|
|
retry:
|
|
argc = 0;
|
|
argv[argc++] = smartctl_path;
|
|
argv[argc++] = "-n";
|
|
argv[argc++] = "standby,3";
|
|
|
|
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 */
|
|
}
|
|
|
|
if (count != 0) {
|
|
if (argc >= ARGS_MAX - 3) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
argv[argc++] = "-d";
|
|
argv[argc++] = "sat";
|
|
}
|
|
|
|
if (smartctl[0] && count == 0) {
|
|
pathprint(extra_args, sizeof(extra_args), smartctl, file);
|
|
pathcpy(extra_split, sizeof(extra_split), extra_args);
|
|
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
|
|
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, smartctl_path, 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;
|
|
if (count != 0)
|
|
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] -d sat %s\n", file, name, smartctl_path, info_opts, file);
|
|
else
|
|
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] %s\n", file, name, smartctl_path, 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", smartctl_path, info_opts, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s -d sat %s'.\n", smartctl_path, info_opts, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", smartctl_path, info_opts, file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, interf) != 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", smartctl_path, info_opts, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s -d sat %s' (not exited).\n", smartctl_path, info_opts, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", smartctl_path, info_opts, file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if first try without custom smartctl command */
|
|
if (count == 0 && smartctl[0] == 0) {
|
|
/*
|
|
* Handle some common cases in Windows.
|
|
*
|
|
* Sometimes the "type" autodetection is wrong, and the command fails at identification
|
|
* stage, returning with error 2.
|
|
*
|
|
* In such conditions we retry using the "sat" type, that often allows to proceed.
|
|
*/
|
|
if (WEXITSTATUS(ret) == 2) {
|
|
++count;
|
|
goto retry;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Spin down a specific device.
|
|
*/
|
|
static int devdown(uint64_t device, const char* name, const char* smartctl)
|
|
{
|
|
char smartctl_path[CONV_MAX];
|
|
char extra_args[PATH_MAX];
|
|
char extra_split[PATH_MAX];
|
|
const char* argv[ARGS_MAX];
|
|
char file[128];
|
|
unsigned argc;
|
|
OS_FILE* f;
|
|
int ret;
|
|
int count;
|
|
|
|
if (smartctl_executable(smartctl_path, sizeof(smartctl_path)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to build smartctl executable path.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
count = 0;
|
|
|
|
retry:
|
|
argc = 0;
|
|
argv[argc++] = smartctl_path;
|
|
argv[argc++] = "-s";
|
|
argv[argc++] = "standby,now";
|
|
|
|
if (count != 0) {
|
|
if (argc >= ARGS_MAX - 3) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
argv[argc++] = "-d";
|
|
argv[argc++] = "sat";
|
|
}
|
|
|
|
if (smartctl[0] && count == 0) {
|
|
pathprint(extra_args, sizeof(extra_args), smartctl, file);
|
|
pathcpy(extra_split, sizeof(extra_split), extra_args);
|
|
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
|
|
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, smartctl_path, 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;
|
|
if (count != 0)
|
|
log_tag("smartctl:%s:%s:run: %s -s standby,now -d sat %s\n", file, name, smartctl_path, file);
|
|
else
|
|
log_tag("smartctl:%s:%s:run: %s -s standby,now %s\n", file, name, smartctl_path, 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", smartctl_path, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now -d sat %s'.\n", smartctl_path, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", smartctl_path, 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] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", smartctl_path, extra_args);
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now -d sat %s' (not exited).\n", smartctl_path, file);
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", smartctl_path, file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if first try without custom smartctl command */
|
|
if (count == 0 && smartctl[0] == 0) {
|
|
/*
|
|
* Handle some common cases in Windows.
|
|
*
|
|
* Sometimes the "type" autodetection is wrong, and the command fails at identification
|
|
* stage, returning with error 2.
|
|
*
|
|
* In such conditions we retry using the "sat" type, that often allows to proceed.
|
|
*/
|
|
if (WEXITSTATUS(ret) == 2) {
|
|
++count;
|
|
goto retry;
|
|
}
|
|
}
|
|
|
|
if (WEXITSTATUS(ret) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("device:%s:%s:exit:%d\n", file, name, WEXITSTATUS(ret));
|
|
if (smartctl[0] && count == 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", smartctl_path, extra_args, WEXITSTATUS(ret));
|
|
else if (count != 0)
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now -d sat %s' with return code %xh.\n", smartctl_path, file, WEXITSTATUS(ret));
|
|
else
|
|
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", smartctl_path, file, WEXITSTATUS(ret));
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("attr:%s:%s:power:down\n", file, name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Spin down a specific device if it's up
|
|
*/
|
|
static int devdownifup(uint64_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;
|
|
}
|
|
|
|
/**
|
|
* Spin up a device.
|
|
*/
|
|
static int devup(uint64_t device, const char* name, const char* wfile)
|
|
{
|
|
wchar_t conv_buf[CONV_MAX];
|
|
HANDLE h;
|
|
BOOL ret;
|
|
DISK_GEOMETRY dg;
|
|
DWORD bytes;
|
|
void* buffer;
|
|
char file[128];
|
|
|
|
snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, wfile), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
bytes = 0;
|
|
ret = DeviceIoControl(h, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &dg, sizeof(dg), &bytes, NULL);
|
|
if (!ret) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(h);
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
buffer = VirtualAlloc(NULL, dg.BytesPerSector, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
if (!buffer) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(h);
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
bytes = 0;
|
|
if (!ReadFile(h, buffer, dg.BytesPerSector, &bytes, NULL)) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(h);
|
|
VirtualFree(buffer, 0, MEM_RELEASE);
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
if (!CloseHandle(h)) {
|
|
DWORD error = GetLastError();
|
|
VirtualFree(buffer, 0, MEM_RELEASE);
|
|
windows_errno(error);
|
|
log_tag("device:%s:%s:error:%lu\n", file, name, error);
|
|
return -1;
|
|
}
|
|
|
|
VirtualFree(buffer, 0, MEM_RELEASE);
|
|
|
|
log_tag("attr:%s:%s:power:up\n", file, name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Thread for spinning up.
|
|
*/
|
|
static void* thread_spinup(void* arg)
|
|
{
|
|
devinfo_t* devinfo = arg;
|
|
uint64_t start;
|
|
|
|
/* skip not rotational devices */
|
|
if (devpower(devinfo->device, devinfo->name, devinfo->wfile) == 0)
|
|
return 0;
|
|
|
|
start = os_tick_ms();
|
|
|
|
if (devup(devinfo->device, devinfo->name, devinfo->wfile) != 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->name, devinfo->wfile, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Thread for spinning down.
|
|
*/
|
|
static void* thread_spindown(void* arg)
|
|
{
|
|
devinfo_t* devinfo = arg;
|
|
uint64_t start;
|
|
|
|
/* skip not rotational devices */
|
|
if (devpower(devinfo->device, devinfo->name, devinfo->wfile) == 0)
|
|
return 0;
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Thread for spinning down.
|
|
*/
|
|
static void* thread_spindownifup(void* arg)
|
|
{
|
|
devinfo_t* devinfo = arg;
|
|
uint64_t start;
|
|
int power;
|
|
|
|
/* skip not rotational devices */
|
|
if (devpower(devinfo->device, devinfo->name, devinfo->wfile) == 0)
|
|
return 0;
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Thread for getting smart info.
|
|
*/
|
|
static void* thread_smart(void* arg)
|
|
{
|
|
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->name, devinfo->wfile, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Thread for getting power info.
|
|
*/
|
|
static void* thread_probe(void* arg)
|
|
{
|
|
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->name, devinfo->wfile, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int device_thread(tommy_list* list, void* (*func)(void* arg))
|
|
{
|
|
int fail = 0;
|
|
tommy_node* i;
|
|
|
|
/* starts all threads */
|
|
for (i = tommy_list_head(list); i != 0; i = i->next) {
|
|
devinfo_t* devinfo = i->data;
|
|
|
|
thread_create(&devinfo->thread, func, devinfo);
|
|
}
|
|
|
|
/* joins 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;
|
|
}
|
|
|
|
if (fail != 0) {
|
|
/* LCOV_EXCL_START */
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void devsync(tommy_list* high)
|
|
{
|
|
(void)high;
|
|
}
|
|
|
|
int devquery(tommy_list* high, tommy_list* low)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* for each device */
|
|
for (i = tommy_list_head(high); i != 0; i = i->next) {
|
|
devinfo_t* devinfo = i->data;
|
|
uint64_t access_stat;
|
|
|
|
if (devresolve(devinfo->mount, devinfo->file, sizeof(devinfo->file), devinfo->wfile, sizeof(devinfo->wfile)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EEXTERNAL, "Failed to resolve path '%s'.\n", devinfo->mount);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* retrieve access stat for the high level device */
|
|
if (devstat(devinfo->device, devinfo->name, devinfo->wfile, &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;
|
|
}
|
|
|
|
/* expand the tree of devices */
|
|
if (devtree(devinfo, low) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EEXTERNAL, "Failed to expand device '%s'.\n", devinfo->file);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int devrun(tommy_list* low, int operation)
|
|
{
|
|
void* (*func)(void* arg) = 0;
|
|
|
|
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)
|
|
{
|
|
for (int i = 0; i < 32; i++) {
|
|
char wfile[PATH_MAX];
|
|
wchar_t conv_buf[CONV_MAX];
|
|
pathprint(wfile, sizeof(wfile), "\\\\.\\PhysicalDrive%d", i);
|
|
HANDLE h;
|
|
|
|
/* open the volume */
|
|
h = CreateFileW(convert(conv_buf, wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
if (error != ERROR_FILE_NOT_FOUND)
|
|
log_tag("enumeration:/dev/pd%d::error:%lu\n", i, error);
|
|
continue;
|
|
}
|
|
|
|
char serial[SMART_MAX];
|
|
serial[0] = 0;
|
|
devattr_property(h, serial, 0, 0);
|
|
if (serial[0]) {
|
|
log_tag("map:/dev/pd%d:%s\n", i, esc_tag(serial));
|
|
}
|
|
|
|
CloseHandle(h);
|
|
}
|
|
|
|
log_tag("map_end\n");
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* temperature */
|
|
|
|
int ambient_temperature(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* app */
|
|
|
|
void app_init(void)
|
|
{
|
|
/*
|
|
* Set LC_ALL=C to make child ignoring the locale when printing info
|
|
*
|
|
* Note anyway that in Windows this trick doesn't work because the
|
|
* Windows locale is used with priority.
|
|
*/
|
|
_wputenv_s(L"LC_ALL", L"C");
|
|
}
|
|
|
|
void app_done(void)
|
|
{
|
|
}
|
|
|
|
void app_default_conf(char* conf, size_t conf_size, const char* argv0)
|
|
{
|
|
char* slash;
|
|
|
|
pathimport(conf, conf_size, argv0);
|
|
|
|
slash = strrchr(conf, '/');
|
|
if (slash) {
|
|
slash[1] = 0;
|
|
pathcat(conf, conf_size, PACKAGE ".conf");
|
|
} else {
|
|
pathcpy(conf, conf_size, PACKAGE ".conf");
|
|
}
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* machine */
|
|
|
|
#ifdef CONFIG_X86
|
|
static void format_freq(char* buf, size_t size, unsigned mhz)
|
|
{
|
|
if (mhz >= 1000)
|
|
snprintf(buf, size, "%.2f GHz", mhz / 1000.0);
|
|
else
|
|
snprintf(buf, size, "%u MHz", mhz);
|
|
}
|
|
#endif
|
|
|
|
void machineinfo(struct machineinfo_struct* info)
|
|
{
|
|
memset(info, 0, sizeof(*info));
|
|
|
|
#if defined(__x86_64__)
|
|
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "x86_64");
|
|
#elif defined(__i386__)
|
|
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "x86_32");
|
|
#elif defined(__aarch64__)
|
|
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "aarch64");
|
|
#elif defined(__arm__)
|
|
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "arm");
|
|
#endif
|
|
|
|
#ifdef CONFIG_X86
|
|
raid_cpu_info(info->cpu_vendor, &info->cpu_family, &info->cpu_model);
|
|
|
|
uint32_t reg[4];
|
|
raid_cpuid(1, 0, reg);
|
|
info->cpu_stepping = reg[0] & 0xF;
|
|
|
|
/* brand string from extended CPUID leaves 0x80000002..0x80000004 */
|
|
raid_cpuid(0x80000000, 0, reg);
|
|
if (reg[0] >= 0x80000004) {
|
|
raid_cpuid(0x80000002, 0, reg);
|
|
memcpy(info->cpu_brand + 0, reg, sizeof(reg));
|
|
raid_cpuid(0x80000003, 0, reg);
|
|
memcpy(info->cpu_brand + 16, reg, sizeof(reg));
|
|
raid_cpuid(0x80000004, 0, reg);
|
|
memcpy(info->cpu_brand + 32, reg, sizeof(reg));
|
|
info->cpu_brand[48] = 0;
|
|
strtrim(info->cpu_brand);
|
|
}
|
|
|
|
/* base and maximum frequency from CPUID leaf 0x16 */
|
|
raid_cpuid(0, 0, reg);
|
|
if (reg[0] >= 0x16) {
|
|
raid_cpuid(0x16, 0, reg);
|
|
unsigned base_mhz = reg[0] & 0xFFFF;
|
|
unsigned max_mhz = reg[1] & 0xFFFF;
|
|
if (base_mhz > 0 && max_mhz > 0 && base_mhz != max_mhz) {
|
|
char base_str[32], max_str[32];
|
|
format_freq(base_str, sizeof(base_str), base_mhz);
|
|
format_freq(max_str, sizeof(max_str), max_mhz);
|
|
snprintf(info->cpu_clock, sizeof(info->cpu_clock), "%s (max %s)", base_str, max_str);
|
|
} else if (base_mhz > 0) {
|
|
format_freq(info->cpu_clock, sizeof(info->cpu_clock), base_mhz);
|
|
} else if (max_mhz > 0) {
|
|
format_freq(info->cpu_clock, sizeof(info->cpu_clock), max_mhz);
|
|
}
|
|
}
|
|
|
|
/* hypervisor detection */
|
|
raid_cpuid(1, 0, reg);
|
|
if (reg[2] & (1U << 31)) {
|
|
raid_cpuid(0x40000000, 0, reg);
|
|
memcpy(info->hypervisor + 0, ®[1], 4);
|
|
memcpy(info->hypervisor + 4, ®[2], 4);
|
|
memcpy(info->hypervisor + 8, ®[3], 4);
|
|
info->hypervisor[12] = 0;
|
|
strtrim(info->hypervisor);
|
|
}
|
|
#endif
|
|
|
|
MEMORYSTATUSEX ms;
|
|
memset(&ms, 0, sizeof(ms));
|
|
ms.dwLength = sizeof(ms);
|
|
if (GlobalMemoryStatusEx(&ms)) {
|
|
info->memory_total_bytes = ms.ullTotalPhys;
|
|
}
|
|
|
|
SYSTEM_INFO si;
|
|
GetSystemInfo(&si);
|
|
info->memory_page_size = si.dwPageSize;
|
|
info->cpu_cores_logical = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
|
|
|
|
/* Query processor topology and cache hierarchy via GetLogicalProcessorInformationEx */
|
|
DWORD len = 0;
|
|
GetLogicalProcessorInformationEx(RelationAll, 0, &len);
|
|
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && len > 0) {
|
|
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* buf = malloc_nofail(len);
|
|
if (GetLogicalProcessorInformationEx(RelationAll, buf, &len)) {
|
|
DWORD offset = 0;
|
|
while (offset < len) {
|
|
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* info_ex = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)((char*)buf + offset);
|
|
if (info_ex->Relationship == RelationProcessorCore) {
|
|
++info->cpu_cores_physical;
|
|
} else if (info_ex->Relationship == RelationCache) {
|
|
CACHE_RELATIONSHIP* cache = &info_ex->Cache;
|
|
/*
|
|
* Select the coherent cache hierarchy associated with the primary
|
|
* core (Core 0). This prevents mixing cache levels from different
|
|
* core types on hybrid architectures (e.g. L1 from P-core, L2 from E-cluster).
|
|
*/
|
|
int matches_core0 = 0;
|
|
if (cache->GroupCount == 0) {
|
|
if (cache->GroupMask.Group == 0 && (cache->GroupMask.Mask & 1) != 0)
|
|
matches_core0 = 1;
|
|
} else {
|
|
for (WORD g = 0; g < cache->GroupCount; ++g) {
|
|
if (cache->GroupMasks[g].Group == 0 && (cache->GroupMasks[g].Mask & 1) != 0) {
|
|
matches_core0 = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matches_core0) {
|
|
if (cache->Level == 1) {
|
|
if (cache->Type == CacheData || cache->Type == CacheUnified)
|
|
info->cache_l1_data = cache->CacheSize;
|
|
else if (cache->Type == CacheInstruction)
|
|
info->cache_l1_instruction = cache->CacheSize;
|
|
} else if (cache->Level == 2) {
|
|
info->cache_l2 = cache->CacheSize;
|
|
} else if (cache->Level == 3) {
|
|
info->cache_l3 = cache->CacheSize;
|
|
}
|
|
}
|
|
}
|
|
if (info_ex->Size == 0)
|
|
break;
|
|
offset += info_ex->Size;
|
|
}
|
|
}
|
|
free(buf);
|
|
}
|
|
|
|
/* OS version via RtlGetVersion */
|
|
typedef LONG (WINAPI* RtlGetVersionPtr)(LPOSVERSIONINFOW);
|
|
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
|
|
if (ntdll) {
|
|
RtlGetVersionPtr pRtlGetVersion = (RtlGetVersionPtr)(void*)GetProcAddress(ntdll, "RtlGetVersion");
|
|
if (pRtlGetVersion) {
|
|
OSVERSIONINFOW vi;
|
|
memset(&vi, 0, sizeof(vi));
|
|
vi.dwOSVersionInfoSize = sizeof(vi);
|
|
if (pRtlGetVersion(&vi) == 0) {
|
|
snprintf(info->os_name, sizeof(info->os_name), "Windows %lu.%lu (Build %lu)",
|
|
(unsigned long)vi.dwMajorVersion, (unsigned long)vi.dwMinorVersion, (unsigned long)vi.dwBuildNumber);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|