@photostructure/fs-metadata 1.3.0 → 1.4.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.
Files changed (52) hide show
  1. package/AGENTS.md +213 -0
  2. package/CHANGELOG.md +54 -0
  3. package/CONTRIBUTING.md +3 -3
  4. package/binding.gyp +0 -22
  5. package/dist/index.cjs +10 -39
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +1 -1
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.mjs +10 -39
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/C++_REVIEW_TODO.md +3 -36
  13. package/doc/LINUX_API_REFERENCE.md +4 -147
  14. package/doc/SECURITY_AUDIT_2026.md +4 -0
  15. package/doc/gotchas.md +27 -0
  16. package/package.json +11 -12
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  23. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  24. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  25. package/scripts/clang-tidy.ts +1 -3
  26. package/scripts/prebuild-linux-glibc.sh +1 -5
  27. package/scripts/sanitizers-test.sh +0 -1
  28. package/src/binding.cpp +5 -15
  29. package/src/common/metadata_worker.h +7 -6
  30. package/src/common/shutdown.h +162 -0
  31. package/src/darwin/get_mount_point.cpp +6 -4
  32. package/src/darwin/hidden.cpp +9 -8
  33. package/src/darwin/hidden.h +4 -3
  34. package/src/darwin/volume_metadata.cpp +15 -1
  35. package/src/darwin/volume_mount_points.cpp +26 -4
  36. package/src/linux/mount_points.ts +8 -42
  37. package/src/linux/volume_metadata.cpp +5 -17
  38. package/src/mount_point_for_path.ts +1 -1
  39. package/src/types/mount_point.ts +1 -1
  40. package/src/types/native_bindings.ts +0 -5
  41. package/src/volume_metadata.ts +16 -7
  42. package/src/volume_mount_points.ts +1 -1
  43. package/src/windows/hidden.cpp +10 -9
  44. package/src/windows/volume_metadata.cpp +5 -1
  45. package/src/windows/volume_mount_points.cpp +22 -4
  46. package/scripts/setup-native.mjs +0 -39
  47. package/src/linux/gio_mount_points.cpp +0 -80
  48. package/src/linux/gio_mount_points.h +0 -37
  49. package/src/linux/gio_utils.cpp +0 -115
  50. package/src/linux/gio_utils.h +0 -69
  51. package/src/linux/gio_volume_metadata.cpp +0 -81
  52. package/src/linux/gio_volume_metadata.h +0 -20
@@ -142,12 +142,10 @@ This document outlines a comprehensive review of all C++ files in the fs-metadat
142
142
 
143
143
  - [x] Review BlkidCache RAII wrapper - Already excellent
144
144
  - [x] Check blkid_get_tag_value free() calls - Correctly using free()
145
- - [x] Verify GIO integration memory management - Proper smart pointers
146
145
  - [x] Validate statvfs error handling - Already comprehensive
147
146
  - [x] **Added**: Input validation for empty mount points
148
147
  - **Platform APIs verified**:
149
148
  - libblkid API availability
150
- - GIO optional dependency handling
151
149
  - statvfs behavior
152
150
 
153
151
  #### `src/linux/blkid_cache.cpp` ✅
@@ -159,38 +157,8 @@ This document outlines a comprehensive review of all C++ files in the fs-metadat
159
157
  - **Platform APIs verified**:
160
158
  - libblkid cache APIs - Proper use of blkid_get_cache/blkid_put_cache
161
159
 
162
- #### `src/linux/gio_utils.cpp` (if GIO enabled)
163
-
164
- - [x] Review g_object_unref patterns - Already using GioResource RAII
165
- - [x] Check GError cleanup - Not used in this file (GError-free patterns)
166
- - [x] Verify GMount/GVolume reference counting - Proper ref/unref pairs
167
- - [x] **Added**: Exception handling in forEachMount callback
168
- - [x] **Added**: Null checks for root.get() before G_IS_FILE
169
- - [x] **Added**: const-correctness for GioResource and callback results
170
- - **Platform APIs verified**:
171
- - GLib/GIO APIs (GNOME) - Proper memory management patterns
172
- - GVfs mount integration
173
-
174
- #### `src/linux/gio_mount_points.cpp` (if GIO enabled) ✅
175
-
176
- - [x] Review g_volume_monitor lifecycle - Correct usage (singleton pattern)
177
- - [x] Check GList cleanup patterns - Using g_list_free_full correctly
178
- - [x] Verify string ownership - Proper GCharPtr smart pointers
179
- - [x] **Added**: const-correctness for local variables (GCharPtr, GFileInfoPtr, etc.)
180
- - **Platform APIs verified**:
181
- - GVolumeMonitor APIs - Reference counting rules
182
- - Mount enumeration
183
-
184
- #### `src/linux/gio_volume_metadata.cpp` (if GIO enabled) ✅
185
-
186
- - [x] Review g_drive object management - Using GObjectPtr smart pointers
187
- - [x] Check g_icon handling - No GIcon usage in this file
188
- - [x] Verify string ownership - Proper GCharPtr usage
189
- - [x] **Added**: Defensive null checks for GObjectPtr::get()
190
- - [x] **Added**: Null checks for string getters
191
- - [x] **Added**: const-correctness for all GCharPtr and GObjectPtr locals
192
- - **Platform APIs verified**:
193
- - GDrive/GVolume metadata APIs - Ownership transfer rules
160
+ > **Note**: GIO/GLib integration was removed in favor of `/proc/self/mounts`
161
+ > parsing in TypeScript. See CHANGELOG.md for details.
194
162
 
195
163
  ## Testing Strategy
196
164
 
@@ -237,7 +205,7 @@ This document outlines a comprehensive review of all C++ files in the fs-metadat
237
205
  - ✅ WNetConnection: Handle ERROR_MORE_DATA with dynamic buffer resize
238
206
  - ✅ GetVolumeInformation: Use MAX_PATH+1 instead of BUFFER_SIZE
239
207
  3. ~~**High**: CoreFoundation reference counting in macOS code~~ ✅ Completed - CFReleaser RAII wrapper
240
- 4. ~~**High**: GIO object lifecycle management~~ ✅ Completed - Using smart pointers throughout
208
+ 4. ~~**High**: GIO object lifecycle management~~ ✅ N/A - GIO integration removed entirely (see CHANGELOG)
241
209
  5. ~~**Medium**: Security - Add comprehensive path validation~~ ✅ Completed
242
210
  - ✅ Check for ".." in all platforms (Windows/Darwin have it, Linux doesn't have hidden file support)
243
211
  - Validate against null bytes and special characters (remaining)
@@ -358,6 +326,5 @@ The Darwin/macOS implementation demonstrates excellent resource management with
358
326
  ### Linux
359
327
 
360
328
  - libblkid: Use free() for blkid_get_tag_value results, blkid_put_cache() for cache
361
- - GLib/GIO: Use g_object_unref() or g_clear_object(), match allocation functions
362
329
  - Key Documentation: libblkid man pages, GNOME Developer Documentation
363
330
  - **Review Status**: ✅ All files properly reviewed and use smart pointers
@@ -4,115 +4,10 @@ Reference for Linux APIs used in fs-metadata, with links to official documentati
4
4
 
5
5
  ## Table of Contents
6
6
 
7
- 1. [GIO/GLib APIs](#gioglib-apis)
8
- 2. [libblkid APIs](#libblkid-apis)
9
- 3. [POSIX File System APIs](#posix-file-system-apis)
10
- 4. [RAII Patterns](#raii-patterns)
11
- 5. [References](#references)
12
-
13
- ## GIO/GLib APIs
14
-
15
- ### Thread Safety Overview
16
-
17
- **Critical**: GIO has different thread safety guarantees for different APIs:
18
-
19
- | API | Thread Safe? | Used? | Notes |
20
- | ------------------------ | ------------ | ----- | ----------------------------------------- |
21
- | `g_unix_mounts_get()` | Yes | ✅ | Uses `getmntent_r()` or internal `G_LOCK` |
22
- | `g_unix_mount_get_*()` | Yes | ✅ | Safe on `GUnixMountEntry` |
23
- | `g_volume_monitor_get()` | **No** | ❌ | Main thread only - **NOT USED** |
24
-
25
- **GIO Threading Docs**: https://docs.gtk.org/gio/class.VolumeMonitor.html
26
-
27
- > "GVolumeMonitor is not thread-default-context aware and should not be used other than from the main thread."
28
-
29
- This codebase only uses thread-safe APIs (`g_unix_mounts_get()` and related functions)
30
- because all native code runs on `Napi::AsyncWorker` threads.
31
-
32
- ### g_unix_mounts_get
33
-
34
- - **Docs**: https://docs.gtk.org/gio/func.unix_mounts_get.html
35
- - **Purpose**: Thread-safe enumeration of mounted filesystems
36
- - **Returns**: `GList*` of `GUnixMountEntry*` (caller owns both)
37
- - **Cleanup**: `g_list_free_full(list, (GDestroyNotify)g_unix_mount_free)`
38
-
39
- ```cpp
40
- GList *mounts = g_unix_mounts_get(nullptr);
41
- for (GList *l = mounts; l != nullptr; l = l->next) {
42
- GUnixMountEntry *entry = static_cast<GUnixMountEntry*>(l->data);
43
- const char *path = g_unix_mount_get_mount_path(entry);
44
- const char *fstype = g_unix_mount_get_fs_type(entry);
45
- }
46
- g_list_free_full(mounts, reinterpret_cast<GDestroyNotify>(g_unix_mount_free));
47
- ```
48
-
49
- **Source**: https://gitlab.gnome.org/GNOME/glib/-/blob/main/gio/gunixmounts.c
50
-
51
- ### GUnixMountEntry Functions
52
-
53
- - **Docs**: https://docs.gtk.org/gio-unix/struct.MountEntry.html
54
- - `g_unix_mount_get_mount_path()` - Returns `const char*` (borrowed)
55
- - `g_unix_mount_get_fs_type()` - Returns `const char*` (borrowed)
56
- - `g_unix_mount_get_device_path()` - Returns `const char*` (borrowed)
57
- - `g_unix_mount_free()` - Free a single entry
58
-
59
- **Note**: String returns are borrowed - do NOT `g_free()` them.
60
-
61
- ### g_volume_monitor_get (NOT USED)
62
-
63
- > ⚠️ **NOT USED IN THIS CODEBASE**: GVolumeMonitor is NOT thread-safe and has been
64
- > removed from fs-metadata. This section is retained for reference only.
65
-
66
- - **Docs**: https://docs.gtk.org/gio/type_func.VolumeMonitor.get.html
67
- - **Purpose**: Get system volume monitor for rich metadata
68
- - **Returns**: Owned `GVolumeMonitor*` reference
69
- - **Cleanup**: **Must** call `g_object_unref()` when done
70
-
71
- **Why Not Used**: GVolumeMonitor can only be safely used from the main thread.
72
- Node.js native addons use `Napi::AsyncWorker` which runs on libuv thread pool
73
- worker threads. Using GVolumeMonitor from these threads causes race conditions:
74
-
75
- ```
76
- GLib-GObject-CRITICAL: g_object_ref: assertion '!object_already_finalized' failed
77
- ```
78
-
79
- The thread-safe `g_unix_mounts_get()` API provides sufficient metadata (fstype,
80
- mountFrom). Rich metadata (label, uri) is obtained from blkid instead.
81
-
82
- ### g_volume_monitor_get_mounts (NOT USED)
83
-
84
- > ⚠️ **NOT USED**: See above - GVolumeMonitor is not thread-safe.
85
-
86
- - **Docs**: https://docs.gtk.org/gio/method.VolumeMonitor.get_mounts.html
87
- - **Returns**: `GList*` of `GMount*` (caller owns list and references)
88
- - **Cleanup**: `g_list_free_full(mounts, g_object_unref)`
89
-
90
- ### GMount / GVolume / GFile Functions (NOT USED)
91
-
92
- > ⚠️ **NOT USED**: These require GVolumeMonitor which is not thread-safe.
93
-
94
- | Function | Returns | Ownership |
95
- | -------------------------------- | ---------- | ------------------- |
96
- | `g_mount_get_root()` | `GFile*` | Caller owns, unref |
97
- | `g_mount_get_volume()` | `GVolume*` | Caller owns, unref |
98
- | `g_mount_get_name()` | `char*` | Caller owns, g_free |
99
- | `g_mount_get_default_location()` | `GFile*` | Caller owns, unref |
100
- | `g_file_get_path()` | `char*` | Caller owns, g_free |
101
- | `g_file_get_uri()` | `char*` | Caller owns, g_free |
102
- | `g_volume_get_name()` | `char*` | Caller owns, g_free |
103
-
104
- **Memory Management Docs**: https://docs.gtk.org/gobject/concepts.html#reference-counting
105
-
106
- ### GLib Memory Functions
107
-
108
- | Function | Purpose |
109
- | -------------------- | ------------------------------------- |
110
- | `g_object_unref()` | Decrement GObject reference count |
111
- | `g_free()` | Free GLib-allocated memory |
112
- | `g_list_free()` | Free GList container only |
113
- | `g_list_free_full()` | Free GList and elements with callback |
114
-
115
- **Docs**: https://docs.gtk.org/glib/func.free.html
7
+ 1. [libblkid APIs](#libblkid-apis)
8
+ 2. [POSIX File System APIs](#posix-file-system-apis)
9
+ 3. [RAII Patterns](#raii-patterns)
10
+ 4. [References](#references)
116
11
 
117
12
  ## libblkid APIs
118
13
 
@@ -221,32 +116,6 @@ if (fd < 0) {
221
116
 
222
117
  ## RAII Patterns
223
118
 
224
- ### GIO Smart Pointers (gio_utils.h)
225
-
226
- ```cpp
227
- // Custom deleters
228
- template <typename T> struct GObjectDeleter {
229
- void operator()(T *ptr) const { if (ptr) g_object_unref(ptr); }
230
- };
231
-
232
- struct GFreeDeleter {
233
- void operator()(void *ptr) const { if (ptr) g_free(ptr); }
234
- };
235
-
236
- // Smart pointer aliases
237
- template <typename T> using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
238
-
239
- using GFilePtr = GObjectPtr<GFile>;
240
- using GMountPtr = GObjectPtr<GMount>;
241
- using GVolumePtr = GObjectPtr<GVolume>;
242
- using GVolumeMonitorPtr = GObjectPtr<GVolumeMonitor>; // Not currently used
243
- using GCharPtr = std::unique_ptr<char, GFreeDeleter>;
244
- ```
245
-
246
- > **Note**: `GVolumeMonitorPtr`, `GMountPtr`, `GVolumePtr`, and `GFilePtr` are
247
- > defined but not currently used because GVolumeMonitor is not thread-safe.
248
- > They are retained for potential future use if a main-thread-only API is added.
249
-
250
119
  ### BlkidCache RAII
251
120
 
252
121
  ```cpp
@@ -285,18 +154,6 @@ FdGuard guard{fd};
285
154
 
286
155
  ## References
287
156
 
288
- ### GIO/GLib Official Documentation
289
-
290
- - [GIO Reference](https://docs.gtk.org/gio/)
291
- - [GLib Reference](https://docs.gtk.org/glib/)
292
- - [GObject Memory Management](https://docs.gtk.org/gobject/concepts.html#reference-counting)
293
- - [GVolumeMonitor](https://docs.gtk.org/gio/class.VolumeMonitor.html)
294
- - [Unix Mounts](https://docs.gtk.org/gio-unix/struct.MountEntry.html)
295
-
296
- ### GLib Source Code
297
-
298
- - [gunixmounts.c](https://gitlab.gnome.org/GNOME/glib/-/blob/main/gio/gunixmounts.c) - Thread-safe mount enumeration implementation
299
-
300
157
  ### libblkid / util-linux
301
158
 
302
159
  - [libblkid(3) man page](https://man7.org/linux/man-pages/man3/libblkid.3.html)
@@ -5,6 +5,10 @@
5
5
  **Scope**: Complete codebase review including API verification against official documentation
6
6
  **Previous Audit**: October-December 2025 (see `doc/SECURITY_AUDIT_2025.md`)
7
7
 
8
+ > **Post-audit note (April 2026):** The Linux GIO/GLib integration discussed in
9
+ > re-verification findings #6 and #7 has since been removed entirely. See the
10
+ > "Removed" section of `CHANGELOG.md` for the rationale.
11
+
8
12
  ## Executive Summary
9
13
 
10
14
  This audit covers all changes since the December 2025 re-audit, focusing on the new
package/doc/gotchas.md CHANGED
@@ -133,6 +133,33 @@ FROM debian:bullseye
133
133
  RUN apt-get update && apt-get install -y nodejs npm
134
134
  ```
135
135
 
136
+ #### Electron Consumers Need libblkid Headers
137
+
138
+ **Problem**: Electron apps that bundle `@photostructure/fs-metadata` fail to build on Linux with:
139
+
140
+ ```
141
+ fatal error: blkid/blkid.h: No such file or directory
142
+ ```
143
+
144
+ …even though `npm install` reports that prebuilds were found.
145
+
146
+ **Why it happens**: `@electron/rebuild` (invoked by `electron-forge`, `electron-builder`, and friends) **always recompiles native modules from source** against Electron's bundled Node ABI. The Node-ABI prebuilds shipped in `prebuilds/` are not Electron-compatible and are ignored. The fresh compile then needs the same system headers a from-source build needs.
147
+
148
+ **Solution**: Install `libblkid-dev` (and friends) on the build machine before running `electron-rebuild` / `electron-forge package`:
149
+
150
+ ```bash
151
+ # Debian/Ubuntu
152
+ sudo apt-get install -y libblkid-dev
153
+
154
+ # Fedora/RHEL
155
+ sudo dnf install -y libblkid-devel
156
+
157
+ # Alpine
158
+ apk add blkid-dev
159
+ ```
160
+
161
+ In CI, add this as a step before `npm install` / the Electron package step. The same applies to any consumer that compiles from source for an unsupported architecture or glibc version. See [CONTRIBUTING.md](../CONTRIBUTING.md#on-ubuntudebian) for the full development dependency list.
162
+
136
163
  #### System Volume Filtering
137
164
 
138
165
  Many mount points on Linux are system-only:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photostructure/fs-metadata",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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",
@@ -31,9 +31,8 @@
31
31
  "clean:gyp-cache": "del-cli --force build %USERPROFILE%/.node-gyp ~/.node-gyp",
32
32
  "clean:native": "node-gyp clean",
33
33
  "node-gyp-rebuild": "node-gyp rebuild",
34
- "setup:native": "node scripts/setup-native.mjs",
35
- "build": "run-s setup:native build:native build:dist",
36
- "build:native": "npm run setup:native && tsx scripts/prebuildify-wrapper.ts",
34
+ "build": "run-s build:native build:dist",
35
+ "build:native": "tsx scripts/prebuildify-wrapper.ts",
37
36
  "build:linux-glibc": "bash scripts/prebuild-linux-glibc.sh",
38
37
  "build:dist": "tsup && node scripts/post-build.mjs",
39
38
  "docs": "typedoc",
@@ -96,28 +95,28 @@
96
95
  },
97
96
  "devDependencies": {
98
97
  "@types/jest": "^30.0.0",
99
- "@types/node": "^25.5.0",
98
+ "@types/node": "^25.6.0",
100
99
  "@types/semver": "^7.7.1",
101
100
  "cross-env": "^10.1.0",
102
101
  "del-cli": "^7.0.0",
103
102
  "eslint": "9.39.1",
104
103
  "eslint-plugin-regexp": "^3.1.0",
105
104
  "eslint-plugin-security": "^4.0.0",
106
- "globals": "^17.4.0",
105
+ "globals": "^17.5.0",
107
106
  "jest": "^30.3.0",
108
107
  "jest-environment-node": "^30.3.0",
109
108
  "jest-extended": "^7.0.0",
110
- "node-gyp": "^12.2.0",
111
- "npm-check-updates": "^19.6.6",
109
+ "node-gyp": "^12.3.0",
110
+ "npm-check-updates": "^22.0.1",
112
111
  "npm-run-all2": "8.0.4",
113
112
  "prebuildify": "^6.0.1",
114
- "prettier": "^3.8.1",
113
+ "prettier": "^3.8.3",
115
114
  "prettier-plugin-organize-imports": "4.3.0",
116
- "terser": "^5.46.1",
117
- "ts-jest": "^29.4.6",
115
+ "terser": "^5.46.2",
116
+ "ts-jest": "^29.4.9",
118
117
  "tsup": "^8.5.1",
119
118
  "tsx": "^4.21.0",
120
- "typedoc": "^0.28.18",
119
+ "typedoc": "^0.28.19",
121
120
  "typescript": "^5.9.3",
122
121
  "typescript-eslint": "^8.57.2"
123
122
  }
@@ -140,8 +140,6 @@ async function generateWindowsCompileCommands(): Promise<boolean> {
140
140
  console.log("Generating compile_commands.json for Windows...");
141
141
 
142
142
  try {
143
- execSync("npm run setup:native", { stdio: "inherit" });
144
-
145
143
  const nodeVersion = process.version.slice(1);
146
144
 
147
145
  // Try multiple possible locations for node-gyp headers
@@ -293,7 +291,7 @@ async function generateUnixCompileCommands(): Promise<void> {
293
291
  process.exit(1);
294
292
  }
295
293
 
296
- execSync("npm run setup:native && bear -- npm run node-gyp-rebuild", {
294
+ execSync("bear -- npm run node-gyp-rebuild", {
297
295
  stdio: "inherit",
298
296
  });
299
297
 
@@ -88,7 +88,7 @@ docker cp . "$CONTAINER_NAME:/tmp/project"
88
88
  docker exec "$CONTAINER_NAME" sh -c "
89
89
  cd /tmp/project && \
90
90
  apt-get update -qq && \
91
- apt-get install -y -qq build-essential python3 libglib2.0-dev libblkid-dev uuid-dev git && \
91
+ apt-get install -y -qq build-essential python3 libblkid-dev uuid-dev git && \
92
92
  # Verify versions
93
93
  echo 'Python version:' && python3 --version && \
94
94
  echo 'GCC version:' && gcc --version | head -1 && \
@@ -100,7 +100,6 @@ docker exec "$CONTAINER_NAME" sh -c "
100
100
  # Copy artifacts back
101
101
  docker cp "$CONTAINER_NAME:/tmp/project/prebuilds" . 2>/dev/null || true
102
102
  docker cp "$CONTAINER_NAME:/tmp/project/build" . 2>/dev/null || true
103
- docker cp "$CONTAINER_NAME:/tmp/project/config.gypi" . 2>/dev/null || true
104
103
 
105
104
  # Fix ownership (docker cp preserves container's root ownership)
106
105
  if [ -d prebuilds ]; then
@@ -109,9 +108,6 @@ fi
109
108
  if [ -d build ]; then
110
109
  chown -R "$(id -u):$(id -g)" build
111
110
  fi
112
- if [ -f config.gypi ]; then
113
- chown "$(id -u):$(id -g)" config.gypi
114
- fi
115
111
 
116
112
  # Clean up container
117
113
  docker rm -f "$CONTAINER_NAME" >/dev/null
@@ -76,7 +76,6 @@ fi
76
76
 
77
77
  # Build the native module
78
78
  echo "Building with AddressSanitizer..."
79
- npm run setup:native
80
79
  npm run clean:native
81
80
  npm run node-gyp-rebuild
82
81
 
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"
@@ -12,10 +13,6 @@
12
13
  #include "darwin/hidden.h"
13
14
  #elif defined(__linux__)
14
15
  #include "common/volume_metadata.h"
15
- #ifdef ENABLE_GIO
16
- #include "linux/gio_mount_points.h"
17
- #include "linux/gio_volume_metadata.h"
18
- #endif
19
16
  #endif
20
17
 
21
18
  namespace {
@@ -43,13 +40,6 @@ Napi::Value SetDebugPrefix(const Napi::CallbackInfo &info) {
43
40
  return env.Undefined();
44
41
  }
45
42
 
46
- #ifdef ENABLE_GIO
47
- Napi::Value GetGioMountPoints(const Napi::CallbackInfo &info) {
48
- const Napi::Env env = info.Env();
49
- return FSMeta::gio::GetMountPoints(env);
50
- }
51
- #endif
52
-
53
43
  #if defined(_WIN32) || defined(__APPLE__)
54
44
  // Fix: Remove extra parameter and use correct signature
55
45
  Napi::Value GetVolumeMountPoints(const Napi::CallbackInfo &info) {
@@ -78,6 +68,10 @@ Napi::Value SetHiddenAttribute(const Napi::CallbackInfo &info) {
78
68
  #endif
79
69
 
80
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
+
81
75
  exports.Set("setDebugLogging", Napi::Function::New(env, SetDebugLogging));
82
76
  exports.Set("setDebugPrefix", Napi::Function::New(env, SetDebugPrefix));
83
77
 
@@ -92,10 +86,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
92
86
  exports.Set("getMountPoint", Napi::Function::New(env, GetMountPointForPath));
93
87
  #endif
94
88
 
95
- #ifdef ENABLE_GIO
96
- exports.Set("getGioMountPoints", Napi::Function::New(env, GetGioMountPoints));
97
- #endif
98
-
99
89
  #if defined(_WIN32) || defined(__APPLE__)
100
90
  exports.Set("isHidden", Napi::Function::New(env, GetHiddenAttribute));
101
91
  exports.Set("setHidden", Napi::Function::New(env, SetHiddenAttribute));
@@ -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 Napi::AsyncWorker {
9
+ class MetadataWorkerBase : public SafeAsyncWorker {
9
10
  protected:
10
11
  std::string mountPoint;
11
12
  VolumeMetadata metadata;
@@ -13,17 +14,17 @@ protected:
13
14
 
14
15
  MetadataWorkerBase(const std::string &path,
15
16
  const Napi::Promise::Deferred &deferred)
16
- : Napi::AsyncWorker(deferred.Env()), mountPoint(path),
17
- deferred_(deferred) {}
17
+ : SafeAsyncWorker(deferred.Env()), mountPoint(path), deferred_(deferred) {}
18
18
 
19
19
  void OnError(const Napi::Error &error) override {
20
- deferred_.Reject(error.Value());
20
+ Napi::HandleScope scope(Env());
21
+ SafeReject(deferred_, error.Value());
21
22
  }
22
23
 
23
24
  void OnOK() override {
24
25
  Napi::HandleScope scope(Env());
25
- deferred_.Resolve(metadata.ToObject(Env()));
26
+ SafeResolve(deferred_, metadata.ToObject(Env()));
26
27
  }
27
28
  }; // class MetadataWorkerBase
28
29
 
29
- } // namespace FSMeta
30
+ } // namespace FSMeta
@@ -0,0 +1,162 @@
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 / napi_reject_deferred
8
+ // fail at that point (env tearing down), node-addon-api throws a C++
9
+ // 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 {
154
+ return FSMeta::IsShuttingDown(shutdownState_);
155
+ }
156
+
157
+ private:
158
+ std::shared_ptr<ShutdownState> shutdownState_;
159
+ std::string error_;
160
+ };
161
+
162
+ } // namespace FSMeta