@photostructure/fs-metadata 1.4.0 → 2.0.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/AGENTS.md +213 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -14
- 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/post-build.mjs +13 -4
- package/scripts/prebuildify-wrapper.ts +13 -4
- package/scripts/precommit.ts +4 -0
- package/src/binding.cpp +5 -0
- package/src/common/metadata_worker.h +8 -6
- package/src/common/shutdown.h +160 -0
- package/src/darwin/get_mount_point.cpp +6 -4
- package/src/darwin/hidden.cpp +9 -8
- package/src/darwin/hidden.h +4 -3
- package/src/darwin/volume_metadata.cpp +15 -1
- package/src/darwin/volume_mount_points.cpp +26 -4
- package/src/fs.ts +4 -1
- package/src/linux/volume_metadata.cpp +5 -1
- package/src/windows/hidden.cpp +10 -9
- package/src/windows/volume_metadata.cpp +5 -1
- package/src/windows/volume_mount_points.cpp +22 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/fs-metadata",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Cross-platform native filesystem metadata retrieval for Node.js",
|
|
5
5
|
"homepage": "https://photostructure.github.io/fs-metadata/",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"default": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
15
|
"import": {
|
|
16
|
-
"types": "./dist/index.d.
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
17
|
"default": "./dist/index.mjs"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"lint:native": "tsx scripts/clang-tidy.ts",
|
|
47
47
|
"lint:tsc": "tsc --noEmit",
|
|
48
48
|
"lint:eslint": "eslint",
|
|
49
|
+
"// check:exports": "verify package.json exports + type declarations resolve correctly (arethetypeswrong). Requires `npm run build:dist` first.",
|
|
50
|
+
"check:exports": "attw --pack .",
|
|
49
51
|
"fmt": "run-p fmt:*",
|
|
50
52
|
"// fmt:cpp": "on ubuntu: `sudo apt install clang-format`. Note that windows emits `invalid format` with this || true approach, but it works and is better than introducing Yet Another Script Script (like scripts/clang-format.mjs).",
|
|
51
53
|
"fmt:cpp": "clang-format --style=LLVM -i src/*.cpp src/*/*.cpp src/*/*.h || echo \"problem with clang-format\"",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"access": "public"
|
|
69
71
|
},
|
|
70
72
|
"engines": {
|
|
71
|
-
"node": ">=
|
|
73
|
+
"node": ">=22"
|
|
72
74
|
},
|
|
73
75
|
"os": [
|
|
74
76
|
"darwin",
|
|
@@ -90,32 +92,33 @@
|
|
|
90
92
|
"cross-platform"
|
|
91
93
|
],
|
|
92
94
|
"dependencies": {
|
|
93
|
-
"node-addon-api": "^8.
|
|
95
|
+
"node-addon-api": "^8.8.0",
|
|
94
96
|
"node-gyp-build": "^4.8.4"
|
|
95
97
|
},
|
|
96
98
|
"devDependencies": {
|
|
99
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
97
100
|
"@types/jest": "^30.0.0",
|
|
98
|
-
"@types/node": "^25.
|
|
101
|
+
"@types/node": "^25.9.1",
|
|
99
102
|
"@types/semver": "^7.7.1",
|
|
100
103
|
"cross-env": "^10.1.0",
|
|
101
104
|
"del-cli": "^7.0.0",
|
|
102
105
|
"eslint": "9.39.1",
|
|
103
106
|
"eslint-plugin-regexp": "^3.1.0",
|
|
104
107
|
"eslint-plugin-security": "^4.0.0",
|
|
105
|
-
"globals": "^17.
|
|
106
|
-
"jest": "^30.
|
|
107
|
-
"jest-environment-node": "^30.
|
|
108
|
+
"globals": "^17.6.0",
|
|
109
|
+
"jest": "^30.4.2",
|
|
110
|
+
"jest-environment-node": "^30.4.1",
|
|
108
111
|
"jest-extended": "^7.0.0",
|
|
109
|
-
"node-gyp": "^12.
|
|
110
|
-
"npm-check-updates": "^
|
|
111
|
-
"npm-run-all2": "
|
|
112
|
+
"node-gyp": "^12.3.0",
|
|
113
|
+
"npm-check-updates": "^22.2.2",
|
|
114
|
+
"npm-run-all2": "9.0.1",
|
|
112
115
|
"prebuildify": "^6.0.1",
|
|
113
116
|
"prettier": "^3.8.3",
|
|
114
117
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
115
|
-
"terser": "^5.
|
|
116
|
-
"ts-jest": "^29.4.
|
|
118
|
+
"terser": "^5.48.0",
|
|
119
|
+
"ts-jest": "^29.4.11",
|
|
117
120
|
"tsup": "^8.5.1",
|
|
118
|
-
"tsx": "^4.
|
|
121
|
+
"tsx": "^4.22.4",
|
|
119
122
|
"typedoc": "^0.28.19",
|
|
120
123
|
"typescript": "^5.9.3",
|
|
121
124
|
"typescript-eslint": "^8.57.2"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/post-build.mjs
CHANGED
|
@@ -7,15 +7,24 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const distDir = join(__dirname, "..", "dist");
|
|
9
9
|
|
|
10
|
-
// Copy .d.ts to .d.cts
|
|
11
|
-
|
|
10
|
+
// Copy .d.ts to .d.cts and .d.mts so each exports condition resolves to a
|
|
11
|
+
// declaration file whose module format matches its JavaScript counterpart.
|
|
12
|
+
//
|
|
13
|
+
// Without a .d.mts file, the `import` condition's types (`index.d.ts`) are
|
|
14
|
+
// interpreted as CommonJS (the package has no `"type": "module"`), while the
|
|
15
|
+
// resolved JavaScript (`index.mjs`) is ESM. That mismatch is the "Masquerading
|
|
16
|
+
// as CJS" / FalseCJS problem reported by arethetypeswrong. A .d.cts pairs with
|
|
17
|
+
// the CJS `.cjs` output; a .d.mts pairs with the ESM `.mjs` output.
|
|
18
|
+
async function createDualTypes() {
|
|
12
19
|
try {
|
|
13
20
|
await copyFile(join(distDir, "index.d.ts"), join(distDir, "index.d.cts"));
|
|
14
21
|
console.log("Created index.d.cts for CommonJS type safety");
|
|
22
|
+
await copyFile(join(distDir, "index.d.ts"), join(distDir, "index.d.mts"));
|
|
23
|
+
console.log("Created index.d.mts for ESM type safety");
|
|
15
24
|
} catch (error) {
|
|
16
|
-
console.error("Error creating
|
|
25
|
+
console.error("Error creating dual declaration files:", error);
|
|
17
26
|
process.exit(1);
|
|
18
27
|
}
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
createDualTypes();
|
|
@@ -21,6 +21,11 @@ console.log(`Building for platform: ${currentPlatform}, arch: ${currentArch}`);
|
|
|
21
21
|
// Set up environment variables to help node-gyp
|
|
22
22
|
const env = { ...process.env };
|
|
23
23
|
|
|
24
|
+
// Architecture-specific compiler defines for Windows. Tracked in a local so we
|
|
25
|
+
// can log the value we set without reading it back out of the environment
|
|
26
|
+
// (which CodeQL flags as clear-text logging of process environment data).
|
|
27
|
+
let clDefines: string | undefined;
|
|
28
|
+
|
|
24
29
|
// Set architecture-specific defines for Windows
|
|
25
30
|
if (currentPlatform === "win32") {
|
|
26
31
|
// Try various environment variables that might work
|
|
@@ -30,9 +35,13 @@ if (currentPlatform === "win32") {
|
|
|
30
35
|
|
|
31
36
|
// Try setting compiler flags directly
|
|
32
37
|
if (currentArch === "x64") {
|
|
33
|
-
|
|
38
|
+
clDefines = "/D_M_X64 /D_WIN64 /D_AMD64_";
|
|
34
39
|
} else if (currentArch === "arm64") {
|
|
35
|
-
|
|
40
|
+
clDefines = "/D_M_ARM64 /D_WIN64";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (clDefines) {
|
|
44
|
+
env.CL = clDefines;
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -53,8 +62,8 @@ if (process.argv.length > 2) {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
console.log(`Running: prebuildify ${args.join(" ")}`);
|
|
56
|
-
if (currentPlatform === "win32" &&
|
|
57
|
-
console.log(`CL environment variable: ${
|
|
65
|
+
if (currentPlatform === "win32" && clDefines) {
|
|
66
|
+
console.log(`CL environment variable: ${clDefines}`);
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// Spawn prebuildify with the arguments
|
package/scripts/precommit.ts
CHANGED
|
@@ -36,6 +36,10 @@ run({ cmd: "npm run fmt", desc: "Formatting code" });
|
|
|
36
36
|
run({ cmd: "npm run lint", desc: "Running linting checks" });
|
|
37
37
|
run({ cmd: "npm run docs", desc: "TypeDoc generation" });
|
|
38
38
|
run({ cmd: "npm run build:dist", desc: "Building distribution files" });
|
|
39
|
+
run({
|
|
40
|
+
cmd: "npm run check:exports",
|
|
41
|
+
desc: "Verifying package exports & type declarations (arethetypeswrong)",
|
|
42
|
+
});
|
|
39
43
|
|
|
40
44
|
// Detect if we're using glibc (vs musl)
|
|
41
45
|
// Check process.report for musl loader - if not found, assume glibc
|
package/src/binding.cpp
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <string>
|
|
4
4
|
|
|
5
5
|
#include "common/debug_log.h"
|
|
6
|
+
#include "common/shutdown.h"
|
|
6
7
|
#if defined(_WIN32)
|
|
7
8
|
#include "windows/fs_meta.h"
|
|
8
9
|
#include "windows/hidden.h"
|
|
@@ -67,6 +68,10 @@ Napi::Value SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
67
68
|
#endif
|
|
68
69
|
|
|
69
70
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
71
|
+
// Register a cleanup hook so in-flight native workers can short-circuit
|
|
72
|
+
// during env teardown instead of racing FreeEnvironment.
|
|
73
|
+
FSMeta::EnsureShutdownHook(env);
|
|
74
|
+
|
|
70
75
|
exports.Set("setDebugLogging", Napi::Function::New(env, SetDebugLogging));
|
|
71
76
|
exports.Set("setDebugPrefix", Napi::Function::New(env, SetDebugPrefix));
|
|
72
77
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/common/metadata_worker.h
|
|
2
2
|
#pragma once
|
|
3
|
+
#include "./shutdown.h"
|
|
3
4
|
#include "./volume_metadata.h"
|
|
4
5
|
#include <napi.h>
|
|
5
6
|
|
|
6
7
|
namespace FSMeta {
|
|
7
8
|
|
|
8
|
-
class MetadataWorkerBase : public
|
|
9
|
+
class MetadataWorkerBase : public SafeAsyncWorker {
|
|
9
10
|
protected:
|
|
10
11
|
std::string mountPoint;
|
|
11
12
|
VolumeMetadata metadata;
|
|
@@ -13,17 +14,18 @@ protected:
|
|
|
13
14
|
|
|
14
15
|
MetadataWorkerBase(const std::string &path,
|
|
15
16
|
const Napi::Promise::Deferred &deferred)
|
|
16
|
-
:
|
|
17
|
-
|
|
17
|
+
: SafeAsyncWorker(deferred.Env()), mountPoint(path), deferred_(deferred) {
|
|
18
|
+
}
|
|
18
19
|
|
|
19
20
|
void OnError(const Napi::Error &error) override {
|
|
20
|
-
|
|
21
|
+
Napi::HandleScope scope(Env());
|
|
22
|
+
SafeReject(deferred_, error.Value());
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
void OnOK() override {
|
|
24
26
|
Napi::HandleScope scope(Env());
|
|
25
|
-
deferred_
|
|
27
|
+
SafeResolve(deferred_, metadata.ToObject(Env()));
|
|
26
28
|
}
|
|
27
29
|
}; // class MetadataWorkerBase
|
|
28
30
|
|
|
29
|
-
} // namespace FSMeta
|
|
31
|
+
} // namespace FSMeta
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/common/shutdown.h
|
|
2
|
+
// Shutdown-safety helpers: a per-env flag set during Node env teardown,
|
|
3
|
+
// plus deferred Resolve/Reject wrappers that swallow C++ Napi errors so they
|
|
4
|
+
// can never escape AsyncWorker callbacks.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: AsyncWorker::OnWorkComplete can run during
|
|
7
|
+
// node::FreeEnvironment cleanup. If napi_resolve_deferred /
|
|
8
|
+
// napi_reject_deferred fail at that point (env tearing down), node-addon-api
|
|
9
|
+
// throws a C++ Napi::Error. With NAPI_CPP_EXCEPTIONS the rethrow path inside
|
|
10
|
+
// WrapVoidCallback then calls ThrowAsJavaScriptException, which can also fail,
|
|
11
|
+
// letting the C++ exception escape into a libuv cleanup hook frame that has
|
|
12
|
+
// no catch - terminate() / SIGABRT.
|
|
13
|
+
//
|
|
14
|
+
// The flag is stored as napi instance data so worker threads (each their own
|
|
15
|
+
// env) don't poison the main env's flag when they tear down.
|
|
16
|
+
//
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include <atomic>
|
|
21
|
+
#include <memory>
|
|
22
|
+
#include <napi.h>
|
|
23
|
+
#include <string>
|
|
24
|
+
|
|
25
|
+
namespace FSMeta {
|
|
26
|
+
|
|
27
|
+
struct ShutdownState {
|
|
28
|
+
std::atomic<bool> shuttingDown{false};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct ModuleInstanceData {
|
|
32
|
+
std::shared_ptr<ShutdownState> shutdownState =
|
|
33
|
+
std::make_shared<ShutdownState>();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
inline ModuleInstanceData *GetInstanceData(napi_env env) {
|
|
37
|
+
void *raw = nullptr;
|
|
38
|
+
if (napi_get_instance_data(env, &raw) != napi_ok) {
|
|
39
|
+
return nullptr;
|
|
40
|
+
}
|
|
41
|
+
return static_cast<ModuleInstanceData *>(raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
inline bool IsShuttingDown(napi_env env) {
|
|
45
|
+
if (auto *d = GetInstanceData(env)) {
|
|
46
|
+
return d->shutdownState->shuttingDown.load(std::memory_order_acquire);
|
|
47
|
+
}
|
|
48
|
+
// No instance data registered (test harness, etc.) - fail safe: not shutting.
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
inline std::shared_ptr<ShutdownState> GetShutdownState(napi_env env) {
|
|
53
|
+
if (auto *d = GetInstanceData(env)) {
|
|
54
|
+
return d->shutdownState;
|
|
55
|
+
}
|
|
56
|
+
return nullptr;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
inline bool IsShuttingDown(const std::shared_ptr<ShutdownState> &state) {
|
|
60
|
+
return state != nullptr &&
|
|
61
|
+
state->shuttingDown.load(std::memory_order_acquire);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Registers per-env shutdown state and a cleanup hook that flips the flag.
|
|
65
|
+
// Idempotent per env: binding.cpp Init runs once per env load.
|
|
66
|
+
inline void EnsureShutdownHook(napi_env env) {
|
|
67
|
+
if (GetInstanceData(env) != nullptr) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
auto *data = new ModuleInstanceData();
|
|
71
|
+
auto *cleanupState = new std::shared_ptr<ShutdownState>(data->shutdownState);
|
|
72
|
+
napi_status status = napi_set_instance_data(
|
|
73
|
+
env, data,
|
|
74
|
+
[](napi_env /*env*/, void *raw, void * /*hint*/) {
|
|
75
|
+
delete static_cast<ModuleInstanceData *>(raw);
|
|
76
|
+
},
|
|
77
|
+
nullptr);
|
|
78
|
+
if (status != napi_ok) {
|
|
79
|
+
delete cleanupState;
|
|
80
|
+
delete data;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
status = napi_add_env_cleanup_hook(
|
|
84
|
+
env,
|
|
85
|
+
[](void *arg) {
|
|
86
|
+
auto *state = static_cast<std::shared_ptr<ShutdownState> *>(arg);
|
|
87
|
+
(*state)->shuttingDown.store(true, std::memory_order_release);
|
|
88
|
+
delete state;
|
|
89
|
+
},
|
|
90
|
+
cleanupState);
|
|
91
|
+
if (status != napi_ok) {
|
|
92
|
+
delete cleanupState;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wrap deferred.Resolve so a teardown-time napi failure cannot escape the
|
|
97
|
+
// AsyncWorker callback as an uncaught C++ exception.
|
|
98
|
+
inline void SafeResolve(const Napi::Promise::Deferred &deferred,
|
|
99
|
+
napi_value value) {
|
|
100
|
+
try {
|
|
101
|
+
deferred.Resolve(value);
|
|
102
|
+
} catch (...) {
|
|
103
|
+
// Env is tearing down; the JS-side promise is unobservable anyway.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
inline void SafeReject(const Napi::Promise::Deferred &deferred,
|
|
108
|
+
napi_value value) {
|
|
109
|
+
try {
|
|
110
|
+
deferred.Reject(value);
|
|
111
|
+
} catch (...) {
|
|
112
|
+
// Env is tearing down; the JS-side promise is unobservable anyway.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class SafeAsyncWorker : public Napi::AsyncWorker {
|
|
117
|
+
public:
|
|
118
|
+
void OnExecute(Napi::Env /*env*/) override {
|
|
119
|
+
try {
|
|
120
|
+
Execute();
|
|
121
|
+
} catch (const std::exception &e) {
|
|
122
|
+
SetError(e.what());
|
|
123
|
+
} catch (...) {
|
|
124
|
+
SetError("Unknown native error");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void OnWorkComplete(Napi::Env env, napi_status status) override {
|
|
129
|
+
if (status != napi_cancelled && !IsShuttingDown()) {
|
|
130
|
+
try {
|
|
131
|
+
Napi::HandleScope scope(env);
|
|
132
|
+
if (status != napi_ok) {
|
|
133
|
+
OnError(Napi::Error::New(env));
|
|
134
|
+
} else if (error_.empty()) {
|
|
135
|
+
OnOK();
|
|
136
|
+
} else {
|
|
137
|
+
OnError(Napi::Error::New(env, error_));
|
|
138
|
+
}
|
|
139
|
+
} catch (...) {
|
|
140
|
+
// Env teardown can make value construction, handle scopes, or deferred
|
|
141
|
+
// resolution fail. The JS promise is no longer observable then.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
Destroy();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected:
|
|
148
|
+
explicit SafeAsyncWorker(Napi::Env env)
|
|
149
|
+
: Napi::AsyncWorker(env), shutdownState_(GetShutdownState(env)) {}
|
|
150
|
+
|
|
151
|
+
void SetError(const std::string &error) { error_ = error; }
|
|
152
|
+
|
|
153
|
+
bool IsShuttingDown() const { return FSMeta::IsShuttingDown(shutdownState_); }
|
|
154
|
+
|
|
155
|
+
private:
|
|
156
|
+
std::shared_ptr<ShutdownState> shutdownState_;
|
|
157
|
+
std::string error_;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
} // namespace FSMeta
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "../common/error_utils.h"
|
|
8
8
|
#include "../common/fd_guard.h"
|
|
9
9
|
#include "../common/path_security.h"
|
|
10
|
+
#include "../common/shutdown.h"
|
|
10
11
|
|
|
11
12
|
#include <fcntl.h>
|
|
12
13
|
#include <string>
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
|
|
17
18
|
namespace FSMeta {
|
|
18
19
|
|
|
19
|
-
class GetMountPointWorker : public
|
|
20
|
+
class GetMountPointWorker : public SafeAsyncWorker {
|
|
20
21
|
public:
|
|
21
22
|
GetMountPointWorker(const std::string &path,
|
|
22
23
|
const Napi::Promise::Deferred &deferred)
|
|
23
|
-
:
|
|
24
|
+
: SafeAsyncWorker(deferred.Env()), path_(path), deferred_(deferred) {}
|
|
24
25
|
|
|
25
26
|
void Execute() override {
|
|
26
27
|
DEBUG_LOG("[GetMountPointWorker] Executing for path: %s", path_.c_str());
|
|
@@ -65,11 +66,12 @@ public:
|
|
|
65
66
|
|
|
66
67
|
void OnOK() override {
|
|
67
68
|
Napi::HandleScope scope(Env());
|
|
68
|
-
deferred_
|
|
69
|
+
SafeResolve(deferred_, Napi::String::New(Env(), result_));
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
void OnError(const Napi::Error &error) override {
|
|
72
|
-
|
|
73
|
+
Napi::HandleScope scope(Env());
|
|
74
|
+
SafeReject(deferred_, error.Value());
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
private:
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
5
|
#include "../common/fd_guard.h"
|
|
6
6
|
#include "../common/path_security.h"
|
|
7
|
+
#include "../common/shutdown.h"
|
|
7
8
|
#include <fcntl.h> // for open(), O_RDONLY, O_CLOEXEC
|
|
8
9
|
#include <string.h> // for strcmp
|
|
9
10
|
#include <sys/mount.h>
|
|
@@ -14,7 +15,7 @@ namespace FSMeta {
|
|
|
14
15
|
|
|
15
16
|
GetHiddenWorker::GetHiddenWorker(std::string path,
|
|
16
17
|
Napi::Promise::Deferred deferred)
|
|
17
|
-
:
|
|
18
|
+
: SafeAsyncWorker(deferred.Env()), path_(std::move(path)),
|
|
18
19
|
deferred_(deferred), is_hidden_(false) {
|
|
19
20
|
DEBUG_LOG("[GetHiddenWorker] created for path: %s", path_.c_str());
|
|
20
21
|
}
|
|
@@ -81,12 +82,12 @@ void GetHiddenWorker::Execute() {
|
|
|
81
82
|
void GetHiddenWorker::OnOK() {
|
|
82
83
|
Napi::HandleScope scope(Env());
|
|
83
84
|
auto env = Env();
|
|
84
|
-
deferred_
|
|
85
|
+
SafeResolve(deferred_, Napi::Boolean::New(env, is_hidden_));
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
void GetHiddenWorker::OnError(const Napi::Error &error) {
|
|
88
89
|
Napi::HandleScope scope(Env());
|
|
89
|
-
deferred_
|
|
90
|
+
SafeReject(deferred_, error.Value());
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
@@ -111,8 +112,8 @@ Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
111
112
|
|
|
112
113
|
SetHiddenWorker::SetHiddenWorker(std::string path, bool hidden,
|
|
113
114
|
Napi::Promise::Deferred deferred)
|
|
114
|
-
:
|
|
115
|
-
|
|
115
|
+
: SafeAsyncWorker(deferred.Env()), path_(std::move(path)), hidden_(hidden),
|
|
116
|
+
deferred_(deferred) {
|
|
116
117
|
DEBUG_LOG("[SetHiddenWorker] created for path: %s, hidden: %d", path_.c_str(),
|
|
117
118
|
hidden_);
|
|
118
119
|
}
|
|
@@ -206,12 +207,12 @@ void SetHiddenWorker::Execute() {
|
|
|
206
207
|
void SetHiddenWorker::OnOK() {
|
|
207
208
|
Napi::HandleScope scope(Env());
|
|
208
209
|
auto env = Env();
|
|
209
|
-
deferred_
|
|
210
|
+
SafeResolve(deferred_, env.Undefined());
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
void SetHiddenWorker::OnError(const Napi::Error &error) {
|
|
213
214
|
Napi::HandleScope scope(Env());
|
|
214
|
-
deferred_
|
|
215
|
+
SafeReject(deferred_, error.Value());
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
@@ -238,4 +239,4 @@ Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
238
239
|
return deferred.Promise();
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
} // namespace FSMeta
|
|
242
|
+
} // namespace FSMeta
|
package/src/darwin/hidden.h
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/darwin/hidden.h
|
|
2
2
|
#pragma once
|
|
3
3
|
#include "../common/hidden.h"
|
|
4
|
+
#include "../common/shutdown.h"
|
|
4
5
|
#include <napi.h>
|
|
5
6
|
|
|
6
7
|
namespace FSMeta {
|
|
7
8
|
|
|
8
|
-
class GetHiddenWorker : public
|
|
9
|
+
class GetHiddenWorker : public SafeAsyncWorker {
|
|
9
10
|
public:
|
|
10
11
|
GetHiddenWorker(std::string path, Napi::Promise::Deferred deferred);
|
|
11
12
|
void Execute() override;
|
|
@@ -18,7 +19,7 @@ private:
|
|
|
18
19
|
bool is_hidden_;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
class SetHiddenWorker : public
|
|
22
|
+
class SetHiddenWorker : public SafeAsyncWorker {
|
|
22
23
|
public:
|
|
23
24
|
SetHiddenWorker(std::string path, bool hidden,
|
|
24
25
|
Napi::Promise::Deferred deferred);
|
|
@@ -32,4 +33,4 @@ private:
|
|
|
32
33
|
Napi::Promise::Deferred deferred_;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
} // namespace FSMeta
|
|
36
|
+
} // namespace FSMeta
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "../common/debug_log.h"
|
|
5
5
|
#include "../common/fd_guard.h"
|
|
6
6
|
#include "../common/path_security.h"
|
|
7
|
+
#include "../common/shutdown.h"
|
|
7
8
|
#include "../common/volume_utils.h"
|
|
8
9
|
#include "./da_mutex.h"
|
|
9
10
|
#include "./fs_meta.h"
|
|
@@ -76,6 +77,11 @@ public:
|
|
|
76
77
|
void Execute() override {
|
|
77
78
|
DEBUG_LOG("[GetVolumeMetadataWorker] Executing for mount point: %s",
|
|
78
79
|
mountPoint.c_str());
|
|
80
|
+
if (IsShuttingDown()) {
|
|
81
|
+
// Avoid kicking off blocking IOKit/DA calls during env teardown.
|
|
82
|
+
SetError("fs-metadata: shutdown in progress");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
79
85
|
try {
|
|
80
86
|
// Validate and canonicalize mount point using realpath()
|
|
81
87
|
// This follows Apple's Secure Coding Guide recommendations
|
|
@@ -207,6 +213,14 @@ private:
|
|
|
207
213
|
DEBUG_LOG("[GetVolumeMetadataWorker] Getting Disk Arbitration info for: %s",
|
|
208
214
|
mountPoint.c_str());
|
|
209
215
|
|
|
216
|
+
if (IsShuttingDown()) {
|
|
217
|
+
// IOServiceGetMatchingService is uncancellable; if we're already in
|
|
218
|
+
// teardown, surface a partial result rather than block FreeEnvironment.
|
|
219
|
+
metadata.status = "partial";
|
|
220
|
+
metadata.error = "shutdown in progress";
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
// Check if this is a network filesystem
|
|
211
225
|
if (metadata.fstype == "smbfs" || metadata.fstype == "nfs" ||
|
|
212
226
|
metadata.fstype == "afpfs" || metadata.fstype == "webdav") {
|
|
@@ -370,4 +384,4 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
370
384
|
return deferred.Promise();
|
|
371
385
|
}
|
|
372
386
|
|
|
373
|
-
} // namespace FSMeta
|
|
387
|
+
} // namespace FSMeta
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "../common/volume_mount_points.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
+
#include "../common/shutdown.h"
|
|
5
6
|
#include "./da_mutex.h"
|
|
6
7
|
#include "./fs_meta.h"
|
|
7
8
|
#include "./raii_utils.h"
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
|
|
14
15
|
namespace FSMeta {
|
|
15
16
|
|
|
16
|
-
class GetVolumeMountPointsWorker : public
|
|
17
|
+
class GetVolumeMountPointsWorker : public SafeAsyncWorker {
|
|
17
18
|
private:
|
|
18
19
|
Napi::Promise::Deferred deferred_;
|
|
19
20
|
std::vector<MountPoint> mountPoints_;
|
|
@@ -22,11 +23,15 @@ private:
|
|
|
22
23
|
public:
|
|
23
24
|
GetVolumeMountPointsWorker(const Napi::Promise::Deferred &deferred,
|
|
24
25
|
uint32_t timeoutMs = 5000)
|
|
25
|
-
:
|
|
26
|
+
: SafeAsyncWorker(deferred.Env()), deferred_(deferred),
|
|
26
27
|
timeoutMs_(timeoutMs) {}
|
|
27
28
|
|
|
28
29
|
void Execute() override {
|
|
29
30
|
DEBUG_LOG("[GetVolumeMountPointsWorker] Executing");
|
|
31
|
+
if (IsShuttingDown()) {
|
|
32
|
+
SetError("fs-metadata: shutdown in progress");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
try {
|
|
31
36
|
MountBufferRAII mntbuf;
|
|
32
37
|
// Use MNT_NOWAIT for better performance - we'll verify accessibility
|
|
@@ -57,6 +62,10 @@ public:
|
|
|
57
62
|
{
|
|
58
63
|
std::lock_guard<std::mutex> lock(g_diskArbitrationMutex);
|
|
59
64
|
|
|
65
|
+
if (IsShuttingDown()) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
DASessionRAII session(DASessionCreate(kCFAllocatorDefault));
|
|
61
70
|
if (session.isValid()) {
|
|
62
71
|
static dispatch_queue_t da_queue = dispatch_queue_create(
|
|
@@ -66,6 +75,10 @@ public:
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
for (int j = 0; j < count; j++) {
|
|
78
|
+
if (IsShuttingDown()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
MountPoint mp;
|
|
70
83
|
mp.mountPoint = mntbuf.get()[j].f_mntonname;
|
|
71
84
|
mp.fstype = mntbuf.get()[j].f_fstypename;
|
|
@@ -88,6 +101,10 @@ public:
|
|
|
88
101
|
const size_t maxConcurrentChecks = 4; // Limit concurrent access checks
|
|
89
102
|
|
|
90
103
|
for (size_t i = 0; i < allMountPoints.size(); i += maxConcurrentChecks) {
|
|
104
|
+
if (IsShuttingDown()) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
std::vector<std::future<std::pair<std::string, bool>>> futures;
|
|
92
109
|
std::vector<MountPoint *> batchPtrs;
|
|
93
110
|
|
|
@@ -182,7 +199,12 @@ public:
|
|
|
182
199
|
result[i] = mountPoints_[i].ToObject(env);
|
|
183
200
|
}
|
|
184
201
|
|
|
185
|
-
deferred_
|
|
202
|
+
SafeResolve(deferred_, result);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void OnError(const Napi::Error &error) override {
|
|
206
|
+
Napi::HandleScope scope(Env());
|
|
207
|
+
SafeReject(deferred_, error.Value());
|
|
186
208
|
}
|
|
187
209
|
};
|
|
188
210
|
|
|
@@ -202,4 +224,4 @@ Napi::Promise GetVolumeMountPoints(const Napi::CallbackInfo &info) {
|
|
|
202
224
|
return deferred.Promise();
|
|
203
225
|
}
|
|
204
226
|
|
|
205
|
-
} // namespace FSMeta
|
|
227
|
+
} // namespace FSMeta
|
package/src/fs.ts
CHANGED
|
@@ -10,7 +10,10 @@ import { withTimeout } from "./async";
|
|
|
10
10
|
*/
|
|
11
11
|
export async function statAsync(
|
|
12
12
|
path: PathLike,
|
|
13
|
-
|
|
13
|
+
// `throwIfNoEntry?: true` selects the overload that resolves to
|
|
14
|
+
// Promise<Stats> rather than Promise<Stats | undefined>; this wrapper always
|
|
15
|
+
// throws (rather than returning undefined) when the path doesn't exist.
|
|
16
|
+
options?: StatOptions & { bigint?: false; throwIfNoEntry?: true },
|
|
14
17
|
): Promise<Stats> {
|
|
15
18
|
return stat(path, options);
|
|
16
19
|
}
|