From 3092773594eb79b4e8ce6891db9523be47543c17 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 29 Apr 2026 17:58:21 -0700 Subject: [PATCH 1/4] Replace native drivelist addon with PowerShell/proc enumeration drivelist is a native C++ addon that shells out to platform-specific tools to list drives. Replace it with direct PowerShell (Get-CimInstance Win32_LogicalDisk) on Windows and /proc/mounts parsing on Linux. The function is called infrequently (game discovery, staging folder selection) so the PowerShell startup cost is negligible. Resolves APP-403 --- etc/Dependency Report.md | 1 - flatpak/generated-sources.json | 7 -- pnpm-lock.yaml | 35 ------ pnpm-workspace.yaml | 2 - src/main/package.json | 1 - src/renderer/package.json | 1 - .../gamemode_management/util/getDriveList.ts | 103 +++++++++++------- 7 files changed, 64 insertions(+), 86 deletions(-) diff --git a/etc/Dependency Report.md b/etc/Dependency Report.md index 89fc6d28a9..5989764e01 100644 --- a/etc/Dependency Report.md +++ b/etc/Dependency Report.md @@ -43,7 +43,6 @@ This is a list of all modules leaked by Vortex to extensions. Any module listed | dequal | 2.0.3 | | dnd-core | 9.5.1 | | draggabilly | 2.4.1 | -| drivelist | 10.0.2 | | electron-context-menu | 3.6.1 | | electron-redux | 1.4.9-sync | | electron-updater | 4.6.5 | diff --git a/flatpak/generated-sources.json b/flatpak/generated-sources.json index a23239c443..1d254aba65 100644 --- a/flatpak/generated-sources.json +++ b/flatpak/generated-sources.json @@ -588,13 +588,6 @@ "dest-filename": "c67356006470e5066ea447e04a3968dca367339d", "dest": "flatpak-node/yarn-mirror" }, - { - "type": "file", - "url": "https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd", - "sha256": "02a2297326e2198a3d3194ff5516a1d18a81d9cb25a797a58322d68ac1014d44", - "dest-filename": "720d1890db11482ec05fc0f6aa176cfa6e6844dd", - "dest": "flatpak-node/yarn-mirror" - }, { "type": "file", "url": "https://codeload.github.com/TanninOne/electron-redux/tar.gz/66bbd9d389579806e8c4ebd87bd513a668cc64a8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aeb1aa2d1f..49aca9be5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,9 +373,6 @@ catalogs: draggabilly: specifier: ^2.2.0 version: 2.4.1 - drivelist: - specifier: git+https://github.com/TanninOne/drivelist#720d1890db11482ec05fc0f6aa176cfa6e6844dd - version: 10.0.2 electron: specifier: 41.3.0 version: 41.3.0 @@ -4045,9 +4042,6 @@ importers: draggabilly: specifier: 'catalog:' version: 2.4.1 - drivelist: - specifier: 'catalog:' - version: https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd electron-context-menu: specifier: 'catalog:' version: 3.6.1 @@ -4683,9 +4677,6 @@ importers: draggabilly: specifier: 'catalog:' version: 2.4.1 - drivelist: - specifier: 'catalog:' - version: https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd electron: specifier: 'catalog:' version: 41.3.0 @@ -7953,9 +7944,6 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - birecord@0.1.1: resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} @@ -8703,11 +8691,6 @@ packages: draggabilly@2.4.1: resolution: {integrity: sha512-HHHLPEPZqRXIDQDFRFdK7RONZausNlJ4WkA73ST7Z6O2HPWttxFHVwHo8nccuDLzXWwiVKRVuc6fTkW+CQA++A==} - drivelist@https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd: - resolution: {tarball: https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd} - version: 10.0.2 - engines: {node: '>=10'} - dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -9208,9 +9191,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} @@ -16068,10 +16048,6 @@ snapshots: big.js@5.2.2: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - birecord@0.1.1: {} birpc@4.0.0: {} @@ -16986,15 +16962,6 @@ snapshots: get-size: 2.0.3 unidragger: 2.4.0 - drivelist@https://codeload.github.com/TanninOne/drivelist/tar.gz/720d1890db11482ec05fc0f6aa176cfa6e6844dd: - dependencies: - bindings: 1.5.0 - debug: 3.2.7 - node-addon-api: 8.5.0 - prebuild-install: 7.1.0 - transitivePeerDependencies: - - supports-color - dts-resolver@2.1.3: {} dunder-proto@1.0.1: @@ -17752,8 +17719,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-uri-to-path@1.0.0: {} - filelist@1.0.6: dependencies: minimatch: 5.1.9 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0b7b503acb..7e93e3b0e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,7 +16,6 @@ allowBuilds: bsatk: true core-js: true crash-dump: true - drivelist: true electron: true esbuild: true @@ -154,7 +153,6 @@ catalog: dequal: ^2.0.3 dnd-core: ^9.4.0 draggabilly: ^2.2.0 - drivelist: git+https://github.com/TanninOne/drivelist#720d1890db11482ec05fc0f6aa176cfa6e6844dd electron: 41.3.0 electron-builder: 24.13.3 electron-context-menu: ^3.1.1 diff --git a/src/main/package.json b/src/main/package.json index bd903ea55c..6c2f0ba002 100644 --- a/src/main/package.json +++ b/src/main/package.json @@ -55,7 +55,6 @@ "dequal": "catalog:", "dnd-core": "catalog:", "draggabilly": "catalog:", - "drivelist": "catalog:", "electron-context-menu": "catalog:", "electron-redux": "catalog:", "electron-updater": "catalog:", diff --git a/src/renderer/package.json b/src/renderer/package.json index aba8f7a037..a59b81ef13 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -97,7 +97,6 @@ "dequal": "catalog:", "dnd-core": "catalog:", "draggabilly": "catalog:", - "drivelist": "catalog:", "electron": "catalog:", "electron-context-menu": "catalog:", "electron-redux": "catalog:", diff --git a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts index d6d5b2d27d..79b5411cae 100644 --- a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts +++ b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts @@ -1,50 +1,75 @@ -import type { list as listT } from "drivelist"; +import { execFile } from "child_process"; +import { readFile } from "fs/promises"; import type { IExtensionApi } from "../../../types/IExtensionContext"; +/** + * Returns mount paths for fixed (non-removable) system drives. + * Uses PowerShell on Windows and /proc/mounts on Linux — no native addons needed. + */ function getDriveList(api: IExtensionApi): Promise { - let list: typeof listT; - try { - list = require("drivelist").list; - if (typeof list !== "function") { - throw new Error('Failed to load "drivelist" module'); - } - } catch (err) { + const impl = + process.platform === "win32" ? getFixedDrivesWindows : getFixedDrivesLinux; + + return impl().catch((err) => { api.showErrorNotification( - "Failed to query list of system drives", - { - message: - "Vortex was not able to query the operating system for the list of system drives. " + - "If this error persists, please configure the list manually.", - error: err, - }, + "Failed to determine list of disk drives. " + + "Please review the settings before scanning for games.", + err, { allowReport: false }, ); - return Promise.resolve(["C:"]); - } + return ["C:"]; + }); +} + +function getFixedDrivesWindows(): Promise { + return new Promise((resolve, reject) => { + execFile( + "powershell.exe", + [ + "-NoProfile", + "-Command", + "Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | Select-Object -ExpandProperty DeviceID", + ], + (err, stdout) => { + if (err) { + return reject(err); + } + const drives = stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((d) => /^[A-Z]:$/i.test(d)); + + resolve(drives.length > 0 ? drives : ["C:"]); + }, + ); + }); +} - return list() - .then((disks) => - disks - .sort() - .filter((disk) => disk.isSystem && !disk.isRemovable) - .reduce((prev, disk) => { - if (disk.mountpoints) { - prev.push(...disk.mountpoints.map((mp) => mp.path)); - } else if (disk["mountpoint"] !== undefined) { - prev.push(disk["mountpoint"]); - } - return prev; - }, []), +async function getFixedDrivesLinux(): Promise { + const raw = await readFile("/proc/mounts", "utf8"); + const drives = raw + .split("\n") + .filter((line) => line.length > 0) + .map((line) => { + const parts = line.split(" "); + return { device: parts[0], mountpoint: parts[1], fstype: parts[2] }; + }) + .filter( + ({ device, fstype }) => + device.startsWith("/dev/") && + !device.startsWith("/dev/loop") && + ["ext4", "btrfs", "xfs", "zfs", "ext3", "ext2", "f2fs"].includes( + fstype, + ), ) - .catch((err) => { - api.showErrorNotification( - "Failed to determine list of disk drives. " + - "Please review the settings before scanning for games.", - err, - { allowReport: false }, - ); - return ["C:"]; - }); + .map(({ mountpoint }) => + // Unescape octal sequences in mount paths (e.g. \040 for space) + mountpoint.replace(/\\([0-7]{3})/g, (_, oct) => + String.fromCharCode(parseInt(oct, 8)), + ), + ); + + return drives.length > 0 ? drives : ["/"]; } export default getDriveList; From a3193a2b762a6b1dfe177732d18d7677e74207b4 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Thu, 30 Apr 2026 10:55:18 -0700 Subject: [PATCH 2/4] Replace PowerShell shelling with koffi FFI for drive enumeration Use GetLogicalDriveStringsW + GetDriveTypeW from kernel32.dll via koffi instead of spawning powershell.exe. This is instant (no process spawn overhead), consistent with the koffi approach used elsewhere, and calls the same Win32 APIs that the old native drivelist module wrapped. --- pnpm-lock.yaml | 8 ++ pnpm-workspace.yaml | 1 + src/renderer/package.json | 1 + .../gamemode_management/util/getDriveList.ts | 82 +++++++++++++------ 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49aca9be5e..89d36bf889 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4746,6 +4746,9 @@ importers: jsonwebtoken: specifier: 'catalog:' version: 9.0.3 + koffi: + specifier: ^2.9.0 + version: 2.16.1 lodash: specifier: 'catalog:' version: 4.17.23 @@ -10072,6 +10075,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + koffi@2.16.1: + resolution: {integrity: sha512-0Ie6CfD026dNfWSosDw9dPxPzO9Rlyo0N8m5r05S8YjytIpuilzMFDMY4IDy/8xQsTwpuVinhncD+S8n3bcYZQ==} + layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -18712,6 +18718,8 @@ snapshots: kind-of@6.0.3: {} + koffi@2.16.1: {} + layout-base@1.0.2: {} lazy-val@1.0.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7e93e3b0e4..be3ddfe694 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,6 +19,7 @@ allowBuilds: electron: true esbuild: true + koffi: true font-scanner: true gamebryo-savegame: true leveldown: true diff --git a/src/renderer/package.json b/src/renderer/package.json index a59b81ef13..45cf4e5176 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -119,6 +119,7 @@ "immutability-helper": "catalog:", "interweave": "catalog:", "is-admin": "catalog:", + "koffi": "^2.9.0", "json-loader": "catalog:", "json-socket": "catalog:", "jsonwebtoken": "catalog:", diff --git a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts index 79b5411cae..ff60c4044a 100644 --- a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts +++ b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts @@ -1,10 +1,9 @@ -import { execFile } from "child_process"; import { readFile } from "fs/promises"; import type { IExtensionApi } from "../../../types/IExtensionContext"; /** * Returns mount paths for fixed (non-removable) system drives. - * Uses PowerShell on Windows and /proc/mounts on Linux — no native addons needed. + * Uses koffi FFI to call Win32 APIs on Windows, /proc/mounts on Linux. */ function getDriveList(api: IExtensionApi): Promise { const impl = @@ -21,28 +20,65 @@ function getDriveList(api: IExtensionApi): Promise { }); } +/** + * Enumerate fixed drives via Win32 GetLogicalDriveStringsW + GetDriveTypeW. + * + * GetLogicalDriveStringsW returns a double-null-terminated buffer of drive + * root paths ("C:\\\0D:\\\0\0"). GetDriveTypeW returns the type for each; + * DRIVE_FIXED (3) is what we want. + */ function getFixedDrivesWindows(): Promise { - return new Promise((resolve, reject) => { - execFile( - "powershell.exe", - [ - "-NoProfile", - "-Command", - "Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | Select-Object -ExpandProperty DeviceID", - ], - (err, stdout) => { - if (err) { - return reject(err); - } - const drives = stdout - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((d) => /^[A-Z]:$/i.test(d)); - - resolve(drives.length > 0 ? drives : ["C:"]); - }, - ); - }); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const koffi = require("koffi"); + const kernel32 = koffi.load("kernel32.dll"); + + const GetLogicalDriveStringsW = kernel32.func( + "uint32 GetLogicalDriveStringsW(uint32 nBufferLength, uint16 *lpBuffer)", + ); + const GetDriveTypeW = kernel32.func( + "uint32 GetDriveTypeW(const uint16 *lpRootPathName)", + ); + + const DRIVE_FIXED = 3; + + // Buffer for drive strings — 26 drives * 4 chars ("X:\\\0") = 104 UTF-16 code units max + const buf = Buffer.alloc(256); + const len = GetLogicalDriveStringsW(buf.length / 2, buf) as number; + + if (len === 0) { + return Promise.resolve(["C:"]); + } + + const drives: string[] = []; + let offset = 0; + + while (offset < len * 2) { + // Read null-terminated UTF-16LE string + const codes: number[] = []; + while (offset < buf.length) { + const code = buf.readUInt16LE(offset); + offset += 2; + if (code === 0) break; + codes.push(code); + } + if (codes.length === 0) break; + + const rootPath = String.fromCharCode(...codes); // e.g. "C:\\" + + // Check drive type + const widePath = Buffer.alloc((rootPath.length + 1) * 2); + for (let i = 0; i < rootPath.length; i++) { + widePath.writeUInt16LE(rootPath.charCodeAt(i), i * 2); + } + + const driveType = GetDriveTypeW(widePath) as number; + if (driveType === DRIVE_FIXED) { + // Strip trailing backslash: "C:\\" → "C:" + drives.push(rootPath.replace(/\\$/, "")); + } + } + + return Promise.resolve(drives.length > 0 ? drives : ["C:"]); } async function getFixedDrivesLinux(): Promise { From 05605875533cf5e81615e6aca18084f3c83c3d91 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 6 May 2026 06:44:13 -0600 Subject: [PATCH 3/4] Update src/renderer/src/extensions/gamemode_management/util/getDriveList.ts Co-authored-by: erri120 --- .../src/extensions/gamemode_management/util/getDriveList.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts index ff60c4044a..4a65f356fd 100644 --- a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts +++ b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts @@ -28,8 +28,7 @@ function getDriveList(api: IExtensionApi): Promise { * DRIVE_FIXED (3) is what we want. */ function getFixedDrivesWindows(): Promise { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const koffi = require("koffi"); + const koffi = await import("koffi"); const kernel32 = koffi.load("kernel32.dll"); const GetLogicalDriveStringsW = kernel32.func( From db1d3776d35e0470e57c429ca960f1b55504c4b1 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 6 May 2026 15:44:20 -0700 Subject: [PATCH 4/4] Add missing async keyword to getFixedDrivesWindows --- .../gamemode_management/util/getDriveList.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts index 4a65f356fd..3dbf1dcf6a 100644 --- a/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts +++ b/src/renderer/src/extensions/gamemode_management/util/getDriveList.ts @@ -1,4 +1,5 @@ import { readFile } from "fs/promises"; + import type { IExtensionApi } from "../../../types/IExtensionContext"; /** @@ -6,8 +7,7 @@ import type { IExtensionApi } from "../../../types/IExtensionContext"; * Uses koffi FFI to call Win32 APIs on Windows, /proc/mounts on Linux. */ function getDriveList(api: IExtensionApi): Promise { - const impl = - process.platform === "win32" ? getFixedDrivesWindows : getFixedDrivesLinux; + const impl = process.platform === "win32" ? getFixedDrivesWindows : getFixedDrivesLinux; return impl().catch((err) => { api.showErrorNotification( @@ -27,16 +27,14 @@ function getDriveList(api: IExtensionApi): Promise { * root paths ("C:\\\0D:\\\0\0"). GetDriveTypeW returns the type for each; * DRIVE_FIXED (3) is what we want. */ -function getFixedDrivesWindows(): Promise { +async function getFixedDrivesWindows(): Promise { const koffi = await import("koffi"); const kernel32 = koffi.load("kernel32.dll"); const GetLogicalDriveStringsW = kernel32.func( "uint32 GetLogicalDriveStringsW(uint32 nBufferLength, uint16 *lpBuffer)", ); - const GetDriveTypeW = kernel32.func( - "uint32 GetDriveTypeW(const uint16 *lpRootPathName)", - ); + const GetDriveTypeW = kernel32.func("uint32 GetDriveTypeW(const uint16 *lpRootPathName)"); const DRIVE_FIXED = 3; @@ -93,15 +91,11 @@ async function getFixedDrivesLinux(): Promise { ({ device, fstype }) => device.startsWith("/dev/") && !device.startsWith("/dev/loop") && - ["ext4", "btrfs", "xfs", "zfs", "ext3", "ext2", "f2fs"].includes( - fstype, - ), + ["ext4", "btrfs", "xfs", "zfs", "ext3", "ext2", "f2fs"].includes(fstype), ) .map(({ mountpoint }) => // Unescape octal sequences in mount paths (e.g. \040 for space) - mountpoint.replace(/\\([0-7]{3})/g, (_, oct) => - String.fromCharCode(parseInt(oct, 8)), - ), + mountpoint.replace(/\\([0-7]{3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8))), ); return drives.length > 0 ? drives : ["/"];