@photostructure/fs-metadata 0.5.0 → 0.5.1
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/C++_REVIEW_TODO.md +1 -1
- package/CHANGELOG.md +11 -2
- package/CLAUDE.md +269 -92
- package/CONTRIBUTING.md +41 -0
- package/README.md +20 -1
- package/WINDOWS_DEBUG_GUIDE.md +89 -0
- package/binding.gyp +3 -2
- package/dist/index.cjs +1440 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +360 -0
- package/dist/index.d.mts +360 -0
- package/dist/index.d.ts +360 -0
- package/dist/index.mjs +1398 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.cjs +1 -0
- package/package.json +25 -27
- package/prebuilds/darwin-arm64/@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-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/scripts/check-memory.mjs +53 -173
- package/scripts/clang-tidy.ts +241 -0
- package/scripts/prebuild-linux-glibc.sh +108 -0
- package/scripts/precommit.ts +45 -0
- package/scripts/sanitizers-test.sh +157 -0
- package/scripts/{configure.mjs → setup-native.mjs} +4 -1
- package/scripts/valgrind-test.mjs +1 -1
- package/scripts/{valgrind.sh → valgrind-test.sh} +15 -0
- package/src/binding.cpp +1 -1
- package/src/common/error_utils.h +17 -0
- package/src/common/metadata_worker.h +4 -1
- package/src/darwin/hidden.cpp +13 -6
- package/src/darwin/volume_metadata.cpp +2 -2
- package/src/darwin/volume_mount_points.cpp +8 -1
- package/src/linux/blkid_cache.cpp +8 -2
- package/src/linux/volume_metadata.cpp +1 -1
- package/src/platform.ts +25 -0
- package/src/test-utils/benchmark-harness.ts +192 -0
- package/src/test-utils/debuglog-child.ts +30 -2
- package/src/test-utils/debuglog-enabled-child.ts +38 -8
- package/src/test-utils/jest-setup.ts +14 -0
- package/src/test-utils/worker-thread-helper.cjs +92 -0
- package/src/windows/hidden.cpp +20 -11
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -131
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/src/array.ts.html +0 -217
- package/coverage/lcov-report/src/async.ts.html +0 -547
- package/coverage/lcov-report/src/debuglog.ts.html +0 -187
- package/coverage/lcov-report/src/defer.ts.html +0 -175
- package/coverage/lcov-report/src/dirname.ts.html +0 -124
- package/coverage/lcov-report/src/error.ts.html +0 -322
- package/coverage/lcov-report/src/fs.ts.html +0 -316
- package/coverage/lcov-report/src/glob.ts.html +0 -472
- package/coverage/lcov-report/src/hidden.ts.html +0 -724
- package/coverage/lcov-report/src/index.html +0 -521
- package/coverage/lcov-report/src/index.ts.html +0 -676
- package/coverage/lcov-report/src/linux/dev_disk.ts.html +0 -316
- package/coverage/lcov-report/src/linux/index.html +0 -146
- package/coverage/lcov-report/src/linux/mount_points.ts.html +0 -364
- package/coverage/lcov-report/src/linux/mtab.ts.html +0 -493
- package/coverage/lcov-report/src/mount_point.ts.html +0 -106
- package/coverage/lcov-report/src/number.ts.html +0 -148
- package/coverage/lcov-report/src/object.ts.html +0 -265
- package/coverage/lcov-report/src/options.ts.html +0 -475
- package/coverage/lcov-report/src/path.ts.html +0 -268
- package/coverage/lcov-report/src/platform.ts.html +0 -112
- package/coverage/lcov-report/src/random.ts.html +0 -205
- package/coverage/lcov-report/src/remote_info.ts.html +0 -553
- package/coverage/lcov-report/src/stack_path.ts.html +0 -298
- package/coverage/lcov-report/src/string.ts.html +0 -382
- package/coverage/lcov-report/src/string_enum.ts.html +0 -208
- package/coverage/lcov-report/src/system_volume.ts.html +0 -301
- package/coverage/lcov-report/src/unc.ts.html +0 -274
- package/coverage/lcov-report/src/units.ts.html +0 -274
- package/coverage/lcov-report/src/uuid.ts.html +0 -157
- package/coverage/lcov-report/src/volume_health_status.ts.html +0 -259
- package/coverage/lcov-report/src/volume_metadata.ts.html +0 -787
- package/coverage/lcov-report/src/volume_mount_points.ts.html +0 -388
- package/coverage/lcov.info +0 -3581
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/src/array.ts.html +0 -217
- package/coverage/src/async.ts.html +0 -547
- package/coverage/src/debuglog.ts.html +0 -187
- package/coverage/src/defer.ts.html +0 -175
- package/coverage/src/dirname.ts.html +0 -124
- package/coverage/src/error.ts.html +0 -322
- package/coverage/src/fs.ts.html +0 -316
- package/coverage/src/glob.ts.html +0 -472
- package/coverage/src/hidden.ts.html +0 -724
- package/coverage/src/index.html +0 -521
- package/coverage/src/index.ts.html +0 -676
- package/coverage/src/linux/dev_disk.ts.html +0 -316
- package/coverage/src/linux/index.html +0 -146
- package/coverage/src/linux/mount_points.ts.html +0 -364
- package/coverage/src/linux/mtab.ts.html +0 -493
- package/coverage/src/mount_point.ts.html +0 -106
- package/coverage/src/number.ts.html +0 -148
- package/coverage/src/object.ts.html +0 -265
- package/coverage/src/options.ts.html +0 -475
- package/coverage/src/path.ts.html +0 -268
- package/coverage/src/platform.ts.html +0 -112
- package/coverage/src/random.ts.html +0 -205
- package/coverage/src/remote_info.ts.html +0 -553
- package/coverage/src/stack_path.ts.html +0 -298
- package/coverage/src/string.ts.html +0 -382
- package/coverage/src/string_enum.ts.html +0 -208
- package/coverage/src/system_volume.ts.html +0 -301
- package/coverage/src/unc.ts.html +0 -274
- package/coverage/src/units.ts.html +0 -274
- package/coverage/src/uuid.ts.html +0 -157
- package/coverage/src/volume_health_status.ts.html +0 -259
- package/coverage/src/volume_metadata.ts.html +0 -787
- package/coverage/src/volume_mount_points.ts.html +0 -388
- package/scripts/clang-tidy.mjs +0 -73
- package/scripts/run-asan.sh +0 -92
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AddressSanitizer and LeakSanitizer test runner for @photostructure/fs-metadata
|
|
3
|
+
# Runs comprehensive memory safety checks on native code
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Check if we're on Linux
|
|
8
|
+
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
|
|
9
|
+
echo "AddressSanitizer tests are only supported on Linux"
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Check for clang
|
|
14
|
+
if ! command -v clang &> /dev/null; then
|
|
15
|
+
echo "Error: clang is required for AddressSanitizer tests"
|
|
16
|
+
echo "Install with: sudo apt-get install clang"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Colors for output
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
YELLOW='\033[1;33m'
|
|
24
|
+
BLUE='\033[0;34m'
|
|
25
|
+
NC='\033[0m' # No Color
|
|
26
|
+
|
|
27
|
+
# Configuration
|
|
28
|
+
CLEAN_BUILD=${CLEAN_BUILD:-1}
|
|
29
|
+
VERBOSE=${VERBOSE:-0}
|
|
30
|
+
OUTPUT_FILE="asan-output.log"
|
|
31
|
+
|
|
32
|
+
echo -e "${YELLOW}Running AddressSanitizer and LeakSanitizer tests...${NC}"
|
|
33
|
+
|
|
34
|
+
# Clean previous builds if requested
|
|
35
|
+
if [[ "$CLEAN_BUILD" == "1" ]]; then
|
|
36
|
+
echo "Cleaning previous builds..."
|
|
37
|
+
rm -rf build/
|
|
38
|
+
fi
|
|
39
|
+
rm -f "$OUTPUT_FILE"
|
|
40
|
+
|
|
41
|
+
# Set up build environment
|
|
42
|
+
export CC=clang
|
|
43
|
+
export CXX=clang++
|
|
44
|
+
export CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
|
|
45
|
+
export CXXFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
|
|
46
|
+
export LDFLAGS="-fsanitize=address"
|
|
47
|
+
|
|
48
|
+
# Comprehensive ASAN options combining both implementations
|
|
49
|
+
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1:check_initialization_order=1:strict_init_order=1:print_module_map=1"
|
|
50
|
+
export LSAN_OPTIONS="suppressions=$(pwd)/.lsan-suppressions.txt:print_suppressions=0"
|
|
51
|
+
|
|
52
|
+
# Increase Node.js heap size for ASan overhead
|
|
53
|
+
export NODE_OPTIONS="--max-old-space-size=8192"
|
|
54
|
+
|
|
55
|
+
# Find and set ASan runtime library
|
|
56
|
+
echo "Detecting ASan runtime library..."
|
|
57
|
+
ASAN_LIB=$(clang -print-file-name=libclang_rt.asan-x86_64.so 2>/dev/null || echo "")
|
|
58
|
+
|
|
59
|
+
if [[ -n "$ASAN_LIB" && "$ASAN_LIB" != *"not found"* && -f "$ASAN_LIB" ]]; then
|
|
60
|
+
export LD_PRELOAD="$ASAN_LIB"
|
|
61
|
+
echo -e "${BLUE}Using ASan library: $ASAN_LIB${NC}"
|
|
62
|
+
else
|
|
63
|
+
# Try common paths as fallback
|
|
64
|
+
for lib in /usr/lib/x86_64-linux-gnu/libasan.so.{8,6} /usr/lib64/libasan.so.{8,6}; do
|
|
65
|
+
if [[ -f "$lib" ]]; then
|
|
66
|
+
export LD_PRELOAD="$lib"
|
|
67
|
+
echo -e "${BLUE}Using ASan library: $lib${NC}"
|
|
68
|
+
break
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [[ -z "${LD_PRELOAD:-}" ]]; then
|
|
74
|
+
echo -e "${YELLOW}Warning: Could not find ASan runtime library${NC}"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Build the native module
|
|
78
|
+
echo "Building with AddressSanitizer..."
|
|
79
|
+
npm run setup:native
|
|
80
|
+
npm run clean:native
|
|
81
|
+
node-gyp configure build
|
|
82
|
+
|
|
83
|
+
# Run tests and capture output
|
|
84
|
+
echo -e "${YELLOW}Running tests with AddressSanitizer...${NC}"
|
|
85
|
+
set +e # Don't exit on test failure
|
|
86
|
+
npm test -- --no-coverage 2>&1 | tee "$OUTPUT_FILE"
|
|
87
|
+
TEST_EXIT_CODE=${PIPESTATUS[0]}
|
|
88
|
+
set -e
|
|
89
|
+
|
|
90
|
+
echo -e "${BLUE}\nFull ASAN output saved to: $OUTPUT_FILE${NC}"
|
|
91
|
+
|
|
92
|
+
# Analyze output for errors specific to our code
|
|
93
|
+
echo -e "\n${YELLOW}Analyzing ASAN output...${NC}"
|
|
94
|
+
|
|
95
|
+
# Count different types of issues
|
|
96
|
+
OUR_ERRORS=0
|
|
97
|
+
OUR_LEAKS=0
|
|
98
|
+
INTERNAL_LEAKS=0
|
|
99
|
+
|
|
100
|
+
# Check for ASAN errors in our code (not V8/Node internals)
|
|
101
|
+
if grep -E "(ERROR: AddressSanitizer|ERROR: LeakSanitizer)" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/)" > /dev/null; then
|
|
102
|
+
OUR_ERRORS=1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Check for direct/indirect leaks in our code with context
|
|
106
|
+
while IFS= read -r line_num; do
|
|
107
|
+
# Get 5 lines before and 10 lines after for context
|
|
108
|
+
start=$((line_num - 5))
|
|
109
|
+
end=$((line_num + 10))
|
|
110
|
+
if sed -n "${start},${end}p" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/|photostructure)" > /dev/null; then
|
|
111
|
+
OUR_LEAKS=1
|
|
112
|
+
break
|
|
113
|
+
fi
|
|
114
|
+
done < <(grep -n "Direct leak\|Indirect leak" "$OUTPUT_FILE" | cut -d: -f1)
|
|
115
|
+
|
|
116
|
+
# Count V8/Node internal leaks for information
|
|
117
|
+
INTERNAL_LEAKS=$(grep -c "leak.*\/usr\/bin\/node" "$OUTPUT_FILE" 2>/dev/null || echo "0")
|
|
118
|
+
INTERNAL_LEAKS="${INTERNAL_LEAKS//[[:space:]]/}" # Remove any whitespace
|
|
119
|
+
|
|
120
|
+
# Report results
|
|
121
|
+
EXIT_CODE=0
|
|
122
|
+
|
|
123
|
+
if [[ "$OUR_ERRORS" -eq 1 ]]; then
|
|
124
|
+
echo -e "${RED}\n✗ AddressSanitizer found errors in fs-metadata code:${NC}"
|
|
125
|
+
grep -E "(ERROR: AddressSanitizer|ERROR: LeakSanitizer)" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/)" | head -20
|
|
126
|
+
EXIT_CODE=1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if [[ "$OUR_LEAKS" -eq 1 ]]; then
|
|
130
|
+
echo -e "${RED}\n✗ LeakSanitizer found memory leaks in fs-metadata code:${NC}"
|
|
131
|
+
# Show leak summary
|
|
132
|
+
grep -A 5 "SUMMARY: AddressSanitizer" "$OUTPUT_FILE" || true
|
|
133
|
+
EXIT_CODE=1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if [[ "$EXIT_CODE" -eq 0 ]]; then
|
|
137
|
+
echo -e "${GREEN}\n✓ AddressSanitizer and LeakSanitizer tests passed (no issues in fs-metadata code)${NC}"
|
|
138
|
+
if [[ "$INTERNAL_LEAKS" -gt 0 ]]; then
|
|
139
|
+
echo -e "${YELLOW} Note: $INTERNAL_LEAKS V8/Node.js internal leaks detected (suppressed)${NC}"
|
|
140
|
+
fi
|
|
141
|
+
else
|
|
142
|
+
echo -e "${RED}\n✗ Memory safety issues detected!${NC}"
|
|
143
|
+
echo -e "${YELLOW}See $OUTPUT_FILE for full details${NC}"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Show ASAN statistics if verbose
|
|
147
|
+
if [[ "$VERBOSE" -eq 1 ]] && grep -q "Stats:" "$OUTPUT_FILE"; then
|
|
148
|
+
echo -e "\n${BLUE}ASAN Statistics:${NC}"
|
|
149
|
+
grep -A 20 "Stats:" "$OUTPUT_FILE" | head -20
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Clean build artifacts to ensure no ASAN-compiled code remains
|
|
153
|
+
echo -e "\n${YELLOW}Cleaning build artifacts...${NC}"
|
|
154
|
+
npm run clean:native > /dev/null 2>&1
|
|
155
|
+
echo -e "${GREEN}✓ Build artifacts cleaned${NC}"
|
|
156
|
+
|
|
157
|
+
exit $EXIT_CODE
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// scripts/
|
|
3
|
+
// scripts/setup-native.mjs
|
|
4
|
+
|
|
5
|
+
// This script sets up the config.gypi file to include GIO support when available.
|
|
6
|
+
// It should be run before building native modules with node-gyp.
|
|
4
7
|
|
|
5
8
|
import { execSync } from "node:child_process";
|
|
6
9
|
import { writeFileSync } from "node:fs";
|
|
@@ -35,6 +35,21 @@ if [ ! -f "$VALGRIND_TEST" ]; then
|
|
|
35
35
|
exit 1
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
+
# Check if dist directory exists (indicates build completed)
|
|
39
|
+
if [ ! -d "$ROOT_DIR/dist" ]; then
|
|
40
|
+
echo -e "${RED}Error: dist/ directory not found. Run 'npm run build:dist' first.${NC}"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Pre-flight check: run the test script without valgrind first
|
|
45
|
+
echo "Running pre-flight check..."
|
|
46
|
+
if ! node "$VALGRIND_TEST" > /dev/null 2>&1; then
|
|
47
|
+
echo -e "${RED}Error: Test script failed to run. Running again to show error:${NC}"
|
|
48
|
+
node "$VALGRIND_TEST"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
echo -e "${GREEN}✓ Pre-flight check passed${NC}"
|
|
52
|
+
|
|
38
53
|
# Use the committed suppressions file
|
|
39
54
|
SUPP_FILE="$ROOT_DIR/.valgrind.supp"
|
|
40
55
|
|
package/src/binding.cpp
CHANGED
package/src/common/error_utils.h
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/common/error_utils.h
|
|
2
2
|
#pragma once
|
|
3
|
+
#include <cstring>
|
|
3
4
|
#include <stdexcept>
|
|
4
5
|
#include <string>
|
|
5
6
|
|
|
@@ -11,9 +12,25 @@ public:
|
|
|
11
12
|
: std::runtime_error(message) {}
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
// Simple error code only version
|
|
14
16
|
inline std::string CreateErrorMessage(const char *operation, int error) {
|
|
15
17
|
return std::string(operation) +
|
|
16
18
|
" failed with error: " + std::to_string(error);
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
// Convenience function for common pattern: "operation failed for 'path': error"
|
|
22
|
+
inline std::string CreatePathErrorMessage(const char *operation,
|
|
23
|
+
const std::string &path, int error) {
|
|
24
|
+
return std::string(operation) + " failed for '" + path +
|
|
25
|
+
"': " + std::string(strerror(error)) + " (" + std::to_string(error) +
|
|
26
|
+
")";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// For operations without a path context
|
|
30
|
+
inline std::string CreateDetailedErrorMessage(const char *operation,
|
|
31
|
+
int error) {
|
|
32
|
+
return std::string(operation) + " failed: " + std::string(strerror(error)) +
|
|
33
|
+
" (" + std::to_string(error) + ")";
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
} // namespace FSMeta
|
|
@@ -20,7 +20,10 @@ protected:
|
|
|
20
20
|
deferred_.Reject(error.Value());
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
void OnOK() override {
|
|
23
|
+
void OnOK() override {
|
|
24
|
+
Napi::HandleScope scope(Env());
|
|
25
|
+
deferred_.Resolve(metadata.ToObject(Env()));
|
|
26
|
+
}
|
|
24
27
|
}; // class MetadataWorkerBase
|
|
25
28
|
|
|
26
29
|
} // namespace FSMeta
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/darwin/hidden.cpp
|
|
2
2
|
#include "hidden.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
|
+
#include "../common/error_utils.h"
|
|
4
5
|
#include <sys/stat.h>
|
|
5
6
|
#include <unistd.h>
|
|
6
7
|
|
|
@@ -27,11 +28,11 @@ void GetHiddenWorker::Execute() {
|
|
|
27
28
|
int error = errno;
|
|
28
29
|
if (error == ENOENT) {
|
|
29
30
|
DEBUG_LOG("[GetHiddenWorker] path not found: %s", path_.c_str());
|
|
30
|
-
SetError("Path not found");
|
|
31
|
+
SetError("Path not found: '" + path_ + "'");
|
|
31
32
|
} else {
|
|
32
33
|
DEBUG_LOG("[GetHiddenWorker] failed to stat path %s: %s (%d)",
|
|
33
34
|
path_.c_str(), strerror(error), error);
|
|
34
|
-
SetError(
|
|
35
|
+
SetError(CreatePathErrorMessage("stat", path_, error));
|
|
35
36
|
}
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
@@ -41,11 +42,13 @@ void GetHiddenWorker::Execute() {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
void GetHiddenWorker::OnOK() {
|
|
45
|
+
Napi::HandleScope scope(Env());
|
|
44
46
|
auto env = Env();
|
|
45
47
|
deferred_.Resolve(Napi::Boolean::New(env, is_hidden_));
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
void GetHiddenWorker::OnError(const Napi::Error &error) {
|
|
51
|
+
Napi::HandleScope scope(Env());
|
|
49
52
|
deferred_.Reject(error.Value());
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -92,11 +95,11 @@ void SetHiddenWorker::Execute() {
|
|
|
92
95
|
int error = errno;
|
|
93
96
|
if (error == ENOENT) {
|
|
94
97
|
DEBUG_LOG("[SetHiddenWorker] path not found: %s", path_.c_str());
|
|
95
|
-
SetError("Path not found");
|
|
98
|
+
SetError("Path not found: '" + path_ + "'");
|
|
96
99
|
} else {
|
|
97
100
|
DEBUG_LOG("[SetHiddenWorker] failed to stat path %s: %s (%d)",
|
|
98
101
|
path_.c_str(), strerror(error), error);
|
|
99
|
-
SetError(
|
|
102
|
+
SetError(CreatePathErrorMessage("stat", path_, error));
|
|
100
103
|
}
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
@@ -109,8 +112,10 @@ void SetHiddenWorker::Execute() {
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
if (chflags(path_.c_str(), new_flags) != 0) {
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
int error = errno;
|
|
116
|
+
DEBUG_LOG("[SetHiddenWorker] failed to set flags for %s: %s (%d)",
|
|
117
|
+
path_.c_str(), strerror(error), error);
|
|
118
|
+
SetError(CreatePathErrorMessage("chflags", path_, error));
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
DEBUG_LOG("[SetHiddenWorker] successfully set hidden=%d for: %s", hidden_,
|
|
@@ -118,11 +123,13 @@ void SetHiddenWorker::Execute() {
|
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
void SetHiddenWorker::OnOK() {
|
|
126
|
+
Napi::HandleScope scope(Env());
|
|
121
127
|
auto env = Env();
|
|
122
128
|
deferred_.Resolve(env.Undefined());
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
void SetHiddenWorker::OnError(const Napi::Error &error) {
|
|
132
|
+
Napi::HandleScope scope(Env());
|
|
126
133
|
deferred_.Reject(error.Value());
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -70,14 +70,14 @@ private:
|
|
|
70
70
|
if (statvfs(mountPoint.c_str(), &vfs) != 0) {
|
|
71
71
|
DEBUG_LOG("[GetVolumeMetadataWorker] statvfs failed: %s (%d)",
|
|
72
72
|
strerror(errno), errno);
|
|
73
|
-
SetError(
|
|
73
|
+
SetError(CreatePathErrorMessage("statvfs", mountPoint, errno));
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
if (statfs(mountPoint.c_str(), &fs) != 0) {
|
|
78
78
|
DEBUG_LOG("[GetVolumeMetadataWorker] statfs failed: %s (%d)",
|
|
79
79
|
strerror(errno), errno);
|
|
80
|
-
SetError(
|
|
80
|
+
SetError(CreatePathErrorMessage("statfs", mountPoint, errno));
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/darwin/volume_mount_points.cpp
|
|
2
2
|
#include "../common/volume_mount_points.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
|
+
#include "../common/error_utils.h"
|
|
4
5
|
#include "./fs_meta.h"
|
|
5
6
|
#include "./raii_utils.h"
|
|
6
7
|
#include <chrono>
|
|
@@ -33,7 +34,13 @@ public:
|
|
|
33
34
|
int count = getmntinfo_r_np(mntbuf.ptr(), MNT_NOWAIT);
|
|
34
35
|
|
|
35
36
|
if (count <= 0) {
|
|
36
|
-
|
|
37
|
+
if (count == 0) {
|
|
38
|
+
throw std::runtime_error("No mount points found");
|
|
39
|
+
} else {
|
|
40
|
+
// getmntinfo_r_np returns -1 on error and sets errno
|
|
41
|
+
throw FSException(
|
|
42
|
+
CreateDetailedErrorMessage("getmntinfo_r_np", errno));
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
for (int i = 0; i < count; i++) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include "blkid_cache.h"
|
|
4
4
|
#include "../common/debug_log.h"
|
|
5
|
+
#include "../common/error_utils.h"
|
|
5
6
|
#include <stdexcept>
|
|
6
7
|
|
|
7
8
|
namespace FSMeta {
|
|
@@ -14,8 +15,13 @@ BlkidCache::BlkidCache() : cache_(nullptr) {
|
|
|
14
15
|
const std::lock_guard<std::mutex> lock(mutex_);
|
|
15
16
|
DEBUG_LOG("[BlkidCache] initializing cache");
|
|
16
17
|
if (blkid_get_cache(&cache_, nullptr) != 0) {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
int error = errno;
|
|
19
|
+
DEBUG_LOG("[BlkidCache] failed to initialize cache: errno=%d", error);
|
|
20
|
+
if (error != 0) {
|
|
21
|
+
throw FSException(CreateDetailedErrorMessage("blkid_get_cache", error));
|
|
22
|
+
} else {
|
|
23
|
+
throw FSException("Failed to initialize blkid cache (no errno set)");
|
|
24
|
+
}
|
|
19
25
|
}
|
|
20
26
|
DEBUG_LOG("[BlkidCache] cache initialized successfully");
|
|
21
27
|
}
|
|
@@ -31,7 +31,7 @@ public:
|
|
|
31
31
|
mountPoint.c_str());
|
|
32
32
|
struct statvfs vfs;
|
|
33
33
|
if (statvfs(mountPoint.c_str(), &vfs) != 0) {
|
|
34
|
-
throw FSException(
|
|
34
|
+
throw FSException(CreatePathErrorMessage("statvfs", mountPoint, errno));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const uint64_t blockSize = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
package/src/platform.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/platform.ts
|
|
2
2
|
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { arch, platform } from "node:process";
|
|
4
5
|
|
|
5
6
|
export const isLinux = platform === "linux";
|
|
@@ -7,3 +8,27 @@ export const isWindows = platform === "win32";
|
|
|
7
8
|
export const isMacOS = platform === "darwin";
|
|
8
9
|
|
|
9
10
|
export const isArm = isLinux && arch.startsWith("arm");
|
|
11
|
+
export const isARM64 = arch === "arm64";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detects if we're running on Alpine Linux by checking /etc/os-release
|
|
15
|
+
*/
|
|
16
|
+
export function isAlpine(): boolean {
|
|
17
|
+
if (!isLinux) return false;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const osRelease = readFileSync("/etc/os-release", "utf8");
|
|
21
|
+
return (
|
|
22
|
+
osRelease.includes("Alpine Linux") || osRelease.includes("ID=alpine")
|
|
23
|
+
);
|
|
24
|
+
} catch {
|
|
25
|
+
return existsSync("/etc/alpine-release");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detects if we're likely running under emulation (as of 202506 there aren't free GHA ARM64 runners)
|
|
31
|
+
*/
|
|
32
|
+
export function isEmulated(): boolean {
|
|
33
|
+
return isLinux && isARM64;
|
|
34
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getTimingMultiplier } from "./test-timeout-config";
|
|
2
|
+
|
|
3
|
+
export interface BenchmarkOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Target duration for the benchmark in milliseconds (default: 20000ms / 20 seconds)
|
|
6
|
+
*/
|
|
7
|
+
targetDurationMs?: number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maximum timeout for the entire benchmark in milliseconds (default: 60000ms / 1 minute)
|
|
11
|
+
*/
|
|
12
|
+
maxTimeoutMs?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimum iterations to run regardless of timing (default: 5)
|
|
16
|
+
*/
|
|
17
|
+
minIterations?: number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maximum iterations to run regardless of timing (default: 10000)
|
|
21
|
+
*/
|
|
22
|
+
maxIterations?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Number of warmup iterations before timing (default: 2)
|
|
26
|
+
*/
|
|
27
|
+
warmupIterations?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to log debug information (default: false)
|
|
31
|
+
*/
|
|
32
|
+
debug?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BenchmarkResult {
|
|
36
|
+
/**
|
|
37
|
+
* Number of iterations actually performed
|
|
38
|
+
*/
|
|
39
|
+
iterations: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Total duration in milliseconds
|
|
43
|
+
*/
|
|
44
|
+
totalDurationMs: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Average duration per iteration in milliseconds
|
|
48
|
+
*/
|
|
49
|
+
avgIterationMs: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the benchmark hit the timeout
|
|
53
|
+
*/
|
|
54
|
+
timedOut: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runs a benchmark operation adaptively based on the performance of the test environment.
|
|
59
|
+
*
|
|
60
|
+
* This harness:
|
|
61
|
+
* 1. Runs warmup iterations to estimate operation time
|
|
62
|
+
* 2. Calculates how many iterations can fit within the target duration
|
|
63
|
+
* 3. Runs the calculated number of iterations with a safety timeout
|
|
64
|
+
*
|
|
65
|
+
* @param operation - The async function to benchmark (should be a single iteration)
|
|
66
|
+
* @param options - Configuration options for the benchmark
|
|
67
|
+
* @returns Results of the benchmark run
|
|
68
|
+
*/
|
|
69
|
+
export async function runAdaptiveBenchmark(
|
|
70
|
+
operation: () => Promise<void>,
|
|
71
|
+
options: BenchmarkOptions = {},
|
|
72
|
+
): Promise<BenchmarkResult> {
|
|
73
|
+
const {
|
|
74
|
+
targetDurationMs = 20_000,
|
|
75
|
+
maxTimeoutMs = 60_000,
|
|
76
|
+
minIterations = 5,
|
|
77
|
+
maxIterations = 10_000,
|
|
78
|
+
warmupIterations = 2,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
// Apply timing multiplier based on environment
|
|
82
|
+
const multiplier = getTimingMultiplier();
|
|
83
|
+
const adjustedTargetMs = targetDurationMs * multiplier;
|
|
84
|
+
const adjustedTimeoutMs = maxTimeoutMs * multiplier;
|
|
85
|
+
|
|
86
|
+
// Debug logging removed to prevent 'Cannot log after tests are done' errors
|
|
87
|
+
|
|
88
|
+
// Run warmup iterations
|
|
89
|
+
|
|
90
|
+
const warmupStart = Date.now();
|
|
91
|
+
for (let i = 0; i < warmupIterations; i++) {
|
|
92
|
+
await operation();
|
|
93
|
+
}
|
|
94
|
+
const warmupDuration = Date.now() - warmupStart;
|
|
95
|
+
const avgWarmupTime = warmupDuration / warmupIterations;
|
|
96
|
+
|
|
97
|
+
// Warmup timing debug info removed to prevent console logging issues
|
|
98
|
+
|
|
99
|
+
// Calculate target iterations based on warmup timing
|
|
100
|
+
// Add 10% safety margin to avoid overshooting
|
|
101
|
+
const safetyMargin = 0.9;
|
|
102
|
+
let targetIterations = Math.floor(
|
|
103
|
+
(adjustedTargetMs * safetyMargin) / avgWarmupTime,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Clamp to min/max bounds
|
|
107
|
+
targetIterations = Math.max(
|
|
108
|
+
minIterations,
|
|
109
|
+
Math.min(maxIterations, targetIterations),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Target iterations debug info removed to prevent console logging issues
|
|
113
|
+
|
|
114
|
+
// Set up timeout promise
|
|
115
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
116
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
117
|
+
timeoutHandle = setTimeout(() => {
|
|
118
|
+
reject(new Error(`Benchmark timeout after ${adjustedTimeoutMs}ms`));
|
|
119
|
+
}, adjustedTimeoutMs);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Run the actual benchmark
|
|
123
|
+
const benchmarkStart = Date.now();
|
|
124
|
+
let completedIterations = 0;
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Run iterations with timeout protection
|
|
129
|
+
await Promise.race([
|
|
130
|
+
(async () => {
|
|
131
|
+
for (let i = 0; i < targetIterations; i++) {
|
|
132
|
+
await operation();
|
|
133
|
+
completedIterations++;
|
|
134
|
+
|
|
135
|
+
// Check if we're approaching the timeout
|
|
136
|
+
const elapsed = Date.now() - benchmarkStart;
|
|
137
|
+
if (elapsed > adjustedTimeoutMs * 0.95) {
|
|
138
|
+
// Approaching timeout - stopping early
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
})(),
|
|
143
|
+
timeoutPromise,
|
|
144
|
+
]);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof Error && error.message.includes("timeout")) {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
// Benchmark timed out
|
|
149
|
+
} else {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalDuration = Date.now() - benchmarkStart;
|
|
157
|
+
const avgIterationTime = totalDuration / completedIterations;
|
|
158
|
+
|
|
159
|
+
const result: BenchmarkResult = {
|
|
160
|
+
iterations: completedIterations,
|
|
161
|
+
totalDurationMs: totalDuration,
|
|
162
|
+
avgIterationMs: avgIterationTime,
|
|
163
|
+
timedOut,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Benchmark results debug info removed to prevent console logging issues
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Helper function to run an operation with adaptive iterations and a callback.
|
|
173
|
+
* This is useful for tests that need to process results after each iteration.
|
|
174
|
+
*
|
|
175
|
+
* @param operation - The async function that returns a value
|
|
176
|
+
* @param callback - Function to process each result
|
|
177
|
+
* @param options - Configuration options for the benchmark
|
|
178
|
+
*/
|
|
179
|
+
export async function runAdaptiveBenchmarkWithCallback<T>(
|
|
180
|
+
operation: () => Promise<T>,
|
|
181
|
+
callback: (result: T, iteration: number) => void | Promise<void>,
|
|
182
|
+
options: BenchmarkOptions = {},
|
|
183
|
+
): Promise<BenchmarkResult> {
|
|
184
|
+
let iterationCount = 0;
|
|
185
|
+
|
|
186
|
+
const wrappedOperation = async () => {
|
|
187
|
+
const result = await operation();
|
|
188
|
+
await callback(result, iterationCount++);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return runAdaptiveBenchmark(wrappedOperation, options);
|
|
192
|
+
}
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { debugLogContext, isDebugEnabled } from "../debuglog";
|
|
2
2
|
|
|
3
|
+
// Ensure clean process state on Windows
|
|
4
|
+
process.on("uncaughtException", (err) => {
|
|
5
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
`Uncaught exception in debuglog-child: ${errorMessage}\n`,
|
|
8
|
+
);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
process.on("unhandledRejection", (reason) => {
|
|
13
|
+
const errorMessage =
|
|
14
|
+
reason instanceof Error ? reason.message : String(reason);
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
`Unhandled rejection in debuglog-child: ${errorMessage}\n`,
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
3
21
|
try {
|
|
4
22
|
const result = {
|
|
5
23
|
isDebugEnabled: isDebugEnabled(),
|
|
6
24
|
debugLogContext: debugLogContext(),
|
|
7
25
|
};
|
|
8
|
-
|
|
26
|
+
// Use process.stdout.write to ensure clean output
|
|
27
|
+
process.stdout.write(JSON.stringify(result));
|
|
9
28
|
process.exit(0);
|
|
10
29
|
} catch (err) {
|
|
11
|
-
|
|
30
|
+
// Don't log the error object directly as it might have circular references
|
|
31
|
+
// that cause issues with Jest's message passing on Windows
|
|
32
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
33
|
+
process.stderr.write(`Error in debuglog-child: ${errorMessage}\n`);
|
|
34
|
+
|
|
35
|
+
// Also log stack trace for debugging
|
|
36
|
+
if (err instanceof Error && err.stack) {
|
|
37
|
+
process.stderr.write(`Stack trace:\n${err.stack}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
process.exit(1);
|
|
13
41
|
}
|