@photostructure/fs-metadata 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +15 -0
- package/README.md +2 -1
- package/dist/index.cjs +11 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.mts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.mjs +10 -3
- package/dist/index.mjs.map +1 -1
- package/doc/LINUX_API_REFERENCE.md +310 -0
- package/doc/MACOS_API_REFERENCE.md +367 -31
- package/doc/WINDOWS_API_REFERENCE.md +35 -2
- package/doc/gotchas.md +28 -0
- package/package.json +15 -18
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/scripts/precommit.ts +4 -1
- package/src/common/fd_guard.h +71 -0
- package/src/{darwin → common}/path_security.h +8 -5
- package/src/common/volume_utils.h +51 -0
- package/src/darwin/hidden.cpp +47 -14
- package/src/darwin/raii_utils.h +8 -8
- package/src/darwin/volume_metadata.cpp +33 -39
- package/src/index.ts +3 -3
- package/src/linux/blkid_cache.cpp +5 -11
- package/src/linux/blkid_cache.h +21 -0
- package/src/linux/gio_utils.cpp +7 -23
- package/src/linux/gio_utils.h +16 -40
- package/src/linux/gio_volume_metadata.cpp +16 -88
- package/src/linux/volume_metadata.cpp +35 -27
- package/src/options.ts +16 -3
- package/src/types/options.ts +1 -1
- package/src/windows/drive_status.h +74 -49
- package/src/windows/error_utils.h +2 -2
- package/src/windows/security_utils.h +47 -2
- package/src/windows/thread_pool.h +29 -4
- package/src/windows/volume_metadata.cpp +17 -12
|
@@ -49,94 +49,22 @@ void addMountMetadata(const std::string &mountPoint, VolumeMetadata &metadata) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
GFile *root = g_mount_get_root(mount);
|
|
71
|
-
if (root) {
|
|
72
|
-
char *path = g_file_get_path(root);
|
|
73
|
-
if (path && mountPoint == path) {
|
|
74
|
-
// Found matching mount - try to get rich metadata
|
|
75
|
-
|
|
76
|
-
// Try to get volume label
|
|
77
|
-
if (metadata.label.empty()) {
|
|
78
|
-
GVolume *volume = g_mount_get_volume(mount);
|
|
79
|
-
if (volume) {
|
|
80
|
-
char *label = g_volume_get_name(volume);
|
|
81
|
-
if (label) {
|
|
82
|
-
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, "
|
|
83
|
-
"label: %s} (from GVolume)",
|
|
84
|
-
path, label);
|
|
85
|
-
metadata.label = label;
|
|
86
|
-
g_free(label);
|
|
87
|
-
}
|
|
88
|
-
g_object_unref(volume);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Try to get mount name
|
|
93
|
-
if (metadata.mountName.empty()) {
|
|
94
|
-
char *mount_name = g_mount_get_name(mount);
|
|
95
|
-
if (mount_name) {
|
|
96
|
-
metadata.mountName = mount_name;
|
|
97
|
-
g_free(mount_name);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Try to get URI
|
|
102
|
-
if (metadata.uri.empty()) {
|
|
103
|
-
GFile *location = g_mount_get_default_location(mount);
|
|
104
|
-
if (location) {
|
|
105
|
-
char *uri = g_file_get_uri(location);
|
|
106
|
-
if (uri) {
|
|
107
|
-
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, uri: "
|
|
108
|
-
"%s} (from GMount)",
|
|
109
|
-
path, uri);
|
|
110
|
-
metadata.uri = uri;
|
|
111
|
-
g_free(uri);
|
|
112
|
-
}
|
|
113
|
-
g_object_unref(location);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
g_free(path);
|
|
118
|
-
g_object_unref(root);
|
|
119
|
-
break; // Found our mount
|
|
120
|
-
}
|
|
121
|
-
if (path)
|
|
122
|
-
g_free(path);
|
|
123
|
-
g_object_unref(root);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Clean up mounts list - this time correctly without double-free
|
|
128
|
-
g_list_free_full(mounts,
|
|
129
|
-
reinterpret_cast<GDestroyNotify>(g_object_unref));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Note: Don't unref monitor - it's a singleton
|
|
133
|
-
}
|
|
134
|
-
} catch (const std::exception &e) {
|
|
135
|
-
DEBUG_LOG("[gio::addMountMetadata] GVolumeMonitor enrichment failed "
|
|
136
|
-
"(expected, not critical): %s",
|
|
137
|
-
e.what());
|
|
138
|
-
// Ignore - we have basic metadata from Unix mount API
|
|
139
|
-
}
|
|
52
|
+
// NOTE: GVolumeMonitor enrichment has been removed.
|
|
53
|
+
//
|
|
54
|
+
// According to GIO documentation:
|
|
55
|
+
// https://docs.gtk.org/gio/class.VolumeMonitor.html
|
|
56
|
+
// "GVolumeMonitor is not thread-default-context aware and so should not
|
|
57
|
+
// be used other than from the main thread, with no thread-default-context
|
|
58
|
+
// active."
|
|
59
|
+
//
|
|
60
|
+
// This function is called from Napi::AsyncWorker::Execute() which runs
|
|
61
|
+
// on a worker thread. Using GVolumeMonitor here causes race conditions
|
|
62
|
+
// leading to GLib-GObject-CRITICAL errors like:
|
|
63
|
+
// "g_object_ref: assertion '!object_already_finalized' failed"
|
|
64
|
+
//
|
|
65
|
+
// The basic metadata (fstype, mountFrom) from g_unix_mounts_get() is
|
|
66
|
+
// sufficient and thread-safe. Rich metadata (label, mountName, uri) can
|
|
67
|
+
// be obtained from blkid or other thread-safe sources.
|
|
140
68
|
|
|
141
69
|
return false; // Stop iteration, we found our mount
|
|
142
70
|
});
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
#include "../common/volume_metadata.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
+
#include "../common/fd_guard.h"
|
|
5
6
|
#include "../common/metadata_worker.h"
|
|
7
|
+
#include "../common/path_security.h"
|
|
8
|
+
#include "../common/volume_utils.h"
|
|
6
9
|
#include "blkid_cache.h"
|
|
7
10
|
#include <fcntl.h> // for open(), O_DIRECTORY, O_RDONLY, O_CLOEXEC
|
|
8
11
|
#include <memory>
|
|
9
12
|
#include <sys/statvfs.h>
|
|
10
|
-
#include <unistd.h>
|
|
13
|
+
#include <unistd.h>
|
|
11
14
|
|
|
12
15
|
#ifdef ENABLE_GIO
|
|
13
16
|
#include "gio_volume_metadata.h"
|
|
@@ -32,6 +35,18 @@ public:
|
|
|
32
35
|
DEBUG_LOG("[LinuxMetadataWorker] starting statvfs for %s",
|
|
33
36
|
mountPoint.c_str());
|
|
34
37
|
|
|
38
|
+
// Validate and canonicalize mount point using realpath()
|
|
39
|
+
// This prevents directory traversal attacks and resolves symlinks
|
|
40
|
+
std::string error;
|
|
41
|
+
std::string validated_mount_point =
|
|
42
|
+
ValidatePathForRead(mountPoint, error);
|
|
43
|
+
if (validated_mount_point.empty()) {
|
|
44
|
+
throw FSException(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
DEBUG_LOG("[LinuxMetadataWorker] Using validated mount point: %s",
|
|
48
|
+
validated_mount_point.c_str());
|
|
49
|
+
|
|
35
50
|
// SECURITY: Use file descriptor-based approach to prevent TOCTOU race
|
|
36
51
|
// condition
|
|
37
52
|
//
|
|
@@ -45,23 +60,18 @@ public:
|
|
|
45
60
|
// O_DIRECTORY: Ensures we're opening a directory, fails if not
|
|
46
61
|
// O_RDONLY: Read-only access (sufficient for fstatvfs)
|
|
47
62
|
// O_CLOEXEC: Close on exec (prevents fd leaks in multithreaded programs)
|
|
48
|
-
int fd = open(
|
|
63
|
+
int fd = open(validated_mount_point.c_str(),
|
|
64
|
+
O_DIRECTORY | O_RDONLY | O_CLOEXEC);
|
|
49
65
|
if (fd < 0) {
|
|
50
66
|
int error = errno;
|
|
51
67
|
DEBUG_LOG("[LinuxMetadataWorker] open failed for %s: %s (%d)",
|
|
52
|
-
|
|
53
|
-
throw FSException(
|
|
68
|
+
validated_mount_point.c_str(), strerror(error), error);
|
|
69
|
+
throw FSException(
|
|
70
|
+
CreatePathErrorMessage("open", validated_mount_point, error));
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
// RAII guard to ensure file descriptor is always closed
|
|
57
|
-
|
|
58
|
-
int fd;
|
|
59
|
-
~FdGuard() {
|
|
60
|
-
if (fd >= 0) {
|
|
61
|
-
close(fd);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} fd_guard{fd};
|
|
74
|
+
FdGuard fd_guard(fd);
|
|
65
75
|
|
|
66
76
|
// Use fstatvfs on the file descriptor instead of statvfs on the path
|
|
67
77
|
// The fd holds a reference to the filesystem, preventing TOCTOU issues
|
|
@@ -69,9 +79,9 @@ public:
|
|
|
69
79
|
if (fstatvfs(fd, &vfs) != 0) {
|
|
70
80
|
int error = errno;
|
|
71
81
|
DEBUG_LOG("[LinuxMetadataWorker] fstatvfs failed for %s: %s (%d)",
|
|
72
|
-
|
|
82
|
+
validated_mount_point.c_str(), strerror(error), error);
|
|
73
83
|
throw FSException(
|
|
74
|
-
CreatePathErrorMessage("fstatvfs",
|
|
84
|
+
CreatePathErrorMessage("fstatvfs", validated_mount_point, error));
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
// fd_guard will automatically close the file descriptor when this
|
|
@@ -83,16 +93,14 @@ public:
|
|
|
83
93
|
const uint64_t freeBlocks = static_cast<uint64_t>(vfs.f_bfree);
|
|
84
94
|
|
|
85
95
|
// Check for overflow before multiplication
|
|
86
|
-
if (blockSize
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw FSException("Free space calculation would overflow");
|
|
95
|
-
}
|
|
96
|
+
if (WouldOverflow(blockSize, totalBlocks)) {
|
|
97
|
+
throw FSException("Total volume size calculation would overflow");
|
|
98
|
+
}
|
|
99
|
+
if (WouldOverflow(blockSize, availBlocks)) {
|
|
100
|
+
throw FSException("Available space calculation would overflow");
|
|
101
|
+
}
|
|
102
|
+
if (WouldOverflow(blockSize, freeBlocks)) {
|
|
103
|
+
throw FSException("Free space calculation would overflow");
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
metadata.remote = false;
|
|
@@ -108,11 +116,11 @@ public:
|
|
|
108
116
|
#ifdef ENABLE_GIO
|
|
109
117
|
try {
|
|
110
118
|
DEBUG_LOG("[LinuxMetadataWorker] collecting GIO metadata for %s",
|
|
111
|
-
|
|
112
|
-
gio::addMountMetadata(
|
|
119
|
+
validated_mount_point.c_str());
|
|
120
|
+
gio::addMountMetadata(validated_mount_point, metadata);
|
|
113
121
|
} catch (const std::exception &e) {
|
|
114
122
|
DEBUG_LOG("[LinuxMetadataWorker] GIO error for %s: %s",
|
|
115
|
-
|
|
123
|
+
validated_mount_point.c_str(), e.what());
|
|
116
124
|
metadata.status = std::string("GIO warning: ") + e.what();
|
|
117
125
|
}
|
|
118
126
|
#endif
|
package/src/options.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
// src/options.ts
|
|
2
2
|
|
|
3
3
|
import { availableParallelism } from "node:os";
|
|
4
|
+
import { env } from "node:process";
|
|
4
5
|
import { compactValues, isObject } from "./object";
|
|
5
6
|
import { isWindows } from "./platform";
|
|
6
7
|
import type { Options } from "./types/options";
|
|
7
8
|
|
|
9
|
+
const DefaultTimeoutMs = 5_000;
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
12
|
+
* Get the default timeout in milliseconds for {@link Options.timeoutMs}.
|
|
13
|
+
*
|
|
14
|
+
* This can be overridden by setting the `FS_METADATA_TIMEOUT_MS` environment
|
|
15
|
+
* variable to a positive integer.
|
|
10
16
|
*
|
|
11
17
|
* Note that this timeout may be insufficient for some devices, like spun-down
|
|
12
18
|
* optical drives or network shares that need to spin up or reconnect.
|
|
19
|
+
*
|
|
20
|
+
* @returns The timeout from env var if valid, otherwise 5000ms
|
|
13
21
|
*/
|
|
14
|
-
export
|
|
22
|
+
export function getTimeoutMsDefault(): number {
|
|
23
|
+
const value = env["FS_METADATA_TIMEOUT_MS"];
|
|
24
|
+
if (value == null) return DefaultTimeoutMs;
|
|
25
|
+
const parsed = parseInt(value, 10);
|
|
26
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DefaultTimeoutMs;
|
|
27
|
+
}
|
|
15
28
|
|
|
16
29
|
/**
|
|
17
30
|
* System paths and globs that indicate system volumes
|
|
@@ -104,7 +117,7 @@ export const SkipNetworkVolumesDefault = false;
|
|
|
104
117
|
* @see {@link optionsWithDefaults} for creating an options object with default values
|
|
105
118
|
*/
|
|
106
119
|
export const OptionsDefault: Options = {
|
|
107
|
-
timeoutMs:
|
|
120
|
+
timeoutMs: getTimeoutMsDefault(),
|
|
108
121
|
maxConcurrency: availableParallelism(),
|
|
109
122
|
systemPathPatterns: [...SystemPathPatternsDefault],
|
|
110
123
|
systemFsTypes: [...SystemFsTypesDefault],
|
package/src/types/options.ts
CHANGED
|
@@ -4,13 +4,9 @@
|
|
|
4
4
|
#include "security_utils.h"
|
|
5
5
|
#include "thread_pool.h"
|
|
6
6
|
#include "windows_arch.h"
|
|
7
|
-
#include <atomic>
|
|
8
7
|
#include <chrono>
|
|
9
|
-
#include <condition_variable>
|
|
10
8
|
#include <future>
|
|
11
|
-
#include <mutex>
|
|
12
9
|
#include <string>
|
|
13
|
-
#include <thread>
|
|
14
10
|
#include <vector>
|
|
15
11
|
|
|
16
12
|
namespace FSMeta {
|
|
@@ -83,7 +79,10 @@ private:
|
|
|
83
79
|
searchPath += "*";
|
|
84
80
|
|
|
85
81
|
WIN32_FIND_DATAA findData;
|
|
86
|
-
|
|
82
|
+
// Use FindHandleGuard - search handles MUST be closed with FindClose,
|
|
83
|
+
// not CloseHandle. See:
|
|
84
|
+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
|
|
85
|
+
FindHandleGuard findHandle(FindFirstFileExA(
|
|
87
86
|
searchPath.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch,
|
|
88
87
|
nullptr,
|
|
89
88
|
FIND_FIRST_EX_LARGE_FETCH | FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY));
|
|
@@ -96,69 +95,77 @@ private:
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
// Successfully opened - drive is healthy
|
|
99
|
-
FindClose
|
|
98
|
+
// FindHandleGuard destructor will call FindClose automatically
|
|
100
99
|
DEBUG_LOG("[DriveStatusChecker] Drive %s is healthy", path.c_str());
|
|
101
100
|
return DriveStatus::Healthy;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
public:
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Submit a drive check to the thread pool and return a future.
|
|
105
|
+
// The caller is responsible for enforcing timeout via future.wait_for().
|
|
106
|
+
// This design avoids detached threads and race conditions.
|
|
107
|
+
static std::future<DriveStatus> CheckDriveAsync(const std::string &path) {
|
|
107
108
|
auto promise = std::make_shared<std::promise<DriveStatus>>();
|
|
108
109
|
auto future = promise->get_future();
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
GetGlobalThreadPool().Submit([path, promise, state, timeoutMs]() {
|
|
114
|
-
// Set up timeout
|
|
115
|
-
auto startTime = std::chrono::steady_clock::now();
|
|
116
|
-
|
|
117
|
-
// Perform the check
|
|
118
|
-
DriveStatus status = CheckDriveInternal(path);
|
|
119
|
-
|
|
120
|
-
// Check if we've timed out
|
|
121
|
-
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
|
122
|
-
if (elapsed > std::chrono::milliseconds(timeoutMs)) {
|
|
123
|
-
status = DriveStatus::Timeout;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Only set the promise if we haven't been cancelled
|
|
127
|
-
if (!state->load()) {
|
|
111
|
+
GetGlobalThreadPool().Submit([path, promise]() {
|
|
112
|
+
try {
|
|
113
|
+
DriveStatus status = CheckDriveInternal(path);
|
|
128
114
|
promise->set_value(status);
|
|
115
|
+
} catch (const std::exception &e) {
|
|
116
|
+
DEBUG_LOG("[DriveStatusChecker] Exception in CheckDriveInternal: %s",
|
|
117
|
+
e.what());
|
|
118
|
+
// Set exception instead of value so caller can handle it
|
|
119
|
+
try {
|
|
120
|
+
promise->set_exception(std::current_exception());
|
|
121
|
+
} catch (...) {
|
|
122
|
+
// Promise may have been abandoned if caller timed out
|
|
123
|
+
}
|
|
124
|
+
} catch (...) {
|
|
125
|
+
DEBUG_LOG(
|
|
126
|
+
"[DriveStatusChecker] Unknown exception in CheckDriveInternal");
|
|
127
|
+
try {
|
|
128
|
+
promise->set_exception(std::current_exception());
|
|
129
|
+
} catch (...) {
|
|
130
|
+
// Promise may have been abandoned if caller timed out
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
133
|
});
|
|
131
134
|
|
|
132
|
-
// Handle timeout in the caller
|
|
133
|
-
if (timeoutMs != INFINITE) {
|
|
134
|
-
std::thread([promise, state, timeoutMs]() {
|
|
135
|
-
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
|
|
136
|
-
if (!state->exchange(true)) {
|
|
137
|
-
try {
|
|
138
|
-
promise->set_value(DriveStatus::Timeout);
|
|
139
|
-
} catch (...) {
|
|
140
|
-
// Promise already satisfied
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}).detach();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
135
|
return future;
|
|
147
136
|
}
|
|
148
137
|
|
|
138
|
+
// Overload that accepts timeoutMs for API compatibility (timeout is enforced
|
|
139
|
+
// in CheckDrive, not here)
|
|
140
|
+
static std::future<DriveStatus> CheckDriveAsync(const std::string &path,
|
|
141
|
+
DWORD /*timeoutMs*/) {
|
|
142
|
+
return CheckDriveAsync(path);
|
|
143
|
+
}
|
|
144
|
+
|
|
149
145
|
static DriveStatus CheckDrive(const std::string &path,
|
|
150
146
|
DWORD timeoutMs = 5000) {
|
|
151
147
|
try {
|
|
152
|
-
auto future = CheckDriveAsync(path
|
|
148
|
+
auto future = CheckDriveAsync(path);
|
|
149
|
+
|
|
150
|
+
// Use wait_for to enforce timeout - no detached threads needed!
|
|
151
|
+
// The worker thread continues running but we return Timeout to caller.
|
|
152
|
+
// The promise will eventually be satisfied (or abandoned).
|
|
153
|
+
auto waitResult = future.wait_for(std::chrono::milliseconds(timeoutMs));
|
|
153
154
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
155
|
+
if (waitResult == std::future_status::timeout) {
|
|
156
|
+
DEBUG_LOG("[DriveStatusChecker] Timeout waiting for drive %s",
|
|
157
|
+
path.c_str());
|
|
156
158
|
return DriveStatus::Timeout;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
// Future is ready - get the result (may throw if worker set exception)
|
|
159
162
|
return future.get();
|
|
163
|
+
} catch (const std::exception &e) {
|
|
164
|
+
DEBUG_LOG("[DriveStatusChecker] Exception checking drive %s: %s",
|
|
165
|
+
path.c_str(), e.what());
|
|
166
|
+
return DriveStatus::Unknown;
|
|
160
167
|
} catch (...) {
|
|
161
|
-
DEBUG_LOG("[DriveStatusChecker]
|
|
168
|
+
DEBUG_LOG("[DriveStatusChecker] Unknown exception checking drive %s",
|
|
162
169
|
path.c_str());
|
|
163
170
|
return DriveStatus::Unknown;
|
|
164
171
|
}
|
|
@@ -172,24 +179,42 @@ public:
|
|
|
172
179
|
futures.reserve(paths.size());
|
|
173
180
|
|
|
174
181
|
// Launch all checks concurrently
|
|
182
|
+
auto startTime = std::chrono::steady_clock::now();
|
|
175
183
|
for (const auto &path : paths) {
|
|
176
|
-
futures.push_back(CheckDriveAsync(path
|
|
184
|
+
futures.push_back(CheckDriveAsync(path));
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
// Collect results
|
|
187
|
+
// Collect results with timeout
|
|
180
188
|
std::vector<DriveStatus> results;
|
|
181
189
|
results.reserve(paths.size());
|
|
182
190
|
|
|
183
191
|
for (size_t i = 0; i < futures.size(); ++i) {
|
|
184
192
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
// Calculate remaining time for this future
|
|
194
|
+
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
|
195
|
+
auto elapsedMs =
|
|
196
|
+
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed)
|
|
197
|
+
.count();
|
|
198
|
+
auto remainingMs = (elapsedMs < static_cast<long long>(timeoutMs))
|
|
199
|
+
? static_cast<DWORD>(timeoutMs - elapsedMs)
|
|
200
|
+
: 0;
|
|
201
|
+
|
|
202
|
+
if (remainingMs == 0 ||
|
|
203
|
+
futures[i].wait_for(std::chrono::milliseconds(remainingMs)) ==
|
|
204
|
+
std::future_status::timeout) {
|
|
205
|
+
DEBUG_LOG("[DriveStatusChecker] Timeout waiting for drive %s",
|
|
206
|
+
paths[i].c_str());
|
|
187
207
|
results.push_back(DriveStatus::Timeout);
|
|
188
208
|
} else {
|
|
189
209
|
results.push_back(futures[i].get());
|
|
190
210
|
}
|
|
211
|
+
} catch (const std::exception &e) {
|
|
212
|
+
DEBUG_LOG(
|
|
213
|
+
"[DriveStatusChecker] Exception getting result for drive %s: %s",
|
|
214
|
+
paths[i].c_str(), e.what());
|
|
215
|
+
results.push_back(DriveStatus::Unknown);
|
|
191
216
|
} catch (...) {
|
|
192
|
-
DEBUG_LOG("[DriveStatusChecker]
|
|
217
|
+
DEBUG_LOG("[DriveStatusChecker] Unknown exception for drive %s",
|
|
193
218
|
paths[i].c_str());
|
|
194
219
|
results.push_back(DriveStatus::Unknown);
|
|
195
220
|
}
|
|
@@ -62,8 +62,8 @@ private:
|
|
|
62
62
|
size_t size = FormatMessageA(
|
|
63
63
|
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
64
64
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
65
|
-
|
|
66
|
-
(LPSTR)&messageBuffer, 0,
|
|
65
|
+
nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
66
|
+
(LPSTR)&messageBuffer, 0, nullptr);
|
|
67
67
|
|
|
68
68
|
// RAII guard ensures LocalFree is called even if exception thrown
|
|
69
69
|
LocalFreeGuard guard(messageBuffer);
|
|
@@ -124,7 +124,7 @@ public:
|
|
|
124
124
|
|
|
125
125
|
// Check if process has required privileges
|
|
126
126
|
static bool HasRequiredPrivileges() {
|
|
127
|
-
HANDLE token;
|
|
127
|
+
HANDLE token = nullptr;
|
|
128
128
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
@@ -169,7 +169,8 @@ public:
|
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
// RAII wrapper for HANDLE resources
|
|
172
|
+
// RAII wrapper for HANDLE resources (uses CloseHandle)
|
|
173
|
+
// For search handles from FindFirstFile*, use FindHandleGuard instead
|
|
173
174
|
class HandleGuard {
|
|
174
175
|
HANDLE handle;
|
|
175
176
|
|
|
@@ -213,6 +214,48 @@ public:
|
|
|
213
214
|
}
|
|
214
215
|
};
|
|
215
216
|
|
|
217
|
+
// RAII wrapper for search handles from FindFirstFile/FindFirstFileEx
|
|
218
|
+
// These handles MUST be closed with FindClose, not CloseHandle.
|
|
219
|
+
// See:
|
|
220
|
+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
|
|
221
|
+
class FindHandleGuard {
|
|
222
|
+
HANDLE handle;
|
|
223
|
+
|
|
224
|
+
public:
|
|
225
|
+
explicit FindHandleGuard(HANDLE h) : handle(h) {}
|
|
226
|
+
|
|
227
|
+
~FindHandleGuard() {
|
|
228
|
+
if (handle != INVALID_HANDLE_VALUE) {
|
|
229
|
+
FindClose(handle);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
FindHandleGuard(FindHandleGuard &&other) noexcept : handle(other.handle) {
|
|
234
|
+
other.handle = INVALID_HANDLE_VALUE;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
FindHandleGuard &operator=(FindHandleGuard &&other) noexcept {
|
|
238
|
+
if (this != &other) {
|
|
239
|
+
if (handle != INVALID_HANDLE_VALUE) {
|
|
240
|
+
FindClose(handle);
|
|
241
|
+
}
|
|
242
|
+
handle = other.handle;
|
|
243
|
+
other.handle = INVALID_HANDLE_VALUE;
|
|
244
|
+
}
|
|
245
|
+
return *this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Delete copy operations
|
|
249
|
+
FindHandleGuard(const FindHandleGuard &) = delete;
|
|
250
|
+
FindHandleGuard &operator=(const FindHandleGuard &) = delete;
|
|
251
|
+
|
|
252
|
+
HANDLE get() const { return handle; }
|
|
253
|
+
|
|
254
|
+
// Check if handle is valid (FindFirstFile returns INVALID_HANDLE_VALUE on
|
|
255
|
+
// failure, not NULL)
|
|
256
|
+
explicit operator bool() const { return handle != INVALID_HANDLE_VALUE; }
|
|
257
|
+
};
|
|
258
|
+
|
|
216
259
|
// RAII wrapper for critical sections
|
|
217
260
|
class CriticalSectionGuard {
|
|
218
261
|
CRITICAL_SECTION cs;
|
|
@@ -244,6 +287,8 @@ public:
|
|
|
244
287
|
// Delete copy/move
|
|
245
288
|
Lock(const Lock &) = delete;
|
|
246
289
|
Lock &operator=(const Lock &) = delete;
|
|
290
|
+
Lock(Lock &&) = delete;
|
|
291
|
+
Lock &operator=(Lock &&) = delete;
|
|
247
292
|
};
|
|
248
293
|
};
|
|
249
294
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
#include <functional>
|
|
7
7
|
#include <memory>
|
|
8
8
|
#include <queue>
|
|
9
|
+
#include <stdexcept>
|
|
10
|
+
#include <string>
|
|
9
11
|
#include <thread>
|
|
10
12
|
#include <vector>
|
|
11
13
|
|
|
@@ -16,22 +18,45 @@ class WorkQueue {
|
|
|
16
18
|
private:
|
|
17
19
|
std::queue<std::function<void()>> tasks;
|
|
18
20
|
CRITICAL_SECTION cs;
|
|
19
|
-
HANDLE workAvailable;
|
|
21
|
+
HANDLE workAvailable = nullptr;
|
|
20
22
|
std::atomic<bool> shutdown{false};
|
|
23
|
+
bool initialized = false;
|
|
21
24
|
|
|
22
25
|
public:
|
|
23
26
|
WorkQueue() {
|
|
24
27
|
InitializeCriticalSection(&cs);
|
|
28
|
+
// CreateEvent returns NULL on failure, not INVALID_HANDLE_VALUE.
|
|
29
|
+
// See:
|
|
30
|
+
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa
|
|
25
31
|
workAvailable = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
|
32
|
+
if (workAvailable == nullptr) {
|
|
33
|
+
DWORD error = GetLastError();
|
|
34
|
+
DeleteCriticalSection(&cs);
|
|
35
|
+
DEBUG_LOG("[WorkQueue] CreateEvent failed with error: %lu", error);
|
|
36
|
+
throw std::runtime_error("WorkQueue: CreateEvent failed with error " +
|
|
37
|
+
std::to_string(error));
|
|
38
|
+
}
|
|
39
|
+
initialized = true;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
~WorkQueue() {
|
|
29
43
|
shutdown = true;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
if (workAvailable != nullptr) {
|
|
45
|
+
SetEvent(workAvailable);
|
|
46
|
+
CloseHandle(workAvailable);
|
|
47
|
+
workAvailable = nullptr;
|
|
48
|
+
}
|
|
49
|
+
if (initialized) {
|
|
50
|
+
DeleteCriticalSection(&cs);
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
// Delete copy/move operations - WorkQueue manages non-copyable resources
|
|
55
|
+
WorkQueue(const WorkQueue &) = delete;
|
|
56
|
+
WorkQueue &operator=(const WorkQueue &) = delete;
|
|
57
|
+
WorkQueue(WorkQueue &&) = delete;
|
|
58
|
+
WorkQueue &operator=(WorkQueue &&) = delete;
|
|
59
|
+
|
|
35
60
|
void Push(std::function<void()> task) {
|
|
36
61
|
EnterCriticalSection(&cs);
|
|
37
62
|
tasks.push(std::move(task));
|
|
@@ -19,12 +19,12 @@ namespace {
|
|
|
19
19
|
class WNetConnection {
|
|
20
20
|
std::string drivePath;
|
|
21
21
|
std::unique_ptr<char[]> buffer;
|
|
22
|
-
DWORD bufferSize;
|
|
22
|
+
DWORD bufferSize = MAX_PATH;
|
|
23
23
|
bool isValid = false;
|
|
24
24
|
|
|
25
25
|
public:
|
|
26
26
|
explicit WNetConnection(const std::string &path)
|
|
27
|
-
: drivePath(path.substr(0, 2))
|
|
27
|
+
: drivePath(path.substr(0, 2)) {
|
|
28
28
|
|
|
29
29
|
// Allocate initial buffer
|
|
30
30
|
buffer = std::make_unique<char[]>(bufferSize);
|
|
@@ -41,6 +41,7 @@ public:
|
|
|
41
41
|
isValid = (result == NO_ERROR);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
~WNetConnection() = default;
|
|
44
45
|
WNetConnection(WNetConnection &&) noexcept = default;
|
|
45
46
|
WNetConnection &operator=(WNetConnection &&) noexcept = default;
|
|
46
47
|
|
|
@@ -85,12 +86,14 @@ inline std::string GetVolumeGUID(const std::string &mountPoint) {
|
|
|
85
86
|
// RAII wrapper for volume information
|
|
86
87
|
class VolumeInfo {
|
|
87
88
|
static constexpr DWORD VOLUME_NAME_SIZE = MAX_PATH + 1; // 261 characters
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
DWORD
|
|
93
|
-
|
|
89
|
+
// Initialize all members to prevent reading uninitialized memory
|
|
90
|
+
// if GetVolumeInformationA fails with ERROR_NOT_READY
|
|
91
|
+
char volumeName[VOLUME_NAME_SIZE] = {0};
|
|
92
|
+
char fstype[VOLUME_NAME_SIZE] = {0};
|
|
93
|
+
DWORD serialNumber = 0;
|
|
94
|
+
DWORD maxComponentLen = 0;
|
|
95
|
+
DWORD fsFlags = 0;
|
|
96
|
+
bool valid = false;
|
|
94
97
|
|
|
95
98
|
public:
|
|
96
99
|
explicit VolumeInfo(const std::string &mountPoint) {
|
|
@@ -111,10 +114,12 @@ public:
|
|
|
111
114
|
|
|
112
115
|
// RAII wrapper for disk space information
|
|
113
116
|
class DiskSpaceInfo {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
ULARGE_INTEGER
|
|
117
|
-
|
|
117
|
+
// Initialize all members to prevent reading uninitialized memory
|
|
118
|
+
// if GetDiskFreeSpaceExA fails with ERROR_NOT_READY
|
|
119
|
+
ULARGE_INTEGER totalBytes = {0};
|
|
120
|
+
ULARGE_INTEGER freeBytes = {0};
|
|
121
|
+
ULARGE_INTEGER totalFreeBytes = {0};
|
|
122
|
+
bool valid = false;
|
|
118
123
|
|
|
119
124
|
public:
|
|
120
125
|
explicit DiskSpaceInfo(const std::string &mountPoint) {
|